BAD SKY.NET

application_layer_protocol_negotiation

ALPN: the TLS extension where the client lists the protocols it speaks, such as h2 and http/1.1, so HTTP/2 is agreed in the handshake.

TLS extensionCode 0x0010 (16)

What it is

application_layer_protocol_negotiation (extension 16), usually called ALPN, is defined in RFC 7301. The client lists the application protocols it can speak over this TLS connection, in order of preference, for example h2 for HTTP/2 and http/1.1. The server picks one and returns it in its reply.

Agreeing on the protocol inside the handshake saves a round trip: as soon as TLS is set up, both sides know whether they will talk HTTP/2 or HTTP/1.1. HTTP/2 over TLS requires ALPN, and HTTP/3 uses it too, with the h3 value over QUIC.

ALPN replaced the older NPN extension (next_protocol_negotiation), which Google used for SPDY. Like SNI, the ALPN list is sent in the clear unless Encrypted Client Hello is used.

Proxies and load balancers that terminate TLS read this list to decide whether they can serve HTTP/2.

Who sends it

Chrome 153 (macOS) browser Sent at position 3
curl_cffi (impersonate chrome) Sent at position 9
curl-impersonate chrome116 Sent at position 10
curl-impersonate ff117 Sent at position 7
curl 8.7 (macOS) Sent at position 7
curl 7.88 (Debian 12) Sent at position 4
Python requests Sent at position 5
Python httpx Sent at position 6
Python httpx (HTTP/2) Sent at position 6
Go net/http Sent at position 9
Node.js fetch Sent at position 6
PHP Guzzle Sent at position 5

Measured on 2026-10-25 against our own test server: sent by 12 of 12 clients. Position counts from 1 and ignores GREASE values.

Why it matters for fingerprinting

Every measured client sends ALPN, but the list differs. Browsers and Chrome impersonators offer h2 first. Python requests and plain httpx offer only http/1.1, and httpx with HTTP/2 enabled still lists http/1.1 first. JA4 records the first ALPN value in its prefix, which is why a Python client shows h1 there while Chrome shows h2.

The extension position varies as well: 3rd in our Chrome capture (Chrome shuffles), 4th for Debian curl, 5th for Python requests and Guzzle, 6th for httpx and Node.js fetch, 9th for Go and curl_cffi.

Check whether your client sends it

Open the test from your browser, or call it from your script, and look for application_layer_protocol_negotiation in the TLS details.

Run the TLS fingerprint test

References

Related