Introducing the nDPI TCP Fingerprint: A Stable, Patent-Free Way to Fingerprint TCP Stacks

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:

  1. 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.
  2. 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.
  3. 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>
FieldWhat it captures
tcp_flagsThe numeric 12-bit flag field. This exposes ECN negotiation (2 = plain SYN, 194 = SYN+ECE+CWR) and AccECN-capable stacks (450).
ttl_bucketThe 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_windowThe advertised window, verbatim and unscaled (window scaling doesn’t apply to SYNs).
options_hashThe 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 hashWhy
MSS valueDepends on path MTU, IPv4 vs IPv6, VPNs and MSS clamping
Timestamp valuesPer-connection clock and echo
TCP-MD5 / TCP-AO MACsPer-segment digests
MPTCP keys, tokens, noncesPer-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 EOLPadding

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:

CaseNTF
Linux, IPv42_64_64240_5ec4846073b9
Linux, IPv4, with a TFO cookie request2_64_64240_5ec4846073b9
Linux, IPv4, with a TFO cookie2_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).

AspectNTFJA4TMuonFP
PacketClient SYNClient SYN (JA4TS: server SYN-ACK)Client SYN
Layoutflags_ttl_win_hash12window_kinds_mss_wscalewindow:kinds:mss:wscale
Human-readableNo (needs the raw string)YesYes
LengthFixedVariableVariable
TCP flags (ECN)IncludedNot includedNot included
Initial TTLIncluded (bucketed)Not includedNot included
MSSExcludedIncludedIncluded
Window-scale shiftIncluded (in hash)IncludedIncluded
TFO / ephemeral optionsRemoved, with NOP paddingKept as option kindsKept as option kinds
Other option values (MPTCP flags, ExIDs, unknown options)Included (in hash)Kind onlyKind 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_fa6f8edaadeb vs 194_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, and flow->metadata.l4.tcp.os_hint
  • JSON / TLV export: tcp_fingerprint and tcp_fingerprint_raw
  • ndpiReader: [TCP Fingerprint: <NTF>/<OS>]
  • Wireshark: the ntop.tcp_fingerprint field via wireshark/ndpi.lua
  • Custom databases: load your own entries with ndpi_add_tcp_fingerprint() or ndpi_load_tcp_fingerprint_file(). Each line has the form <NTF>,<numeric ndpi_os>, for example 2_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 !

Share