Every TCP connection starts with a SYN, and that first packet says a lot about the machine that sent it. The Linux, Windows, macOS and Android stacks each set different flags, choose different initial TTLs, advertise different receive windows, and, above all, lay out their TCP options in different orders. Passive TCP fingerprinting turns those differences into a compact signature.
Today we are announcing the nDPI TCP Fingerprint (NTF), a modern, open, patent-free TCP fingerprint format that has been included in nDPI for a few years now. It is implemented in nDPI and specified in a public draft (spec, v0.4). This post covers what NTF is, how it differs from other popular TCP fingerprints (JA4T and MuonFP), and how to use it alongside JA5 to reduce false positives and catch inconsistencies.
Why another TCP fingerprint?
Existing formats have three limitations:
- Instability. If a fingerprint includes values that change with the path or the session, the same device produces many different fingerprints. This inflates fingerprint databases and makes matching brittle.
- Licensing. Some fingerprinting methods come with license terms that restrict certain commercial uses, which is a problem if you want to embed them in a product.
- Limited coverage. Some formats ignore signals such as ECN negotiation (TCP flags) and the initial TTL, which are strong OS discriminators.
NTF addresses all three.
Open and patent-free
NTF is a patent-free format. The specification is released under CC BY 4.0, so anyone can implement, adapt and redistribute it with attribution. The reference implementation in nDPI is released under the LGPLv3. Third parties can build producers that generate byte-identical fingerprints, since the spec documents the exact algorithm, its behavior on malformed input, and a set of test vectors, plus a reference implementation in Python.
For comparison, the JA4+ suite publishes JA4 (TLS client) under the BSD 3-Clause license, while the other JA4+ methods, including JA4T, are under the FoxIO License 1.1, which restricts some commercial uses. If you plan to embed a fingerprint in a commercial product, check the terms of each format first.
What an NTF looks like
NTF is computed from a single packet, the first SYN of a flow. It needs no completed handshake, no payload, and no bidirectional visibility. The result is a short ASCII string with four underscore-separated fields:
<tcp_flags>_<ttl_bucket>_<tcp_window>_<options_hash>
| Field | What it captures |
|---|---|
tcp_flags | The numeric 12-bit flag field. This exposes ECN negotiation (2 = plain SYN, 194 = SYN+ECE+CWR) and AccECN-capable stacks (450). |
ttl_bucket | The observed TTL / Hop Limit rounded up to 32, 64, 128, 192 or 255. This recovers the likely initial TTL and removes the dependence on hop count. |
tcp_window | The advertised window, verbatim and unscaled (window scaling doesn’t apply to SYNs). |
options_hash | The first 48 bits of a SHA-256 over a hex serialization of the TCP options in wire order. |
For example, a Linux client sending an IPv4 SYN produces:
2_64_64240_5ec4846073b9
ndpiReader prints it together with the OS hint:
[TCP Fingerprint: 2_64_64240_5ec4846073b9/Linux]
Designed for Stability
NTF follows the same philosophy as JA5, which strips ephemeral inputs from TLS fingerprints: one stack should produce one fingerprint. NTF removes every input that is a property of the path, the connection or the destination, rather than of the sending stack:
| Removed from the hash | Why |
|---|---|
| MSS value | Depends on path MTU, IPv4 vs IPv6, VPNs and MSS clamping |
| Timestamp values | Per-connection clock and echo |
| TCP-MD5 / TCP-AO MACs | Per-segment digests |
| MPTCP keys, tokens, nonces | Per-connection. The version and flags are kept as a stack signal. |
| TCP Fast Open (including its NOP padding) | Depends on the per-server cookie cache |
| Everything after the first EOL | Padding |
What remains is what actually identifies the stack: the flags, the initial TTL, the window, the option layout and order (including NOP placement), and constant option values such as the window-scale shift.
Same Stack, Same Fingerprint
The Linux stack shows the effect well. Here is the same operating system in different situations:
| Case | NTF |
|---|---|
| Linux, IPv4 | 2_64_64240_5ec4846073b9 |
| Linux, IPv4, with a TFO cookie request | 2_64_64240_5ec4846073b9 |
| Linux, IPv4, with a TFO cookie | 2_64_64240_5ec4846073b9 |
| Linux, IPv6 (different MSS and window) | 2_64_64800_5ec4846073b9 |
The options hash stays identical across all four. The IPv6 case differs only in the window field, because Linux picks the largest multiple of the MSS that fits in 65535 (64240 = 44×1460, 64800 = 45×1440). Consumers that need to match across IPv4 and IPv6 can compare only the flags, TTL and options-hash fields.
The effect on the nDPI built-in database was substantial. When we converted the database to the new format, 24 of the 63 distinct previous fingerprints differed from another entry only in the MSS value. The 65 active entries collapsed into 46. On the nDPI regression corpus (4,524 fingerprinted flows), distinct fingerprints dropped from 141 to 115, the number of flows with an OS hint rose from 1,878 to 1,907, and no flow changed from one OS to another.
The spec also documents the remaining sources of drift: TTLs near a bucket edge, and middleboxes that bleach ECN bits (which flips the flags field between 194 and 2). It also handles crafted or malformed options deterministically, so a crafted SYN yields a well-defined fingerprint instead of undefined behavior.
How NTF Compares to Other Formats
NTF sits alongside two other passive TCP fingerprints that use the same SYN-derived inputs: JA4T from FoxIO’s JA4+ suite, and MuonFP, which nDPI also supports (metadata.tcp_fingerprint_format = 1).
| Aspect | NTF | JA4T | MuonFP |
|---|---|---|---|
| Packet | Client SYN | Client SYN (JA4TS: server SYN-ACK) | Client SYN |
| Layout | flags_ttl_win_hash12 | window_kinds_mss_wscale | window:kinds:mss:wscale |
| Human-readable | No (needs the raw string) | Yes | Yes |
| Length | Fixed | Variable | Variable |
| TCP flags (ECN) | Included | Not included | Not included |
| Initial TTL | Included (bucketed) | Not included | Not included |
| MSS | Excluded | Included | Included |
| Window-scale shift | Included (in hash) | Included | Included |
| TFO / ephemeral options | Removed, with NOP padding | Kept as option kinds | Kept as option kinds |
| Other option values (MPTCP flags, ExIDs, unknown options) | Included (in hash) | Kind only | Kind only |
A few points from the comparison:
- The MSS trade-off is deliberate. JA4T keeps the MSS on purpose, because an MSS below what the link MTU would allow can reveal VPNs, tunnels and proxies on the path. NTF drops it so that one stack yields one fingerprint regardless of path. NTF is therefore the better key for OS and stack identification and for database lookups, while JA4T is the better signal for path analysis. The two are complementary.
- NTF sees more of the stack. A macOS SYN with and without ECN negotiation gets the same JA4T, but NTF separates them (
2_64_65535_fa6f8edaadebvs194_64_65535_fa6f8edaadeb). NTF also hashes MPTCP versions and flags, RFC 6994 experiment IDs and the values of unknown options, which the other two record only by kind. - JA4T sees TFO; NTF deliberately does not. A client’s JA4T changes depending on whether it holds a TFO cookie for a given server. This is the same session-state drift that JA5 removes for TLS, and NTF removes it for TCP.
- Readability vs compactness. JA4T and MuonFP can be read and wildcarded by eye. NTF’s fixed layout suits hash-table lookups and flow export, and you can export the pre-hash raw options string (
metadata.tcp_fingerprint_raw) when you need to audit or rebuild a database.
NTF + JA5: Better Together
TLS fingerprints such as JA5 describe the application’s TLS library and configuration. NTF describes the operating system’s TCP/IP stack. Because they come from different layers, combining them gives you information that neither has on its own.
Reducing False Positives
Many applications share the same TLS library and configuration, so a TLS fingerprint alone can be ambiguous. Adding the TCP fingerprint lets you check whether the TLS fingerprint is plausible for the OS that the flow appears to come from. This narrows candidate matches and cuts false positives in classification and detection rules. For this reason, nDPI uses NTF by default as the L4 component of its composite client fingerprint. You can turn this off with metadata.ndpi_fingerprint_ignore_tcp_fp.
Detecting Inconsistencies
A mismatch between layers is often more interesting than either layer alone. Suppose a flow’s TLS fingerprint matches Apple Mail, but the TCP fingerprint of the same flow is 2_64_65535_5ec4846073b9, which nDPI’s built-in database maps to Android. Apple Mail doesn’t run on Android, so something is off:
- a spoofed TLS client fingerprint,
- a tool or proxy re-originating the connection, or
- a mislabeled or misconfigured device.
The same reasoning applies to a “Windows browser” whose SYN carries a Linux or network-device stack, or to a fleet device that suddenly presents a different stack.
Two caveats apply:
- The OS hint is a hint, not an identification. The mapping is many-to-one, and one fingerprint may legitimately match several OS families (for example, macOS and iOS/iPadOS share an options hash).
- Every NTF input is sender-controlled, so a crafted SYN can impersonate any fingerprint. NTF must not be used as an authentication signal. It works best as one input to a scoring or correlation pipeline.
Bonus: Scanner Detection
While computing NTF, nDPI also flags characteristic scanner behavior. A SYN with no options at all raises a “Massive scanner detected” risk (with a masscan/zmap hint based on the window), and an MSS-only SYN from a public address raises “Unusual TCP fingerprint (scanner detected?)”. These risks are independent of the fingerprint string.
Getting Started
NTF is the default TCP fingerprint in nDPI (metadata.tcp_fingerprint_format = 0).
- C API:
flow->metadata.l4.tcp.fingerprint,flow->metadata.l4.tcp.fingerprint_raw, andflow->metadata.l4.tcp.os_hint - JSON / TLV export:
tcp_fingerprintandtcp_fingerprint_raw - ndpiReader:
[TCP Fingerprint: <NTF>/<OS>] - Wireshark: the
ntop.tcp_fingerprintfield viawireshark/ndpi.lua - Custom databases: load your own entries with
ndpi_add_tcp_fingerprint()orndpi_load_tcp_fingerprint_file(). Each line has the form<NTF>,<numeric ndpi_os>, for example2_64_14600_b88686e220ac,5.
If you maintain a custom fingerprint database built with an earlier nDPI version, it must be regenerated. This revision of the format changed the option serialization, so old and new fingerprints are not comparable.
The full specification, including the ABNF grammar, the serialization algorithm, malformed-input behavior, test vectors and a Python reference implementation, is in the nDPI repository. The spec is currently a draft (v0.4), and we welcome feedback, test vectors from new stacks, and independent implementations.
Enjoy !
