From ce0071c1efd68ee77436e48b812798b207bb9248 Mon Sep 17 00:00:00 2001 From: Leo Antunes Date: Mon, 6 Jul 2026 21:50:33 +0200 Subject: [PATCH 1/2] chore(ci): add go 1.26 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c84e00..038ad17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - go-version: [1.24, 1.25] + go-version: [1.24, 1.25, 1.26] steps: - name: Checkout code From afa01c62adf481b4e1574093636425747cae7a90 Mon Sep 17 00:00:00 2001 From: Leo Antunes Date: Mon, 6 Jul 2026 21:50:45 +0200 Subject: [PATCH 2/2] chore: add CLAUDE.md --- CLAUDE.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cea11d4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +JA4Plus is a small, dependency-free Go library that generates [JA4+ fingerprints](https://github.com/FoxIO-LLC/ja4) from TLS handshake data. It currently implements only the **JA4** fingerprint (based on `tls.ClientHelloInfo`). JA4H is intentionally omitted because Go's `net/http` doesn't expose header order (see README for details). + +## Commands + +- Run all tests: `go test ./...` +- Run tests with the race detector (as CI does): `go test -v -race ./...` +- Run a single test/subtest: `go test -run 'TestJA4/Basic_ClientHelloInfo' -v` +- Run benchmarks with allocation stats: `go test -bench=. -benchmem` +- Check for dependency/go.mod drift (as CI does): `go mod tidy -diff` + +CI (`.github/workflows/ci.yml`) runs on Go 1.24 and 1.25: `go mod tidy -diff` then `go test -v -race ./...`. + +## Architecture + +The public API is split across two files: + +- **ja4plus.go** — the `JA4()` function and its helpers. It's a direct, byte-level implementation of the JA4 spec: a fixed-width prefix (protocol/TLS version/SNI presence/cipher+extension counts/first-ALPN) is built as raw bytes via `appendTwoDigits`/`appendHexUint16`, followed by two truncated (6-byte) SHA-256 hashes from `cipherSuiteHash` and `extensionHash`. GREASE values ([RFC 8701](https://www.rfc-editor.org/rfc/rfc8701.html)) must be filtered out of versions, cipher suites, extensions, and ALPN protocols before anything is counted or hashed — `greaseFilter` is the single source of truth for that check. The SNI (0x0000) and ALPN (0x0010) extension IDs are counted in the prefix but must still be excluded from `extensionHash`'s input. +- **middlewares.go** — `JA4Middleware`, a helper for wiring JA4 into an `http.Server` despite Go not exposing `tls.ClientHelloInfo` to HTTP handlers. It caches fingerprints keyed by remote address (`StoreFingerprintFromClientHello`, invoked from `tls.Config.GetConfigForClient`), evicts them on connection close/hijack (`ConnStateCallback`, wired to `http.Server.ConnState`), and exposes the cached fingerprint to handlers via `Wrap` + `JA4FromContext`. All three hooks must be wired together (see the doc comment on `JA4Middleware` for the full example) or fingerprints won't be available/will leak. + +There are no external dependencies — only the Go standard library is used. + +## Performance + +`JA4()` is optimized down to a single heap allocation per call; `old.bench`/`new.bench` capture the before/after of a past optimization pass (15 allocs & 682ns/op → 1 alloc & 207ns/op). When touching this hot path, avoid reintroducing allocations — e.g. prefer appending bytes directly to the preallocated `out`/list slices over `string`/`fmt` formatting — and re-run `go test -bench=BenchmarkJA4 -benchmem` before/after to confirm allocation count and ns/op don't regress. + +## Testing conventions + +Tests are table-driven (`TestJA4`, `TestExtensionHash`). Expected fingerprint/hash values are hardcoded hex strings derived from the real SHA-256 output — when hashing/format logic changes intentionally, recompute expected values from the new behavior rather than hand-guessing them. + +`examples_test.go` and `middlewares_test.go` contain `Example*` functions that double as runnable documentation on pkg.go.dev; keep them consistent with real usage. `ExampleJA4`'s `// Output:` comment is Go-version-sensitive (default cipher suites can change across Go releases) — see the comment above it.