Skip to content

fix(python): handle ALPN edge cases like the rust implementation - #322

Open
nkbeast wants to merge 2 commits into
FoxIO-LLC:mainfrom
nkbeast:fix/python-alpn-edge-cases
Open

fix(python): handle ALPN edge cases like the rust implementation#322
nkbeast wants to merge 2 commits into
FoxIO-LLC:mainfrom
nkbeast:fix/python-alpn-edge-cases

Conversation

@nkbeast

@nkbeast nkbeast commented Sep 4, 2026

Copy link
Copy Markdown

An empty ALPN value — a zero-length protocol name inside the ALPN extension — made ord(alpn[0]) raise IndexError in to_ja4() and to_ja4s():

  File "ja4.py", line 206, in to_ja4s
    if ord(alpn[0]) > 127:
IndexError: string index out of range

tshark -T ek reports tls.handshake.extensions_alpn_str as an empty string when the ALPN extension carries a zero-length name, so one ServerHello like that aborts the whole run in to_ja4s() (it isn't guarded in main()), losing every fingerprint from the capture. In to_ja4() the exception is swallowed and streams with an empty client-side ALPN silently disappear from the output.

Two smaller divergences from the rust implementation came from the same block: a single-character ALPN rendered as one character instead of two (x instead of x0), and non-ascii was only checked at position 0 (éx became 99 where rust yields 9x).

The handling now mirrors first_last() in rust/ja4/src/tls.rs: first and last character of the value, non-ascii replaced with 9 per character, a single character padded with 0, and an empty value treated like a missing extension (00). to_ja4() and to_ja4s() share one helper now.

NK added 2 commits September 5, 2026 02:16
An empty ALPN value (a zero-length protocol name inside the ALPN
extension) made ord(alpn[0]) raise IndexError in to_ja4 and to_ja4s.
to_ja4s is not guarded in main(), so one ServerHello with an empty ALPN
aborted the whole run and every fingerprint from the capture was lost;
in to_ja4 the exception was swallowed and the stream silently dropped
from the output.

A single-character ALPN also rendered as one character where the
fingerprint expects two, and non-ascii was only checked at the front,
so "ex" became "99" while the rust implementation yields "9x".

Mirror first_last() from rust/ja4/src/tls.rs: keep the first and last
character, replace a non-ascii character with '9' per character, pad a
single character with '0', and treat an empty value like a missing
extension ('00').
@vlvkobal

Copy link
Copy Markdown
Member

Thanks for fixing the empty-ALPN crash - that part looks good. I’m not sure Rust is the right reference for these edge cases, though.

For a one-character ALPN, the current spec treats the character as both first and last ("x" -> "xx"), and that’s also what Wireshark and Zeek do. Rust’s "x0" is the odd one out.

The non-alphanumeric handling differs too: the spec and Wireshark use the hex fallback, Rust replaces characters with "9", and Zeek leaves them as-is. So matching Rust here would still leave Python inconsistent with the documented behavior and the other implementations.

Could you take another look at these cases? Feel free to open separate issues for inconsistencies in the other implementations if they aren’t already tracked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants