fanything
June 19, 2026 · View on GitHub
Warning
The format is still in alpha design. If you see anything concerning the extensibility of the format, let us know.
fanything is an awesome, patent-unencumbered fingerprinting format for correlating SSH, TLS, DTLS, X.509 certificate, QUIC, IKE, RDP, and TCP/IP stack behavior.
The repository defines an algorithm named FAN/1 (Flexible Anything Network fingerprint, version 1). FAN/1 is intentionally simple: each fingerprint starts with an explicit namespace, keeps a readable normalized feature string for analysts, appends a stable SHA-256 digest for indexing in systems such as MISP, and can include a 128-bit SimHash companion value for similarity comparisons.
FAN/1 fingerprint format
A fingerprint has five semantic parts:
fan1:<protocol>:<role>:<mode>:<base64url-normalized-features>:<digest-algorithm>:<hex-digest>
fan1identifies the algorithm version.<protocol>is currentlytls,dtls,x509,ssh,quic,ike,rdp, ortcpip, but the namespace can be extended to other services and transport behaviors.<role>identifies handshake direction, for exampleclient,server, orpeer.<mode>isactiveorpassive.<base64url-normalized-features>is the normalized canonical feature string, encoded without padding so it remains safe in JSON, CSV, MISP attributes, and URLs.<digest-algorithm>identifies the digest that follows.sha256is the required exact-match digest.simhash128is the optional similarity digest.<hex-digest>is the lowercase hexadecimal digest value for the named algorithm. Forsha256, it is SHA-256 over the unencoded canonical feature string.
The canonical FAN/1 fingerprint string should use sha256 when exact identity
is required:
fan1:<protocol>:<role>:<mode>:<base64url-normalized-features>:sha256:<hex-digest>
Collectors may also calculate a simhash128 value over normalized feature
tokens and expose it either as metadata or as an alternate FAN/1 string with the
simhash128 digest algorithm. SimHash is not a cryptographic identity digest;
it is useful because the Hamming distance between two 128-bit SimHashes can be
used to estimate how close two canonical feature strings are. This helps
analysts find related client, server, or peer behaviors that differ by a few
negotiated algorithms or extensions.
The JSON output also includes the plain features field so analysts can inspect
and pivot on individual parts without decoding the fingerprint.
Active and passive collection
FAN/1 fingerprints can be collected in two modes:
passivemeans the collector does not interact with the endpoint. It only parses traffic that was already observed, for example from a pcap file.fanfp.pyis passive and emits"mode":"passive"in each JSON object and in each FAN/1 fingerprint prefix.activemeans the collector creates its own network interaction with the endpoint. The NSE scriptsfanything-tls.nse,fanything-dtls.nse, andfanything-ssh.nseare active: they connect to the target, send protocol probes, emitmode: activein Nmap output, and embedactivein each FAN/1 fingerprint prefix.
The collection mode is part of the FAN/1 prefix and output metadata, but not the
normalized feature string. Store mode as metadata too, so active probes and
passive observations can be filtered without parsing the fingerprint.
Role model
The role component describes which side of the protocol behavior is being
fingerprinted. Keeping the role in the primary fingerprint string prevents
collisions between values that may have similar feature lists but very different
correlation meaning.
clientis used when the observed handshake message is an initiator proposal. For TLS and DTLS this means the ClientHello: supported protocol versions, cipher suite order, extension order, supported groups, signature algorithms, and ALPN values. Aclientfingerprint answers: "what implementation or tool appears to be initiating connections?"serveris used when the observed handshake message is a responder selection. For TLS and DTLS this means the ServerHello: selected protocol version, selected cipher suite, and server extensions. Aserverfingerprint answers: "what service behavior appears to be answering connections?"peeris used when the protocol exposes comparable active-handshake characteristics from both sides, or when the extractor cannot reliably assign a stricter initiator/responder label from a single payload. SSH usespeerbecause both endpoints send an identification string and aSSH_MSG_KEXINITpacket with the same family of proposal lists. Apeerfingerprint answers: "what SSH implementation behavior did this endpoint advertise?"- For TCP/IP stack fingerprints,
clientidentifies a SYN without ACK andserveridentifies a SYN-ACK. These roles answer: "what TCP/IP stack behavior appeared in the connection setup packet?" - For RDP X.224 fingerprints,
clientidentifies a Connection Request andserveridentifies a Connection Confirm.
In practical correlation, treat client, server, and peer as different
attribute types even when the same protocol is involved. For example, a TLS
client fingerprint should be correlated with other TLS client observations, not
with TLS server observations, because the canonical feature strings represent
different handshake semantics.
TLS client fingerprints
TLS client fingerprints are built from ClientHello fields:
tls|client|v=<legacy_version>|c=<cipher_suites>|e=<extensions>|g=<supported_groups>|p=<ec_point_formats>|sv=<supported_versions>|alpn=<alpn_protocols>|sig=<signature_algorithms>
TLS server fingerprints are built from ServerHello fields:
tls|server|v=<legacy_version>|c=<selected_cipher>|e=<extensions>|sv=<selected_supported_version>
Active TLS fingerprints come from probes generated by a scanner. Passive TLS fingerprints come from observed traffic. The mode is carried outside this canonical feature string, in the FAN/1 prefix and metadata.
Normalization rules:
- Values are decimal integers unless the source value is text, such as ALPN.
- Lists preserve wire order because order is often behaviorally meaningful.
- GREASE values are removed from TLS lists before canonicalization.
- Missing fields are represented as empty strings.
X.509 certificate fingerprints
Passive TLS captures can also expose server Certificate handshake messages.
fanfp.py parses DER-encoded X.509 certificates from those messages and emits a
separate x509 protocol fingerprint for each certificate in the observed chain.
The feature string is designed for similarity matching between certificates
created by the same software, CA profile, appliance, or generation workflow, so
it records certificate structure, OIDs, names, extension layout, and validity
shape rather than depending only on a raw certificate hash.
x509|server|idx=<chain_index>|ver=<x509_version>|serial_len=<serial_byte_length>|sig=<outer_signature_algorithm_oid>|tbs_sig=<tbs_signature_algorithm_oid>|issuer=<issuer_name_oid_values>|subject=<subject_name_oid_values>|valid_days=<validity_window_days>|spki_alg=<subject_public_key_algorithm_oid>|spki_param=<algorithm_parameter_oid_or_der>|pk_bits=<subject_public_key_bit_string_size>|san=<subject_alt_names>|ku=<key_usage_bits>|eku=<extended_key_usage_oids>|bc=<basic_constraints>|ski=<subject_key_identifier_shape>|aki=<authority_key_identifier_shape>|pol=<certificate_policy_oids>|aia=<authority_info_access_shape>|crldp=<crl_distribution_point_shape>|nc=<name_constraints_shape>|ext=<critical_flag_and_extension_oid_order>
Normalization rules:
- Object identifiers are emitted in dotted decimal form and extension order is preserved because OID choices and ordering can identify certificate generation stacks.
- Name attributes are emitted as
oid=valuepairs in DER order. - DNS, IP, email, URI, and OID Subject Alternative Names are decoded when possible; complex GeneralName values are represented by short stable hashes.
- Potentially large structured extensions such as AIA, CRL distribution points, name constraints, SKI, and AKI are represented by short SHA-256-derived shape tokens so the fingerprint remains compact.
serial_lenandvalid_dayscapture generation-profile behavior without forcing every regenerated certificate to have a different similarity shape.
See documentation/X509.md for the complete field reference.
TCP/IP stack fingerprints
TCP/IP stack fingerprints are passive, single-packet fingerprints for TCP SYN and SYN-ACK packets. They are intentionally close to the SinFP family of TCP/IP stack signatures while remaining within the FAN/1 feature-string model. The extractor records IP-level signals, TCP header values, ordered TCP options, and packet quirks that are useful for implementation and operating-system correlation.
tcpip2|<role>|ip=<ip_version>|ttl=<observed_ttl_or_hop_limit>|it=<initial_ttl_bucket>|olen=<tcp_options_length>|win=<tcp_window>|mss=<mss>|ws=<window_scale>|sack=<0_or_1>|ts=<zero_or_nz>|opts=<ordered_tcp_options>|df=<0_or_1>|plen=<tcp_payload_length>|ql=<quirks>
Roles:
clientis emitted for SYN packets without ACK.serveris emitted for SYN-ACK packets.
Normalization rules:
- TCP option order is preserved because option layout is behaviorally meaningful.
- Recognized options are normalized as
mss<value>,ws<value>,sackok,sack<length>,ts,tfo,nop, andeol; unknown options are retained asopt<kind>:<hex_payload>. - The
itfield buckets observed TTL/hop-limit to common initial values:32,64,128, or255. - Missing option-derived values are emitted as empty strings, and SACK-permitted
is emitted as
0or1. - Quirk flags are sorted and de-duplicated; currently emitted quirks include malformed/truncated options, non-zero EOL padding, SYN payloads, missing IPv4 DF, non-zero IPv4 ID with DF, and non-zero timestamp echo in SYN/SYN-ACK.
See documentation/TCPIP.md for the complete TCP/IP stack fingerprint field reference.
RDP X.224 fingerprints
RDP fingerprints currently cover the initial TPKT/X.224 connection exchange. If an RDP Negotiation Request or Response is present in the same TPKT packet, its fixed fields are included. Later MCS, CredSSP, TLS, and encrypted RDP layers are not decoded by the RDP parser.
rdp|client|tpkt_v=<tpkt_version>|tpkt_rsv=<tpkt_reserved>|tpkt_len=<tpkt_length>|x224_len=<x224_length_indicator>|pdu=<x224_pdu_type>|dst_ref=<destination_reference>|src_ref=<source_reference>|class=<class_option>|neg_type=<rdp_negotiation_type>|neg_flags=<rdp_negotiation_flags>|neg_len=<rdp_negotiation_length>|neg_proto=<requested_protocols>|neg_selected=<selected_protocol>
rdp|server|tpkt_v=<tpkt_version>|tpkt_rsv=<tpkt_reserved>|tpkt_len=<tpkt_length>|x224_len=<x224_length_indicator>|pdu=<x224_pdu_type>|dst_ref=<destination_reference>|src_ref=<source_reference>|class=<class_option>|neg_type=<rdp_negotiation_type>|neg_flags=<rdp_negotiation_flags>|neg_len=<rdp_negotiation_length>|neg_proto=<requested_protocols>|neg_selected=<selected_protocol>
See documentation/RDP.md for the complete RDP X.224 field reference.
DTLS fingerprints
DTLS fingerprints use the same TLS-family field shape under the dtls
namespace. DTLS runs over UDP and its ClientHello has a cookie field; FAN/1
parses the cookie for protocol handling but does not include it in the
canonical feature string.
dtls|client|v=<dtls_version>|c=<cipher_suites>|e=<extensions>|g=<supported_groups>|p=<ec_point_formats>|sv=<supported_versions>|alpn=<alpn_protocols>|sig=<signature_algorithms>
dtls|server|v=<dtls_version>|c=<selected_cipher>|e=<extensions>|sv=<selected_supported_version>
Passive DTLS fingerprints come from observed UDP datagrams. Active DTLS
fingerprints come from fanything-dtls.nse, which sends a DTLS ClientHello and
retries once when a server returns HelloVerifyRequest.
SSH fingerprints
SSH fingerprints combine the cleartext identification string with the
SSH_MSG_KEXINIT proposal lists.
ssh|peer|id=<software_id>|kex=<kex_algorithms>|hostkey=<server_host_key_algorithms>|enc_c2s=<encryption_algorithms_client_to_server>|enc_s2c=<encryption_algorithms_server_to_client>|mac_c2s=<mac_algorithms_client_to_server>|mac_s2c=<mac_algorithms_server_to_client>|comp_c2s=<compression_algorithms_client_to_server>|comp_s2c=<compression_algorithms_server_to_client>|lang_c2s=<languages_client_to_server>|lang_s2c=<languages_server_to_client>|follows=<first_kex_packet_follows>
Normalization rules:
- The protocol prefix in the identification line is removed, leaving the implementation software string.
- Comma-separated algorithm lists preserve wire order.
- Missing fields are represented as empty strings.
QUIC fingerprints
QUIC fingerprints are built from QUIC Initial packets. When Python
cryptography is available, fanfp.py derives QUIC Initial secrets for QUIC v1
and draft-29, decrypts CRYPTO frames, reassembles the TLS handshake stream, and
emits TLS-derived client or server features under the quic protocol:
quic|client|v=<quic_version>|tls_v=<legacy_tls_version>|c=<cipher_suites>|e=<extensions>|g=<supported_groups>|p=<ec_point_formats>|sv=<supported_versions>|alpn=<alpn_protocols>|sig=<signature_algorithms>
quic|server|v=<quic_version>|tls_v=<legacy_tls_version>|c=<selected_cipher>|e=<extensions>|sv=<selected_supported_version>
If Initial decryption is unavailable, the extractor falls back to QUIC long header metadata:
quic|peer|v=<quic_version>|type=initial|dcid_len=<destination_connection_id_length>|scid_len=<source_connection_id_length>|token_len=<token_length>|len=<packet_length>
Normalization rules:
- QUIC and TLS version values are decimal integers.
- TLS list fields follow the same normalization rules as TCP TLS fingerprints.
- The mode is carried outside the canonical feature string, in the FAN/1 prefix and metadata.
Usage
Passive pcap extraction:
python3 fanfp.py capture.pcap
python3 fanfp.py test/chromium-perdu.com-quick.pcap
Malformed TLS records are skipped by default so one truncated frame does not stop
processing the rest of the capture. Use --strict to raise parsing errors, or
--verbose-errors to print skipped malformed record details to stderr.
The command emits one JSON object per fingerprint:
{"mode":"passive","protocol":"tls","role":"client","fingerprint":"fan1:tls:client:passive:...:sha256:...","fingerprint_simhash128":"fan1:tls:client:passive:...:simhash128:...","features":"tls|client|...","sha256":"...","simhash128":"...","flow":{"src":"192.0.2.10","sport":51514,"dst":"198.51.100.20","dport":443}}
Cluster similar observations by SimHash distance:
python3 fanfp.py capture.pcap > fanfp.jsonl
python3 fanfp_cluster.py fanfp.jsonl
python3 fanfp_cluster.py --threshold 8 --format json fanfp.jsonl
fanfp_cluster.py reads JSON Lines, concatenated JSON objects, a single JSON
object, or a JSON array from a file or stdin. It compares the simhash128
values with Hamming distance, groups records at or below the threshold, and prints the matching
flow pairs so analysts can see which observed flows are close to each other.
The default threshold is 12 bits out of 128. By default, comparisons are kept
within the same protocol, role, and mode; use --cross-roles if you want
to compare every record against every other record.
Active service probing with Nmap NSE:
nmap -Pn -p443 --script ./fanything-tls.nse 192.0.2.20
nmap -sU -p4433 --script ./fanything-dtls.nse 192.0.2.20
nmap -Pn -p3389 --script ./fanything-rdp.nse 192.0.2.20
nmap -Pn -p22 --script ./fanything-ssh.nse 192.0.2.20
The TLS NSE script probes TLS in this order: TLSv1.3, TLSv1.2, TLSv1.1,
TLSv1.0, SSLv3, then SSLv2, and stops at the first full server
fingerprint. A single protocol version can be forced for testing:
nmap -Pn -p443 --script ./fanything-tls.nse --script-args fanything-tls.tls-version=TLSv1.2 192.0.2.20
Cipher tables are version-oriented in the script. See active_scan.md
for cipher-suite rationale and the Firefox/NSS source references used for TLS
1.3, TLS 1.2, DTLS 1.3, DTLS 1.2, and historical SSLv3-era ordering. Modern
TLS/DTLS active cipher lists are pinned to Firefox ESR 140 series
(140.12.0 listed when checked on 2026-06-18) and NSS
SSL_ImplementedCiphers[] order from mozilla-esr140.
The DTLS NSE script probes DTLSv1.3, DTLSv1.2, then DTLSv1.0 over UDP and
stops at the first full server fingerprint. A single DTLS version can be forced
for testing:
nmap -sU -p4433 --script ./fanything-dtls.nse --script-args fanything-dtls.dtls-version=DTLSv1.3 192.0.2.20
The RDP NSE script sends a TPKT/X.224 Connection Request with an RDP Negotiation Request, then fingerprints the server X.224 Connection Confirm and same-packet Negotiation Response when present:
nmap -Pn -p3389 --script ./fanything-rdp.nse 192.0.2.20
The SSH NSE script sends an SSH identification string, reads the server
identification string and SSH_MSG_KEXINIT when available, then emits the same
ssh|peer|... feature shape as fanfp.py. If a server only provides a banner,
the algorithm-list fields are emitted empty, matching passive extraction.
Local NSE testing against OpenSSL servers:
test/nse-openssl.sh
test/nse-ssh.py
test/fanfp-quic.sh
Local QUIC fixture test:
test/fanfp-quic.sh
The test harness creates a temporary certificate, starts openssl s_server for
TLSv1.3, TLSv1.2, TLSv1.1, and TLSv1.0, runs fanything-tls.nse against
each server, verifies default scan stops at the first successful TLS version,
and prints the observed features and fingerprint values. SSLv3 and
SSLv2 are reported as skipped when the local OpenSSL build no longer provides
those server modes. The SSH harness starts a deterministic local SSH test
server and validates fanything-ssh.nse. The QUIC harness validates passive
QUIC Initial extraction against the Chromium pcap fixture.
Wireshark live dissection
The wireshark-plugin/fanfp.lua post-dissector computes FAN/1 fingerprints
in real time as Wireshark dissects frames. It runs on every packet and adds a
FAN/1 Fingerprints tree to the packet detail pane for any frame that
carries a supported handshake — TLS, DTLS, QUIC, SSH, IKEv2, or RDP X.224.

The detail pane shows the protocol, role, canonical features string, and
the full fan1:…:sha256:… fingerprint for each detected handshake in the
selected packet. Multiple fingerprints can appear in a single frame when both
handshake directions are visible at once.
Loading the plugin
Pass the script on the command line to avoid installing it globally:
wireshark -X lua_script:wireshark-plugin/fanfp.lua capture.pcap
tshark -X lua_script:wireshark-plugin/fanfp.lua -r capture.pcap -T fields \
-e fanfp.protocol -e fanfp.role -e fanfp.fingerprint
Or copy wireshark-plugin/fanfp.lua to your Wireshark personal Lua plugins
folder (Help › About Wireshark › Folders › Personal Lua Plugins) and restart
Wireshark so the plugin loads automatically for every capture.
Display filters
Once the plugin is loaded, fanfp.* fields are available as display filters:
| Filter | Effect |
|---|---|
fanfp.fingerprint | Show all packets with a FAN/1 fingerprint |
fanfp.protocol == "dtls" | DTLS handshake frames only |
fanfp.protocol == "ike" | IKEv2 handshake frames only |
fanfp.protocol == "rdp" | RDP X.224 connection frames only |
fanfp.role == "client" | Client-side fingerprints only |
fanfp.role == "server" | Server-side fingerprints only |
MISP correlation hints
Recommended storage approaches:
- Store
fingerprintas the primary correlation value. - Store
sha256as a compact secondary pivot. - Store
simhash128orfingerprint_simhash128as a similarity pivot when you want to calculate Hamming distances between fingerprints and cluster near matches. - Store
featuresin a comment, object attribute, or custom object field for analyst readability. - Store
modeas collection metadata so active probes and passive observations can be filtered independently. - Keep the
flowandframemetadata when available so events can be traced back to packets.
Because FAN/1 embeds the protocol, role, and collection mode in the fingerprint, mixed SSH, TLS, DTLS, QUIC, IKE, RDP, and future service fingerprints can share the same attribute namespace without losing type information.