Documentation
¶
Overview ¶
Package ja4plus computes JA4+ network fingerprints from the packets that gopacket decodes.
Methods ¶
This package implements eleven methods, and ten fingerprinters carry them. JA4LFingerprinter writes both JA4L and JA4LS, so the count of fingerprinters is one below the count of methods. Read the ten as a count of fingerprinters, and never as a count of methods.
The list below names each method and the input it reads.
- JA4 reads a TLS client hello. A TCP connection carries one, and a QUIC initial packet carries one.
- JA4S reads a TLS server hello.
- JA4H reads an HTTP request.
- JA4X reads an X.509 certificate.
- JA4SSH reads a window of SSH packets.
- JA4T reads a TCP SYN packet.
- JA4TS reads a TCP SYN-ACK packet.
- JA4L reads the client timing of a TCP handshake, or of a QUIC exchange.
- JA4LS reads the server timing of the same exchange.
- JA4D reads a DHCP packet.
- JA4D6 reads a DHCPv6 packet.
The list above names what this package implements, and it names no FoxIO list. Three FoxIO records at commit 27f0cbf9fd3000c072f82a0f7d0361dc99acf6c8 name three different sets of methods, so no single FoxIO record states the set above. testdata/foxio.pin holds that commit.
Processor holds the ten fingerprinters, and it gives every packet to each one. The list above holds eleven rows, because JA4LFingerprinter carries two of them. A caller that wants one method alone builds the fingerprinter that carries it. A fingerprinter returns a non-fatal error, and it returns no panic, because every packet is untrusted input.
Build toolchain ¶
The language version and the build toolchain answer two different questions, and this section states both. A language version decides which consumer compiles the module. A build toolchain decides which standard library a built binary links.
The go.mod file declares the Go 1.25 language version. A language version is not a toolchain, so a consumer on a Go 1.25 toolchain compiles this module.
The maintainer moved the version from 1.24 to 1.25 on 2026-08-15, and issue #725 holds the ruling. The gopacket v1.7.1 dependency declares go 1.25.0 in its own go.mod, and it repairs a decoder panic on untrusted input. A Go 1.24 consumer no longer compiles this module, and the ruling lands in v1.1.0.
The minimum build toolchain is go1.25.13. It is the oldest toolchain this project measured at zero called vulnerabilities of the standard library. On 2026-08-14, govulncheck v1.6.0 reported 13 for go1.24.13, 0 for go1.25.13, 4 for go1.26.5 and 0 for go1.26.6. So a later toolchain is not a clean toolchain by itself, and a user on the Go 1.26 line takes go1.26.6 or later.
Every count above moves without a change to this repository, because the vulnerability database is live. The README states the measurement, and it names the command that re-takes it.
Network ¶
This package performs no network input and no network output. It imports no HTTP client, so no call of it reaches the network. LookupFingerprint reads the embedded table or the cache file, and it makes no request.
One function of this library reaches the network, and it lives in another package: LookupFingerprintRemote of github.com/Crank-Git/ja4plus-go/ja4db. A program that imports the core package alone links no HTTP client.
The maintainer ruled that boundary on 2026-08-14, and docs/audit/network-boundary.md holds the record, the three options and the reason.
Concurrency ¶
One Processor serves one goroutine, and one fingerprinter serves one goroutine. Every fingerprinter holds state that no lock guards. Two goroutines that share one instance write a data race. The race detector reports the race. The library does not detect it at run time.
A caller who wants more than one goroutine takes one of two patterns.
- Route each packet with Processor.GetShardKey, and give each goroutine its own Processor. The key holds the sorted five-tuple, so a packet and its reply reach one Processor.
- Share one SyncProcessor, which serializes every call with one mutex.
The first pattern gives higher throughput, because the per-packet path acquires no lock. The second pattern costs one mutex acquisition for each packet. The README shows both patterns as code.
License ¶
This repository holds material under two licenses, and NOTICE at the repository root states which license covers which material.
The BSD 3-Clause license covers the original Go code, and LICENSE at the repository root holds that text. FoxIO licenses the JA4 method under BSD 3-Clause, and it publishes that text as LICENSE-JA4.
FoxIO License 1.1 covers the other methods that this package implements: JA4S, JA4H, JA4T, JA4TS, JA4L, JA4LS, JA4X, JA4SSH, JA4D and JA4D6. FoxIO License 1.1 permits non-commercial use only. A commercial user contacts FoxIO for those methods, and this project gives no legal advice.
This package embeds the file data/ja4plus-mapping.csv, and that file comes from FoxIO. FoxIO License 1.1 covers it, so every program that links this package carries FoxIO material. data/README.md names the source of the file.
FoxIO publishes FoxIO License 1.1 at https://github.com/FoxIO-LLC/ja4/blob/main/LICENSE.
Example ¶
Example reads one TCP SYN packet through a Processor and prints each fingerprint.
`go doc` prints the package comment, and pkg.go.dev renders this example under it. So this function is the runnable example of the package documentation, which FR-release-10 requires.
The packet carries a window of 65535 and no TCP option. So the option list, the maximum segment size and the window scale of the JA4T value each read `00`.
processor := ja4plus.NewProcessor()
results, err := processor.ProcessPacket(concurrencySYNPacket(40000))
if err != nil {
fmt.Println("error:", err)
return
}
for _, result := range results {
fmt.Printf("%s %s\n", result.Type, result.Fingerprint)
}
Output: ja4t 65535_00_00_00
Index ¶
- Variables
- func CachedDatabasePath() (string, error)
- func CalculateDistance(latencyUS int, propagationFactor float64) float64
- func CalculateDistanceKm(latencyUS int, propagationFactor float64) float64
- func ComputeJA4(packet gopacket.Packet) string
- func ComputeJA4D(packet gopacket.Packet) string
- func ComputeJA4D6(packet gopacket.Packet) string
- func ComputeJA4H(packet gopacket.Packet) string
- func ComputeJA4S(packet gopacket.Packet) string
- func ComputeJA4T(packet gopacket.Packet) string
- func ComputeJA4TS(packet gopacket.Packet) string
- func ComputeJA4XFromDER(certDER []byte) string
- func ComputeJA4XFromPEM(pemData []byte) string
- func ComputeJA4XFromPacket(packet gopacket.Packet) string
- func DecryptQUICPacket(payload, secret []byte, connectionIDLength int) ([]byte, error)
- func EstimateHopCount(ttl uint8) int
- func EstimateOS(ttl uint8) string
- func LookupHASSH(hassh string) string
- type ConnectionWindowCloser
- type DatabaseInfo
- type FingerprintResult
- type Fingerprinter
- type HASSHResult
- type JA4D6Fingerprinter
- type JA4DFingerprinter
- type JA4Fingerprinter
- type JA4HFingerprinter
- type JA4LFingerprinter
- type JA4SFingerprinter
- type JA4SSHFingerprinter
- func (f *JA4SSHFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
- func (f *JA4SSHFingerprinter) CloseConnectionWindow(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string) []FingerprintResult
- func (f *JA4SSHFingerprinter) CloseOpenWindows() []FingerprintResult
- func (f *JA4SSHFingerprinter) GetHASSHFingerprints() []HASSHResult
- func (f *JA4SSHFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
- func (f *JA4SSHFingerprinter) Reset()
- type JA4TFingerprinter
- type JA4TSFingerprinter
- type JA4XFingerprinter
- type KeyLog
- type LookupResult
- type Processor
- func (p *Processor) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
- func (p *Processor) CloseConnectionWindow(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string) []FingerprintResult
- func (p *Processor) CloseOpenWindows() []FingerprintResult
- func (p *Processor) GetShardKey(packet gopacket.Packet) string
- func (p *Processor) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, []error)
- func (p *Processor) Reset()
- type ProcessorOption
- type SSHSessionInfo
- type SyncProcessor
- func (p *SyncProcessor) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
- func (p *SyncProcessor) CloseConnectionWindow(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string) []FingerprintResult
- func (p *SyncProcessor) CloseOpenWindows() []FingerprintResult
- func (p *SyncProcessor) GetShardKey(packet gopacket.Packet) string
- func (p *SyncProcessor) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, []error)
- func (p *SyncProcessor) Reset()
- type WindowCloser
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNoSecret = errors.New("ja4plus: no TLS secret for the connection")
ErrNoSecret reports that no secret is available for the connection. A caller who supplies no key log reads this error, and a caller whose key log holds no secret for the connection reads it too.
Functions ¶
func CachedDatabasePath ¶
CachedDatabasePath returns the path where a cached ja4plus-mapping.csv downloaded by `ja4plus db update` is stored. The directory is created if it does not already exist.
func CalculateDistance ¶
CalculateDistance estimates physical distance in miles from one-way latency. Uses speed of light in fiber optic cable (0.128 miles/us). propagationFactor accounts for non-direct routing (default 1.6).
func CalculateDistanceKm ¶
CalculateDistanceKm estimates physical distance in kilometers from one-way latency. Uses speed of light in fiber optic cable (0.206 km/us).
func ComputeJA4 ¶
ComputeJA4 is a one-shot function that extracts a JA4 fingerprint from a packet. Returns an empty string if the packet is not a TLS ClientHello.
func ComputeJA4D ¶
ComputeJA4D is a one-shot function that computes the JA4D fingerprint for a single packet.
func ComputeJA4D6 ¶
ComputeJA4D6 is a one-shot function that computes the JA4D6 fingerprint for a single packet.
func ComputeJA4H ¶
ComputeJA4H extracts the TCP payload from a packet, parses it as an HTTP request, and returns the JA4H fingerprint string. Returns "" if the packet does not contain an HTTP request. It returns "" for a request whose body the packet does not complete, because the ruling of #455 states that such a request reaches no value. One rule governs every path that produces a JA4H value.
func ComputeJA4S ¶
ComputeJA4S is a one-shot function that extracts a JA4S fingerprint from a packet. Returns an empty string if the packet is not a TLS ServerHello.
func ComputeJA4T ¶
ComputeJA4T is a one-shot function that computes the JA4T fingerprint for a single packet.
func ComputeJA4TS ¶
ComputeJA4TS is a one-shot function that computes the JA4TS fingerprint for a single packet.
It reads one packet through a new fingerprinter, so that packet is always the first SYN-ACK of its connection. The value therefore carries no part e. A caller that needs part e keeps one JA4TSFingerprinter across the packets of the connection.
func ComputeJA4XFromDER ¶
ComputeJA4XFromDER computes a JA4X fingerprint from DER-encoded certificate bytes. Returns an empty string if the certificate cannot be parsed.
Format: {issuer_hash}_{subject_hash}_{extension_hash}
func ComputeJA4XFromPEM ¶
ComputeJA4XFromPEM computes a JA4X fingerprint from PEM-encoded certificate bytes. Returns an empty string if the certificate cannot be parsed.
func ComputeJA4XFromPacket ¶
ComputeJA4XFromPacket is a one-shot function that extracts JA4X fingerprints from a single packet. It creates a temporary fingerprinter, so it does not support stream reassembly. For multi-packet streams, use JA4XFingerprinter.
func DecryptQUICPacket ¶
DecryptQUICPacket returns the frame bytes of one QUIC packet, which the secret protects. The secret is one TLS traffic secret, and KeyLog.Secret returns one. connectionIDLength states the Destination Connection ID length of a short header packet. A long header packet carries its own lengths, so it ignores that argument. It returns ErrNoSecret when the caller supplies no secret, and it produces no fingerprint in that case. It returns a non-fatal error for a packet it cannot read, and it never panics. RFC 9001 Section 5.1 states the key derivation, and Section 5.4.1 states the header protection.
func EstimateHopCount ¶
EstimateHopCount estimates the number of network hops based on observed TTL.
func EstimateOS ¶
EstimateOS estimates the operating system based on observed TTL value.
func LookupHASSH ¶
LookupHASSH returns a human-readable name for a known HASSH fingerprint, or "" if the fingerprint is not in the built-in lookup table.
Types ¶
type ConnectionWindowCloser ¶
type ConnectionWindowCloser interface {
// CloseConnectionWindow returns the value of the window that one connection holds
// open, and it then removes the connection. It names the connection by the same key
// CleanupConnection accepts. The maintainer ruled the method on 2026-08-12, and issue
// #216 records the ruling.
CloseConnectionWindow(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string) []FingerprintResult
}
ConnectionWindowCloser is the interface that a fingerprinter implements when one named connection holds a window open. JA4SSH holds such a window, and no other method of this library does.
The interface sits beside Fingerprinter and beside WindowCloser. Each one declares one capability, as `http.Flusher` and `http.Hijacker` each do, so a type that implements one capability keeps the dispatch of that capability. The maintainer ruled the split on 2026-08-12, and issue #268 records the ruling.
type DatabaseInfo ¶
type DatabaseInfo struct {
// Source is "embedded" or "cache".
Source string
// Path is the cache path when Source == "cache", empty otherwise.
Path string
// Entries is the number of fingerprints loaded.
Entries int
// ModTime is the modification time of the cache file (zero for embedded).
ModTime time.Time
}
DatabaseInfo describes the active lookup database.
func GetDatabaseInfo ¶
func GetDatabaseInfo() DatabaseInfo
GetDatabaseInfo returns metadata about the currently-active database. It reads one snapshot of the table, so the source, the path and the record count describe one state.
type FingerprintResult ¶
type FingerprintResult struct {
// Fingerprint holds the value of the method.
Fingerprint string
// Raw holds the unhashed form of the value.
Raw string
// OriginalOrder holds `JA4_o`, which hashes each list of the wire-order raw form.
// `RawOriginalOrder` holds the same two lists unhashed, so the two fields read one
// input. `testdata/foxio/reference/python/ja4.py:291` states the rule, and issue #277
// records the field.
OriginalOrder string
// RawOriginalOrder holds the wire-order form unhashed, so it reads the same input as
// OriginalOrder. The form keeps the wire order, it sorts no list, and it preserves the
// SNI value and the ALPN value.
RawOriginalOrder string
// Type names the method that produced the value.
Type string
// SrcIP is the source address of the packet.
SrcIP string
// DstIP is the destination address of the packet.
DstIP string
// SrcPort is the source port of the packet.
SrcPort uint16
// DstPort is the destination port of the packet.
DstPort uint16
// Timestamp is the capture time of the packet.
Timestamp time.Time
}
FingerprintResult holds a single fingerprint and its metadata.
Four fields carry one method value each, and the FoxIO key suffix names each one. `Fingerprint` carries the bare key, `Raw` carries `_r`, `OriginalOrder` carries `_o` and `RawOriginalOrder` carries `_ro`. A fingerprinter that produces no value for one of the four leaves that field empty. The vector set that the fingerprinter reads states the reason. The FoxIO per-stream vector set publishes `JA4H_ro` and no `JA4H_r` value, and the FoxIO per-packet vector set publishes `ja4.ja4h_r` on 126 records. `JA4H` therefore fills both fields, because the per-packet set states the raw sorted value that the per-stream set omits. Issue #290 recorded that the two sets differ, and issue #310 filled `Raw`.
type Fingerprinter ¶
type Fingerprinter interface {
// ProcessPacket reads one packet and returns each fingerprint of it, with any
// non-fatal error. An implementation returns an error and never a panic, because
// every packet is untrusted input.
ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
// Reset clears every state table of the fingerprinter. A caller reuses the
// fingerprinter on a second packet source after this call.
Reset()
// CleanupConnection removes internal state associated with a connection
// identified by the given 5-tuple. Each fingerprinter normalizes the tuple
// to its own internal key format. This prevents state leaks in long-running
// monitors. For QUIC-keyed fingerprinters (JA4, JA4S), this also cleans up
// any DCID-to-tuple mappings.
CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
}
Fingerprinter is the interface that all JA4+ fingerprinters implement.
type HASSHResult ¶
type HASSHResult struct {
// Fingerprint holds the HASSH value.
Fingerprint string
// Banner holds the SSH identification string of the side.
Banner string
// Type is `client` or `server`.
Type string
// ConnKey names the connection that produced the value.
ConnKey string
}
HASSHResult holds a HASSH fingerprint and associated metadata.
type JA4D6Fingerprinter ¶
type JA4D6Fingerprinter struct {
}
JA4D6Fingerprinter generates JA4D6 DHCPv6 fingerprints.
Format: {type:5}{size:4}{ip:1}{fqdn:1}_{options}_{request_list}
- type: 5-char DHCPv6 message type abbreviation
- size: 4-digit length of the DUID inside option 1 (Client Identifier), capped 9999, default "0000" if no option 1.
- ip: 'i' if IATA option (option 4) present, else 'n'
- fqdn: 'd' if Client FQDN (option 39) present, else 'n'
- options: dash-joined option type codes in PRESENCE ORDER (no exclusions), including nested sub-options. Default "00".
- request_list: dash-joined items of the Option Request option (option 6, ORO). Default "00".
One JA4D6Fingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func NewJA4D6 ¶
func NewJA4D6() *JA4D6Fingerprinter
NewJA4D6 creates a new JA4D6 DHCPv6 fingerprinter.
func (*JA4D6Fingerprinter) CleanupConnection ¶
func (f *JA4D6Fingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection is a no-op for JA4D6 (stateless per-packet fingerprinter).
func (*JA4D6Fingerprinter) ProcessPacket ¶
func (f *JA4D6Fingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4D6 fingerprint results for DHCPv6 messages.
func (*JA4D6Fingerprinter) Reset ¶
func (f *JA4D6Fingerprinter) Reset()
Reset clears the state of the fingerprinter. JA4D6 holds no state, so this method changes nothing. It keeps the Fingerprinter interface whole. Issue #25 removed the results slice, which grew without a bound.
type JA4DFingerprinter ¶
type JA4DFingerprinter struct {
}
JA4DFingerprinter generates JA4D DHCP fingerprints (FoxIO PR #267/#270).
Format: {type:5}{size:4}{ip:1}{fqdn:1}_{options}_{request_list}
- type: 5-char DHCP message type abbreviation (from option 53)
- size: 4-digit Maximum DHCP Message Size (option 57), capped 9999, default "0000"
- ip: 'i' if Requested IP Address (option 50) is present, else 'n'
- fqdn: 'd' if Client FQDN (option 81) carries a domain name, else 'n'
- options: dash-joined option type codes in PRESENCE ORDER, excluding Pad (0), MessageType (53), Requested IP (50), FQDN (81). Default "00".
- request_list: dash-joined items of the Parameter Request List (option 55) in original order. Default "00".
One JA4DFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4DFingerprinter) CleanupConnection ¶
func (f *JA4DFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection is a no-op for JA4D (stateless per-packet fingerprinter).
func (*JA4DFingerprinter) ProcessPacket ¶
func (f *JA4DFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4D fingerprint results for DHCP messages.
func (*JA4DFingerprinter) Reset ¶
func (f *JA4DFingerprinter) Reset()
Reset clears the state of the fingerprinter. JA4D holds no state, so this method changes nothing. It keeps the Fingerprinter interface whole. Issue #25 removed the results slice, which grew without a bound.
type JA4Fingerprinter ¶
type JA4Fingerprinter struct {
// contains filtered or unexported fields
}
JA4Fingerprinter computes JA4 TLS Client Hello fingerprints.
One JA4Fingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4Fingerprinter) CleanupConnection ¶
func (f *JA4Fingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes internal state for the given connection. JA4 QUIC state is keyed by DCID hex. This method looks up the DCID via the dcidToTuple reverse map and cleans the corresponding fragments. The caller names the two endpoints in either order, because the reverse map holds the order of the datagram that carried the client hello. The caller names the address pair that a FingerprintResult carries, which is the reported key. JA4L holds the same contract. A tunneled connection groups under the inner address pair, so this method reads the reported key of each connection as well as the grouping key. It falls back to the grouping key, because a caller of GetShardKey holds that key instead. FR-gaps-14e states the fallback, and `ja4plus/fingerprinters/ja4l.py:216` holds it too. Issue #193 records the leak that the absent index caused.
func (*JA4Fingerprinter) ProcessPacket ¶
func (f *JA4Fingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4 fingerprint results.
func (*JA4Fingerprinter) Reset ¶
func (f *JA4Fingerprinter) Reset()
Reset clears the QUIC fragment table and the connection identifier table. The fingerprinter keeps no result, because ProcessPacket returns each result to the caller. Issue #25 removed the results slice, which grew without a bound.
type JA4HFingerprinter ¶
type JA4HFingerprinter struct {
// contains filtered or unexported fields
}
JA4HFingerprinter generates JA4H fingerprints from HTTP request packets. It uses TCP stream reassembly to handle multi-segment HTTP requests.
One JA4HFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4HFingerprinter) CleanupConnection ¶
func (f *JA4HFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes internal state for the given connection. JA4H uses directional arrow keys: srcIP:srcPort->dstIP:dstPort.
func (*JA4HFingerprinter) ProcessPacket ¶
func (f *JA4HFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4H fingerprints if the packet contains an HTTP request (possibly reassembled from multiple segments). It produces the value at the packet that completes the request, and never at the packet that ends the header block. The maintainer ruled that emission frame at #455, and `parser.HTTPMessageIsComplete` holds the rule.
func (*JA4HFingerprinter) Reset ¶
func (f *JA4HFingerprinter) Reset()
Reset clears the TCP stream reassembler. The fingerprinter keeps no result, because ProcessPacket returns each result to the caller. Issue #25 removed the results slice, which grew without a bound.
type JA4LFingerprinter ¶
type JA4LFingerprinter struct {
// contains filtered or unexported fields
}
JA4LFingerprinter generates JA4L latency fingerprints from TCP handshake timing or QUIC/UDP exchange timing.
One JA4LFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4LFingerprinter) CleanupConnection ¶
func (f *JA4LFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes internal state for the given connection. The caller names the address pair that a FingerprintResult carries, which is the reported key. A tunneled connection groups under the inner address pair, so this method reads the grouping key from the reported key first. JA4L normalizes keys lexicographically by IP then port.
func (*JA4LFingerprinter) ProcessPacket ¶
func (f *JA4LFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4L fingerprints if a handshake timing measurement can be computed. Supports both TCP and UDP/QUIC.
func (*JA4LFingerprinter) Reset ¶
func (f *JA4LFingerprinter) Reset()
Reset clears the connection table. The fingerprinter keeps no result, because ProcessPacket returns each result to the caller. Issue #25 removed the results slice, which grew without a bound.
type JA4SFingerprinter ¶
type JA4SFingerprinter struct {
// contains filtered or unexported fields
}
JA4SFingerprinter computes JA4S TLS Server Hello fingerprints.
One JA4SFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4SFingerprinter) CleanupConnection ¶
func (f *JA4SFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes internal state for the given connection. JA4S QUIC state is keyed by directional tuple: srcIP:srcPort-dstIP:dstPort. The caller names the address pair that a FingerprintResult carries, which is the reported key. JA4L holds the same contract. The caller names the two endpoints in either order, so this method reads both orders. A tunneled connection groups under the inner address pair, so this method reads the grouping key from the index first. It falls back to the key the caller gave, because a caller of GetShardKey holds the grouping key instead. FR-gaps-14e states the fallback, and `ja4plus/fingerprinters/ja4l.py:216` holds it too. Issue #193 records the leak that the absent index caused.
func (*JA4SFingerprinter) ProcessPacket ¶
func (f *JA4SFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4S fingerprint results.
func (*JA4SFingerprinter) Reset ¶
func (f *JA4SFingerprinter) Reset()
Reset clears the QUIC connection identifier table and the two index tables. The fingerprinter keeps no result, because ProcessPacket returns each result to the caller. Issue #25 removed the results slice, which grew without a bound.
type JA4SSHFingerprinter ¶
type JA4SSHFingerprinter struct {
// contains filtered or unexported fields
}
JA4SSHFingerprinter generates JA4SSH fingerprints from SSH traffic patterns. It tracks per-connection packet sizes and ACK counts in a rolling window.
Format: c{client_mode}s{server_mode}_c{client_pkts}s{server_pkts}_c{client_acks}s{server_acks}
One JA4SSHFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func NewJA4SSH ¶
func NewJA4SSH(packetCount int) *JA4SSHFingerprinter
NewJA4SSH creates a new JA4SSH fingerprinter. If packetCount is 0, the default window of 200 packets is used.
func (*JA4SSHFingerprinter) CleanupConnection ¶
func (f *JA4SSHFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes internal state for the given connection. JA4SSH normalizes keys by the three steps of decideEndpoints. It removes the handshake entry of the pair too. That entry would otherwise outlive the connection, because the caller states that the connection ended. The port removes the same entry at `ja4plus/fingerprinters/ja4ssh.py:541-544`. It emits no fingerprint. A caller that wants the open window of the connection calls CloseConnectionWindow instead, and issue #216 records that ruling.
func (*JA4SSHFingerprinter) CloseConnectionWindow ¶
func (f *JA4SSHFingerprinter) CloseConnectionWindow(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string) []FingerprintResult
CloseConnectionWindow returns the value of the window that one connection holds open, and it then removes the connection.
The caller calls the method when it evicts one connection, which is the moment the reference publishes the final window. `rust/ja4/src/ssh.rs:45-55` and `zeek/ja4ssh/main.zeek:160-164` both emit at teardown, and CloseOpenWindows reaches every connection at once, which is the wrong instrument for one connection that just ended. The maintainer ruled the method on 2026-08-12, and issue #216 records the ruling.
It names the connection by the same key CleanupConnection accepts, so the caller names the two endpoints in either order. It returns an empty slice for a connection the state table does not hold, and an empty slice for a window that holds no SSH packet. It removes the connection in both cases, so a second call returns an empty slice. The result names the client of the connection as the source and the server as the destination. CloseOpenWindows names the two endpoints the same way. No packet triggers this emission, so the result reads the endpoints of the connection. The method is opt-in. CleanupConnection still emits nothing, so a caller that only reclaims memory receives no fingerprint it did not ask for.
func (*JA4SSHFingerprinter) CloseOpenWindows ¶
func (f *JA4SSHFingerprinter) CloseOpenWindows() []FingerprintResult
CloseOpenWindows returns the value of the window that each connection holds open, and it starts a new window on each one.
The caller calls the method when the packet source ends. A connection whose last window never reaches the threshold holds that window open, and this method is the one rule that emits it. ProcessPacket emits the open window on a packet that carries the FIN flag and the ACK flag. A connection that sends such a packet therefore holds no window open. `rust/ja4/src/ssh.rs:45-55` and `zeek/ja4ssh/main.zeek:160-164` both emit that window, and the port's issues #105, #199 and #214 hold the ruling.
It returns the values in the order the packet source opened the connections. It returns an empty slice for a window that holds no SSH packet, so a second call returns an empty slice. Each result names the client of the connection as the source and the server as the destination. No packet triggers this emission, so the result reads the endpoints of the connection. A result that ProcessPacket returns names the sender of the packet that filled the window, which is the direction that every other method of this library reports. The method is opt-in. A caller who never calls it loses the open window, and the library forces no flush.
func (*JA4SSHFingerprinter) GetHASSHFingerprints ¶
func (f *JA4SSHFingerprinter) GetHASSHFingerprints() []HASSHResult
GetHASSHFingerprints returns all collected HASSH fingerprints across tracked connections.
func (*JA4SSHFingerprinter) ProcessPacket ¶
func (f *JA4SSHFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4SSH fingerprints when a window fills. It returns the open window of the connection on a packet that carries the FIN flag and the ACK flag. Such a packet closes the connection. Issue #222 holds the readings.
func (*JA4SSHFingerprinter) Reset ¶
func (f *JA4SSHFingerprinter) Reset()
Reset clears the connection table. The fingerprinter keeps no result, because ProcessPacket returns each result to the caller. Issue #25 removed the results slice, which grew without a bound. It keeps the arrival counter. The counter orders the connections that CloseOpenWindows publishes, and a counter that returns to zero would order a new connection against a stale number. It empties the order list too, because the state table and that list name one set of connections. It keeps the packet counter, which schedules the age pass alone. It empties the handshake table and the handshake order list, which step 2 of the client direction reads. `.claude/rules/concurrency.md` states that a new state map reaches CleanupConnection and Reset.
type JA4TFingerprinter ¶
type JA4TFingerprinter struct {
}
JA4TFingerprinter fingerprints TCP SYN packets (client-side).
One JA4TFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4TFingerprinter) CleanupConnection ¶
func (f *JA4TFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection is a no-op for JA4T (stateless per-packet fingerprinter).
func (*JA4TFingerprinter) ProcessPacket ¶
func (f *JA4TFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4T fingerprint results for SYN packets.
func (*JA4TFingerprinter) Reset ¶
func (f *JA4TFingerprinter) Reset()
Reset clears the state of the fingerprinter. JA4T holds no state, so this method changes nothing. It keeps the Fingerprinter interface whole. Issue #25 removed the results slice, which grew without a bound.
type JA4TSFingerprinter ¶
type JA4TSFingerprinter struct {
// contains filtered or unexported fields
}
JA4TSFingerprinter fingerprints TCP SYN-ACK packets (server-side).
A connection the server answered twice or more carries part e, which holds the delay of each SYN-ACK after the first. A RST that the server sends on such a connection appends `-R` and its own delay.
One JA4TSFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4TSFingerprinter) CleanupConnection ¶
func (f *JA4TSFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes the stored SYN-ACK times and stored parts of the connection. The key names the server first, because every SYN-ACK travels from the server. The caller names either direction, so this method drops both orderings.
func (*JA4TSFingerprinter) ProcessPacket ¶
func (f *JA4TSFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4TS fingerprint results for SYN-ACK packets. It returns one result for a RST packet of a connection that already sent a SYN-ACK. It returns no result for any other packet.
func (*JA4TSFingerprinter) Reset ¶
func (f *JA4TSFingerprinter) Reset()
Reset clears the state of the fingerprinter. It drops every connection, so a second capture reads none of the SYN-ACK times of the first one.
type JA4XFingerprinter ¶
type JA4XFingerprinter struct {
// contains filtered or unexported fields
}
JA4XFingerprinter computes JA4X X.509 certificate fingerprints. It is stateful: it tracks TCP streams to reassemble TLS Certificate messages that may span multiple TCP segments.
One JA4XFingerprinter serves one goroutine. It holds state that no lock guards. Give each goroutine its own instance, or share one SyncProcessor.
func (*JA4XFingerprinter) CleanupConnection ¶
func (f *JA4XFingerprinter) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes internal state for the given connection. JA4X uses directional keys: srcIP:srcPort-dstIP:dstPort. Both directions are cleaned since the certificate may arrive from either side.
func (*JA4XFingerprinter) ProcessPacket ¶
func (f *JA4XFingerprinter) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, error)
ProcessPacket processes a packet and returns JA4X fingerprint results.
func (*JA4XFingerprinter) Reset ¶
func (f *JA4XFingerprinter) Reset()
Reset clears the stream table and the certificate set. The fingerprinter keeps no result, because ProcessPacket returns each result to the caller. Issue #25 removed the results slice, which grew without a bound.
type KeyLog ¶
type KeyLog struct {
// contains filtered or unexported fields
}
KeyLog holds the TLS secrets of one or more connections, and the client random of the connection identifies each one.
A caller builds a KeyLog with ParseKeyLog or with ReadKeyLogFromCapture, and the value does not change after that. Any number of goroutines read one KeyLog.
The library reads a secret only when the caller supplies one. It reads no key material outside the capture file and outside the reader the caller passes.
func ParseKeyLog ¶
ParseKeyLog returns the KeyLog of a key log in the NSS key log format, which `draft-ietf-tls-keylogfile` specifies. It ignores a line it cannot read. It reads at most 16 megabytes.
func ReadKeyLogFromCapture ¶
ReadKeyLogFromCapture returns the KeyLog that the Decryption Secrets Blocks of a pcapng capture carry. It returns an empty KeyLog for a capture that holds no such block. It returns an error for a reader that holds no pcapng capture. FR-gaps-15 states this requirement, and `gopacket` v1.1.19 discards the block.
func (*KeyLog) ClientRandoms ¶
ClientRandoms returns the client random of every connection the key log holds, sorted.
func (*KeyLog) Secret ¶
Secret returns the secret of one label for the connection that the client random identifies. It returns ErrNoSecret when the key log holds no such secret. A Decryption Secrets Block that holds a secret for a connection the capture does not carry reaches no caller, because no packet of the capture states that client random.
type LookupResult ¶
type LookupResult struct {
// Application names the program that produces the fingerprint.
Application string
// Type names the class of the program.
Type string
// Notes holds the free text of the record.
Notes string
}
LookupResult holds the result of a fingerprint database lookup.
func LookupFingerprint ¶
func LookupFingerprint(fingerprint string) *LookupResult
LookupFingerprint looks up a JA4+ fingerprint in the local FoxIO database. If a cached database file is present at the user-cache path (see CachedDatabasePath), it is used; otherwise the embedded database is used. Returns nil if the fingerprint is not found. This function never makes network calls.
The library reads the file time and the size of the cache file at each call, and it rebuilds the table when one of the two changes. A program that updates the database therefore reads the new table at its next lookup, and it needs no restart. A rebuild that cannot parse the cache file leaves the previous table in place. A cache file that the program deleted makes the library read the embedded copy.
This function is safe for concurrent use.
The remote lookup lives in the package github.com/Crank-Git/ja4plus-go/ja4db, which this package does not import.
Example ¶
ExampleLookupFingerprint reads the local database for one fingerprint.
The example prints the result of a value that no database holds, and it does so for a reason. Two databases answer this call: the embedded FoxIO mapping, and the cache file that `ja4plus db update` writes. A machine that holds a cache file answers a hit differently from a machine that holds none. An example compares its printed text exactly. A miss returns nil under every database, so this example states the one result that each machine reproduces.
The lookup reaches no network. github.com/Crank-Git/ja4plus-go/ja4db holds the remote lookup.
package main
import (
"fmt"
ja4plus "github.com/Crank-Git/ja4plus-go"
)
func main() {
// Each part of this value is a placeholder, so no database record carries it.
result := ja4plus.LookupFingerprint("t00i000000_000000000000_000000000000")
fmt.Println(result == nil)
}
Output: true
type Processor ¶
type Processor struct {
// contains filtered or unexported fields
}
Processor runs all JA4+ fingerprinters on each packet and aggregates results. Errors from individual fingerprinters are non-fatal; they are collected and returned alongside any successful results.
One Processor serves one goroutine. Every fingerprinter holds state that no lock guards. Two goroutines that share one Processor write a data race. The race detector reports the race. The library does not detect it at run time.
A caller who wants more than one goroutine takes one of two patterns.
- Route each packet with GetShardKey, and give each goroutine its own Processor. GetShardKey returns one key for both directions of one connection.
- Share one SyncProcessor, which serializes every call with one mutex.
The first pattern gives higher throughput, because the per-packet path acquires no lock. The second pattern costs one mutex acquisition for each packet.
Example ¶
ExampleProcessor reads two connections through one Processor, and it then clears the state of the Processor.
One Processor serves one goroutine, and this example uses one goroutine. A caller that wants more than one goroutine reads the `# Concurrency` section of the package documentation.
A long-running monitor calls CleanupConnection when a connection ends, and it calls Reset when it starts a second packet source. State that neither call removes stays in the Processor for the life of the program.
processor := ja4plus.NewProcessor()
for _, srcPort := range []uint16{40000, 40001} {
results, err := processor.ProcessPacket(concurrencySYNPacket(srcPort))
if err != nil {
fmt.Println("error:", err)
return
}
for _, result := range results {
fmt.Printf("%s from %s:%d to %s:%d\n",
result.Type, result.SrcIP, result.SrcPort, result.DstIP, result.DstPort)
}
}
processor.Reset()
Output: ja4t from 192.168.1.1:40000 to 10.0.0.1:443 ja4t from 192.168.1.1:40001 to 10.0.0.1:443
func NewProcessor ¶
func NewProcessor(options ...ProcessorOption) *Processor
NewProcessor creates a Processor with all fingerprinters initialized. It applies each option in the order the caller states, so the last option that writes one field decides that field. It ignores a nil option, because a caller that builds an option slice leaves a slot empty.
func (*Processor) CleanupConnection ¶
func (p *Processor) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes internal state for the given connection across all fingerprinters. Each fingerprinter normalizes the 5-tuple to its own internal key format. Call this when a connection is evicted from the monitor's tracker to prevent state leaks in long-running processes. The caller names the address pair that a FingerprintResult carries, which is the reported key. One call reaches every fingerprinter, so one contract governs them all.
func (*Processor) CloseConnectionWindow ¶
func (p *Processor) CloseConnectionWindow(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string) []FingerprintResult
CloseConnectionWindow returns the value of the window that one connection holds open, and it then removes the connection from every fingerprinter that holds such a window.
The caller calls the method when it evicts one connection. It reaches each fingerprinter that implements ConnectionWindowCloser and joins the results, in the order the processor runs the fingerprinters. A fingerprinter that implements no ConnectionWindowCloser holds no window open, so the call skips it. The caller names the address pair that a FingerprintResult carries, which is the reported key. CleanupConnection accepts the same key. It removes the connection, so a second call returns an empty slice. CleanupConnection still emits nothing, so a caller that only reclaims memory receives no fingerprint it did not ask for. The maintainer ruled the method on 2026-08-12, and issue #216 records the ruling.
func (*Processor) CloseOpenWindows ¶
func (p *Processor) CloseOpenWindows() []FingerprintResult
CloseOpenWindows returns the value of the window that each fingerprinter holds open.
The caller calls the method when the packet source ends. It reaches each fingerprinter that implements WindowCloser and joins the results, in the order the processor runs the fingerprinters. A fingerprinter that implements no WindowCloser holds no window open, so the call skips it. A second call returns an empty slice, because the first call started a new window. FR-parity-31 states this requirement.
func (*Processor) GetShardKey ¶
GetShardKey returns a stable key that routes one packet to one Processor shard. The key is the sorted five-tuple, so a packet and its reply return one key. The key reads no QUIC connection identifier, so every packet of one QUIC connection returns one key after the identifier changes. It returns an empty string for a packet that carries neither TCP nor UDP. The caller decides what to do with an empty key. GetShardKey acquires no lock and holds no state.
**`monitorConnectionKey` in `cmd/ja4plus/watch.go` holds a second copy of this grouping rule.** #80 wrote it for the connection table of `ja4plus watch`, and it returns a `connectionKey` where this method returns a formatted string. **A change to the rule below changes that function too.** This method is exported and it decides the rule; that function follows it. The Epic 13 cross-member review found the pair, and issue #614 holds the reading.
Example ¶
package main
import (
"fmt"
"hash/fnv"
"net"
"sync"
"time"
ja4plus "github.com/Crank-Git/ja4plus-go"
"github.com/gopacket/gopacket"
"github.com/gopacket/gopacket/layers"
)
// processSharded runs one Processor for each shard and returns every result.
// Each goroutine owns its Processor, so no lock guards the per-packet path.
func processSharded(packets []gopacket.Packet, shards int) []ja4plus.FingerprintResult {
inputs := make([]chan gopacket.Packet, shards)
outputs := make(chan []ja4plus.FingerprintResult, shards)
var wg sync.WaitGroup
for i := range inputs {
inputs[i] = make(chan gopacket.Packet, 16)
wg.Add(1)
go func(in <-chan gopacket.Packet) {
defer wg.Done()
proc := ja4plus.NewProcessor()
var results []ja4plus.FingerprintResult
for packet := range in {
got, _ := proc.ProcessPacket(packet)
results = append(results, got...)
}
outputs <- results
}(inputs[i])
}
router := ja4plus.NewProcessor()
for _, packet := range packets {
key := router.GetShardKey(packet)
if key == "" {
continue
}
inputs[shardIndex(key, shards)] <- packet
}
for _, in := range inputs {
close(in)
}
wg.Wait()
close(outputs)
var all []ja4plus.FingerprintResult
for results := range outputs {
all = append(all, results...)
}
return all
}
// shardIndex returns the shard that owns the key. The key holds the sorted five-tuple,
// so a packet and its reply reach one shard and one Processor.
func shardIndex(key string, shards int) int {
digest := fnv.New32a()
_, _ = digest.Write([]byte(key))
return int(digest.Sum32() % uint32(shards))
}
func main() {
packets := concurrencySYNPackets(8)
results := processSharded(packets, 4)
fmt.Println(countJA4T(results))
}
// countJA4T returns the number of JA4T results. One TCP SYN packet produces one of them,
// so the count is stable whatever order the goroutines run in.
func countJA4T(results []ja4plus.FingerprintResult) int {
count := 0
for _, result := range results {
if result.Type == "ja4t" {
count++
}
}
return count
}
// concurrencySYNPackets returns the requested number of TCP SYN packets. Each packet
// carries its own source port, so the packets reach more than one shard.
func concurrencySYNPackets(count int) []gopacket.Packet {
packets := make([]gopacket.Packet, 0, count)
for i := 0; i < count; i++ {
packets = append(packets, concurrencySYNPacket(uint16(40000+i)))
}
return packets
}
// concurrencySYNPacket returns one TCP SYN packet from the source port.
func concurrencySYNPacket(srcPort uint16) gopacket.Packet {
ip := &layers.IPv4{
SrcIP: net.ParseIP("192.168.1.1"),
DstIP: net.ParseIP("10.0.0.1"),
Protocol: layers.IPProtocolTCP,
Version: 4,
TTL: 64,
}
tcp := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(443),
SYN: true,
Window: 65535,
}
_ = tcp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
options := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}
_ = gopacket.SerializeLayers(buf, options, ip, tcp)
packet := gopacket.NewPacket(buf.Bytes(), layers.LayerTypeIPv4, gopacket.Default)
packet.Metadata().Timestamp = time.Now()
return packet
}
Output: 8
Example (ShardedProcessors) ¶
ExampleProcessor_GetShardKey_shardedProcessors routes each packet to one of four Processor goroutines.
It mirrors the first fenced Go block of `docs/concurrency.md`.
`concurrency_doc_test.go` holds `ExampleProcessor_GetShardKey`, which runs the same pattern against packets it builds. This example carries the suffix because the two names share one package.
package main
import (
"fmt"
"hash/fnv"
"os"
"sync"
ja4plus "github.com/Crank-Git/ja4plus-go"
"github.com/gopacket/gopacket"
"github.com/gopacket/gopacket/pcapgo"
)
func main() {
const shards = 4
f, err := os.Open("capture.pcap")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// The page teaches the read path, and a close error of a read-only file changes no
// fingerprint. This comment reaches no comparison, because the matching rule of
// `docs_go_samples_test.go` drops every comment.
defer f.Close() //nolint:errcheck // The mirrored page states no error path here.
reader, err := pcapgo.NewReader(f)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// This Processor computes routing keys, and it processes no packet. GetShardKey
// holds no state, so one goroutine may use it while the shards run.
router := ja4plus.NewProcessor()
queues := make([]chan gopacket.Packet, shards)
var wg sync.WaitGroup
for i := range queues {
queues[i] = make(chan gopacket.Packet, 1024)
wg.Add(1)
go func(in <-chan gopacket.Packet) {
defer wg.Done()
// This goroutine owns the Processor, and no other goroutine touches it.
proc := ja4plus.NewProcessor()
for pkt := range in {
results, _ := proc.ProcessPacket(pkt)
for _, r := range results {
fmt.Printf("[%s] %s\n", r.Type, r.Fingerprint)
}
}
// The goroutine that owns the Processor closes its windows.
for _, r := range proc.CloseOpenWindows() {
fmt.Printf("[%s] %s\n", r.Type, r.Fingerprint)
}
}(queues[i])
}
for {
data, ci, err := reader.ReadPacketData()
if err != nil {
break
}
pkt := gopacket.NewPacket(data, reader.LinkType(), gopacket.Default)
pkt.Metadata().Timestamp = ci.Timestamp
// A packet that carries neither TCP nor UDP returns an empty key, and the hash
// of an empty key sends it to one fixed shard. Every packet reaches one shard.
h := fnv.New32a()
_, _ = h.Write([]byte(router.GetShardKey(pkt)))
queues[h.Sum32()%shards] <- pkt
}
for _, q := range queues {
close(q)
}
wg.Wait()
}
Output:
func (*Processor) ProcessPacket ¶
func (p *Processor) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, []error)
ProcessPacket runs all fingerprinters on the given packet. It returns all fingerprint results and any non-fatal errors encountered.
Example ¶
ExampleProcessor_ProcessPacket reads every packet of one capture file through one Processor.
It mirrors the fenced Go block of `docs/usage.md`.
package main
import (
"fmt"
"os"
ja4plus "github.com/Crank-Git/ja4plus-go"
"github.com/gopacket/gopacket"
"github.com/gopacket/gopacket/pcapgo"
)
func main() {
f, _ := os.Open("capture.pcap")
// The page teaches the read path, and a close error of a read-only file changes no
// fingerprint. This comment reaches no comparison, because the matching rule of
// `docs_go_samples_test.go` drops every comment.
defer f.Close() //nolint:errcheck // The mirrored page states no error path here.
reader, _ := pcapgo.NewReader(f)
proc := ja4plus.NewProcessor()
for {
data, ci, err := reader.ReadPacketData()
if err != nil {
break
}
pkt := gopacket.NewPacket(data, reader.LinkType(), gopacket.Default)
pkt.Metadata().Timestamp = ci.Timestamp
results, _ := proc.ProcessPacket(pkt)
for _, r := range results {
fmt.Printf("[%s] %s:%d -> %s:%d %s\n",
r.Type, r.SrcIP, r.SrcPort, r.DstIP, r.DstPort, r.Fingerprint)
}
}
}
Output:
type ProcessorOption ¶
type ProcessorOption func(*Processor)
ProcessorOption sets one option of a Processor at construction.
The maintainer ruled this shape on 2026-08-15 UTC, and comment 5299963400 of issue #649 records the ruling. Epic 10 (#100) freezes the exported surface, so a later option costs one more WithX function against this type rather than a new exported constructor. Issue #649 is the reversal path.
An option runs after the constructor fills every fingerprinter, so an option reads a Processor that is complete.
func WithKeyLog ¶
func WithKeyLog(keyLog *KeyLog) ProcessorOption
WithKeyLog returns the option that gives a Processor the key log.
The Processor holds the pointer the caller supplies, and it writes nothing to the key log. A fingerprinter that needs no secret ignores it. A caller that supplies nil gives the Processor no key log. The doc comment of KeyLog states that the value does not change after construction. So a sharded caller gives one KeyLog to every Processor, and the packet path takes no lock.
type SSHSessionInfo ¶
type SSHSessionInfo struct {
// SessionType names the class of the session.
SessionType string
// Description states the reading in one sentence.
Description string
// ClientMode is the mode of the client payload size.
ClientMode int
// ServerMode is the mode of the server payload size.
ServerMode int
// ClientSSH is the count of SSH packets the client sent.
ClientSSH int
// ServerSSH is the count of SSH packets the server sent.
ServerSSH int
// ClientACK is the count of bare ACKs the client sent.
ClientACK int
// ServerACK is the count of bare ACKs the server sent.
ServerACK int
}
SSHSessionInfo holds the interpretation of a JA4SSH fingerprint.
func InterpretJA4SSH ¶
func InterpretJA4SSH(fingerprint string) *SSHSessionInfo
InterpretJA4SSH parses a JA4SSH fingerprint and classifies the session type. Returns nil if the fingerprint format is invalid.
type SyncProcessor ¶
type SyncProcessor struct {
// contains filtered or unexported fields
}
SyncProcessor wraps a Processor and serializes every call with one mutex. Every method is safe to call from any number of goroutines at the same time. One Processor for each shard gives higher throughput, because the per-packet path then acquires no lock. Route packets to the shards with GetShardKey. A SyncProcessor exposes no way to reach the inner Processor, because a caller who reaches it can break the contract.
Example ¶
package main
import (
"fmt"
"net"
"sync"
"time"
ja4plus "github.com/Crank-Git/ja4plus-go"
"github.com/gopacket/gopacket"
"github.com/gopacket/gopacket/layers"
)
// processShared shares one SyncProcessor between the workers and returns every result.
// The mutex serializes every call, so this pattern gives lower throughput than a shard
// for each goroutine.
func processShared(packets []gopacket.Packet, workers int) []ja4plus.FingerprintResult {
proc := ja4plus.NewSyncProcessor()
input := make(chan gopacket.Packet, 16)
outputs := make(chan []ja4plus.FingerprintResult, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
var results []ja4plus.FingerprintResult
for packet := range input {
got, _ := proc.ProcessPacket(packet)
results = append(results, got...)
}
outputs <- results
}()
}
for _, packet := range packets {
input <- packet
}
close(input)
wg.Wait()
close(outputs)
var all []ja4plus.FingerprintResult
for results := range outputs {
all = append(all, results...)
}
return all
}
func main() {
packets := concurrencySYNPackets(8)
results := processShared(packets, 4)
fmt.Println(countJA4T(results))
}
// countJA4T returns the number of JA4T results. One TCP SYN packet produces one of them,
// so the count is stable whatever order the goroutines run in.
func countJA4T(results []ja4plus.FingerprintResult) int {
count := 0
for _, result := range results {
if result.Type == "ja4t" {
count++
}
}
return count
}
// concurrencySYNPackets returns the requested number of TCP SYN packets. Each packet
// carries its own source port, so the packets reach more than one shard.
func concurrencySYNPackets(count int) []gopacket.Packet {
packets := make([]gopacket.Packet, 0, count)
for i := 0; i < count; i++ {
packets = append(packets, concurrencySYNPacket(uint16(40000+i)))
}
return packets
}
// concurrencySYNPacket returns one TCP SYN packet from the source port.
func concurrencySYNPacket(srcPort uint16) gopacket.Packet {
ip := &layers.IPv4{
SrcIP: net.ParseIP("192.168.1.1"),
DstIP: net.ParseIP("10.0.0.1"),
Protocol: layers.IPProtocolTCP,
Version: 4,
TTL: 64,
}
tcp := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(443),
SYN: true,
Window: 65535,
}
_ = tcp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
options := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}
_ = gopacket.SerializeLayers(buf, options, ip, tcp)
packet := gopacket.NewPacket(buf.Bytes(), layers.LayerTypeIPv4, gopacket.Default)
packet.Metadata().Timestamp = time.Now()
return packet
}
Output: 8
func NewSyncProcessor ¶
func NewSyncProcessor(options ...ProcessorOption) *SyncProcessor
NewSyncProcessor returns a SyncProcessor that holds a new Processor. It passes each option to NewProcessor, so one option type serves both constructors. A caller that states no option receives a SyncProcessor with no key log.
func (*SyncProcessor) CleanupConnection ¶
func (p *SyncProcessor) CleanupConnection(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string)
CleanupConnection removes the state of the connection from every fingerprinter. Call it when the monitor evicts a connection, because state with no removal path leaks in a long-running monitor.
func (*SyncProcessor) CloseConnectionWindow ¶
func (p *SyncProcessor) CloseConnectionWindow(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto string) []FingerprintResult
CloseConnectionWindow returns the value of the window that one connection holds open, and it then removes the connection. Call it when the monitor evicts a connection, because CleanupConnection emits nothing and the open window is then lost. The mutex serializes it against a ProcessPacket call, so no result escapes while another goroutine changes the fingerprinter state.
func (*SyncProcessor) CloseOpenWindows ¶
func (p *SyncProcessor) CloseOpenWindows() []FingerprintResult
CloseOpenWindows returns the value of the window that each fingerprinter holds open. Call it when the packet source ends, because a connection whose last window never reaches the threshold holds that window open. The mutex serializes it against a ProcessPacket call, so no result escapes while another goroutine changes the fingerprinter state.
func (*SyncProcessor) GetShardKey ¶
func (p *SyncProcessor) GetShardKey(packet gopacket.Packet) string
GetShardKey returns one routing key for both directions of the connection. It returns an empty string for a packet that carries neither a TCP layer nor a UDP layer. The caller decides what to do with an empty key. It takes the mutex like every other exported method, because FR-concurrency-13 states one rule for the whole type.
func (*SyncProcessor) ProcessPacket ¶
func (p *SyncProcessor) ProcessPacket(packet gopacket.Packet) ([]FingerprintResult, []error)
ProcessPacket runs every fingerprinter on the packet and returns the results with any non-fatal errors. It holds the mutex for the whole call, so a result never escapes while another goroutine changes the fingerprinter state.
Example ¶
ExampleSyncProcessor_ProcessPacket shares one SyncProcessor across four worker goroutines.
It mirrors the second fenced Go block of `docs/concurrency.md`.
package main
import (
"fmt"
"os"
"sync"
ja4plus "github.com/Crank-Git/ja4plus-go"
"github.com/gopacket/gopacket"
"github.com/gopacket/gopacket/pcapgo"
)
func main() {
f, err := os.Open("capture.pcap")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// The page teaches the read path, and a close error of a read-only file changes no
// fingerprint. This comment reaches no comparison, because the matching rule of
// `docs_go_samples_test.go` drops every comment.
defer f.Close() //nolint:errcheck // The mirrored page states no error path here.
reader, err := pcapgo.NewReader(f)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Every worker shares this one SyncProcessor, and the mutex serializes each call.
proc := ja4plus.NewSyncProcessor()
queue := make(chan gopacket.Packet, 1024)
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for pkt := range queue {
results, _ := proc.ProcessPacket(pkt)
for _, r := range results {
fmt.Printf("[%s] %s\n", r.Type, r.Fingerprint)
}
}
}()
}
for {
data, ci, err := reader.ReadPacketData()
if err != nil {
break
}
pkt := gopacket.NewPacket(data, reader.LinkType(), gopacket.Default)
pkt.Metadata().Timestamp = ci.Timestamp
queue <- pkt
}
close(queue)
wg.Wait()
// Every worker has stopped, so one call closes every open window.
for _, r := range proc.CloseOpenWindows() {
fmt.Printf("[%s] %s\n", r.Type, r.Fingerprint)
}
}
Output:
func (*SyncProcessor) Reset ¶
func (p *SyncProcessor) Reset()
Reset clears the state of every fingerprinter. The mutex serializes it against a ProcessPacket call. It runs before that call or after it, and never during it.
type WindowCloser ¶
type WindowCloser interface {
// CloseOpenWindows returns the value of the window that each connection holds open,
// and it starts a new window on each one. A second call returns an empty slice.
CloseOpenWindows() []FingerprintResult
}
WindowCloser is the interface that a fingerprinter implements when a connection holds a window open at the end of the packet source. JA4SSH holds such a window, and no other method of this library does.
The interface sits beside Fingerprinter and not inside it. Fingerprinter is exported, and a new method on it breaks every third-party implementation, which `v1.0.0` forbids for the whole `v1` series. A caller discovers this interface with a type assertion, as a caller of `io.WriterTo` does. A stateless fingerprinter implements nothing, and Processor skips it. The maintainer ruled this placement on 2026-08-11, and issue #53 records the ruling.
The interface declares one method, and ConnectionWindowCloser declares the other one. A two-method interface skipped a type that implements one of the two methods. That type then lost the dispatch of the method it does implement. The maintainer ruled the split on 2026-08-12, and issue #268 records the ruling.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
ja4plus
command
|
|
|
examples
|
|
|
readcapture
command
Command readcapture reads a capture file and prints one line for each fingerprint.
|
Command readcapture reads a capture file and prints one line for each fingerprint. |
|
shardedprocessors
command
Command shardedprocessors routes each packet to one of four Processor goroutines.
|
Command shardedprocessors routes each packet to one of four Processor goroutines. |
|
syncprocessor
command
Command syncprocessor shares one SyncProcessor across four worker goroutines.
|
Command syncprocessor shares one SyncProcessor across four worker goroutines. |
|
internal
|
|
|
capture
Package capture opens a capture handle on one live interface.
|
Package capture opens a capture handle on one live interface. |
|
dbcache
Package dbcache validates a JA4+ fingerprint database and installs it in the cache file.
|
Package dbcache validates a JA4+ fingerprint database and installs it in the cache file. |
|
fuzzprop
Package fuzzprop holds the properties that every fuzz target of this repository runs.
|
Package fuzzprop holds the properties that every fuzz target of this repository runs. |
|
keylog
Package keylog reads the TLS secrets that a capture carries.
|
Package keylog reads the TLS secrets that a capture carries. |
|
mutationdiff
command
Command mutationdiff compares two mutation reports and names every new unsettled mutation.
|
Command mutationdiff compares two mutation reports and names every new unsettled mutation. |
|
mutationreport
command
Command mutationreport renders the JSON output of `gremlins` as a tracked report.
|
Command mutationreport renders the JSON output of `gremlins` as a tracked report. |
|
Package ja4db asks the ja4db.com service for the record of one JA4+ fingerprint.
|
Package ja4db asks the ja4db.com service for the record of one JA4+ fingerprint. |
