mic.git


mic

~ proxy for JA4-TLS fingerprinting evasion
git clone https://github.com/djnnvx/mic.git
log | files | refs | archive | README | LICENSE

commit 050d2f263528bcbb55f91b0d338f59b418e18776
parent b11226c70712291699e90011ef47f25bcb121c23
Author: djnn <[email protected]>
Date: Sat, 5 Sep 2026 08:55:09 +0200

cleanups and bugfixes

Diffstat:
Mcmd/client.go | 2--
Mcmd/client_test.go | 3+--
Mcmd/probe/main.go | 48+++++++++++-------------------------------------
Mcmd/root.go | 3---
Mfingerprint/ja4.go | 24++++++++++--------------
Mfingerprint/ja4_test.go | 21+++++++++------------
Mfingerprint/ja4s.go | 12+++++-------
Mfingerprint/ja4s_test.go | 17++++-------------
Mfingerprint/tls.go | 6+++++-
Mfingerprint/tls_test.go | 2++
Mproxy/ca.go | 13+++++--------
Mproxy/ca_test.go | 4++--
Mproxy/https.go | 13++++---------
Mproxy/integration_test.go | 14++++----------
Mproxy/ja4_integration_test.go | 20+++++++++-----------
Mproxy/ja4capture_test.go | 17++++++-----------
Mproxy/ja4s_capture_test.go | 16++++++----------
Mproxy/ja4s_integration_test.go | 21+++++----------------
Mproxy/proxy.go | 18+++++++-----------
Mproxy/proxy_test.go | 1-
Mproxy/serverfront.go | 1-

21 files changed, 95 insertions(+), 181 deletions(-)

diff --git a/cmd/client.go b/cmd/client.go
@@ -10,8 +10,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-// proxyURL renders a listen address as something a user can paste into curl -x.
-// A bare ":8080" needs a host.
 func proxyURL(listen string) string {
 	if strings.HasPrefix(listen, ":") {
 		return "localhost" + listen
diff --git a/cmd/client_test.go b/cmd/client_test.go
@@ -7,8 +7,7 @@ import (
 	"testing"
 )
 
-// run executes the client command with args, using an unroutable listen port so
-// that anything reaching p.Run() fails there instead of binding a real socket.
+// runClient uses an invalid port so p.Run() fails instead of binding a socket.
 func runClient(t *testing.T, args ...string) error {
 	t.Helper()
 	c := newClientCmd()
diff --git a/cmd/probe/main.go b/cmd/probe/main.go
@@ -1,5 +1,5 @@
-// probe dials tlsinfo.me/json with each known utls preset and prints the
-// JA4 hash that the server observes. Run once to populate the fingerprint table.
+// probe prints the JA4 hash tlsinfo.me observes for each mic profile, to
+// populate the fingerprint table.
 package main
 
 import (
@@ -8,40 +8,16 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"maps"
 	"net"
 	"net/http"
+	"slices"
 
 	utls "github.com/bogdanfinn/utls"
 	"golang.org/x/net/http2"
-)
 
-var presets = []struct {
-	name string
-	id   utls.ClientHelloID
-}{
-	// Chrome
-	{"HelloChrome_120", utls.HelloChrome_120},
-	{"HelloChrome_120_PQ", utls.HelloChrome_120_PQ},
-	{"HelloChrome_131", utls.HelloChrome_131},
-	{"HelloChrome_133", utls.HelloChrome_133},
-	// Firefox
-	{"HelloFirefox_120", utls.HelloFirefox_120},
-	// Safari / iOS
-	{"HelloSafari_15_6_1", utls.HelloSafari_15_6_1},
-	{"HelloSafari_16_0", utls.HelloSafari_16_0},
-	{"HelloIOS_15_5", utls.HelloIOS_15_5},
-	{"HelloIOS_15_6", utls.HelloIOS_15_6},
-	{"HelloIOS_16_0", utls.HelloIOS_16_0},
-	// Edge
-	{"HelloEdge_85", utls.HelloEdge_85},
-	{"HelloEdge_106", utls.HelloEdge_106},
-	// Opera
-	{"HelloOpera_89", utls.HelloOpera_89},
-	{"HelloOpera_90", utls.HelloOpera_90},
-	{"HelloOpera_91", utls.HelloOpera_91},
-	// Android
-	{"HelloAndroid_11_OkHttp", utls.HelloAndroid_11_OkHttp},
-}
+	"github.com/djnnvx/mic/fingerprint"
+)
 
 func dialUTLS(ctx context.Context, id utls.ClientHelloID) (*utls.UConn, error) {
 	tcpConn, err := (&net.Dialer{}).DialContext(ctx, "tcp", "tlsinfo.me:443")
@@ -75,9 +51,7 @@ func probe(id utls.ClientHelloID) (string, error) {
 	defer uconn.Close()
 
 	if uconn.ConnectionState().NegotiatedProtocol == "h2" {
-		// Reuse the already-established uconn rather than letting the Transport
-		// dial again. The first handshake is the one that produced the JA4 we
-		// want to measure.
+		// Reuse the handshaked uconn. A Transport redial would measure a different handshake.
 		cc, err := (&http2.Transport{}).NewClientConn(uconn)
 		if err != nil {
 			return "", err
@@ -106,12 +80,12 @@ func probe(id utls.ClientHelloID) (string, error) {
 }
 
 func main() {
-	for _, p := range presets {
-		ja4, err := probe(p.id)
+	for _, name := range slices.Sorted(maps.Keys(fingerprint.NameTable)) {
+		ja4, err := probe(fingerprint.NameTable[name])
 		if err != nil {
-			fmt.Printf("%-22s  ERROR: %v\n", p.name, err)
+			fmt.Printf("%-22s  ERROR: %v\n", name, err)
 			continue
 		}
-		fmt.Printf("%-22s  %s\n", p.name, ja4)
+		fmt.Printf("%-22s  %s\n", name, ja4)
 	}
 }
diff --git a/cmd/root.go b/cmd/root.go
@@ -8,9 +8,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-// loadCAPool reads a PEM bundle into a cert pool. AppendCertsFromPEM reports
-// failure only through its return value, and an empty pool makes every upstream
-// handshake fail with "unknown authority" instead of naming the real cause.
 func loadCAPool(path string) (*x509.CertPool, error) {
 	pem, err := os.ReadFile(path)
 	if err != nil {
diff --git a/fingerprint/ja4.go b/fingerprint/ja4.go
@@ -9,8 +9,8 @@ import (
 	"strings"
 )
 
-// noValues is the JA4 placeholder for a field with no values. The spec uses it
-// rather than sha256 of the empty string so an empty field is visible.
+// noValues is the JA4 placeholder for an empty field. The spec uses it instead
+// of sha256 of the empty string.
 const noValues = "000000000000"
 
 // ClientHelloFields holds the parsed TLS ClientHello data used to compute JA4.
@@ -30,9 +30,8 @@ func isGREASE(v uint16) bool {
 	return v&0x0f == 0x0a && v>>8 == v&0xff
 }
 
-// handshakeBody unwraps the TLS record and handshake headers from raw,
-// verifying the record content type, the handshake type, and length fields.
-// It returns the handshake body. tag prefixes errors ("ja4" or "ja4s").
+// handshakeBody unwraps the TLS record and handshake headers from raw, verifying
+// content type, handshake type and lengths. tag prefixes errors ("ja4" or "ja4s").
 func handshakeBody(raw []byte, hsType byte, tag string) ([]byte, error) {
 	if len(raw) < 5 {
 		return nil, fmt.Errorf("%s: record too short (%d bytes)", tag, len(raw))
@@ -267,10 +266,9 @@ func buildJA4a(ch *ClientHelloFields) string {
 	return fmt.Sprintf("t%s%s%02d%02d%s", tlsVer, sniChar, nc, ne, alpn)
 }
 
-// alpnChars is the two-character ALPN field: the first and last bytes of the
-// value. If either end byte is not ASCII alphanumeric, the whole value is
-// hex-encoded and the first and last characters of that hex string are used.
-// This also keeps client-controlled raw bytes out of the fingerprint.
+// alpnChars is the two-character ALPN field: first and last byte of the value.
+// If either end byte is not ASCII alphanumeric, hex-encode the whole value and
+// take the first and last hex characters instead.
 func alpnChars(v string) string {
 	if v == "" {
 		return "00"
@@ -287,7 +285,7 @@ func isALPNAlnum(b byte) bool {
 	return b >= '0' && b <= '9' || b >= 'A' && b <= 'Z' || b >= 'a' && b <= 'z'
 }
 
-// min99 caps n at 99, the maximum that fits in the two-digit JA4_a counter.
+// min99 caps n at 99, the max for the two-digit JA4_a counter.
 func min99(n int) int {
 	if n > 99 {
 		return 99
@@ -335,9 +333,8 @@ func buildJA4b(ciphers []uint16) string {
 	return fmt.Sprintf("%x", h)[:12]
 }
 
-// buildJA4c hashes sorted extension types (excluding SNI and ALPN, which vary
-// per-connection) then signature algorithms in wire order. Sorting is what
-// distinguishes JA4 from the raw JA4_r variant.
+// buildJA4c hashes sorted extension types (SNI and ALPN excluded) then signature
+// algorithms in wire order. Sorting is what distinguishes JA4 from JA4_r.
 func buildJA4c(exts []uint16, sigAlgs []uint16) string {
 	var filtered []uint16
 	for _, e := range exts {
@@ -356,7 +353,6 @@ func buildJA4c(exts []uint16, sigAlgs []uint16) string {
 	}
 	preimage := strings.Join(extParts, ",")
 
-	// No separator at all when there are no signature algorithms.
 	if len(sigAlgs) > 0 {
 		sigParts := make([]string, len(sigAlgs))
 		for i, s := range sigAlgs {
diff --git a/fingerprint/ja4_test.go b/fingerprint/ja4_test.go
@@ -8,9 +8,8 @@ import (
 	"testing"
 )
 
-// buildClientHello synthesizes a TLS handshake record carrying a ClientHello,
-// returned as raw bytes ready for ParseClientHello. extensions is the
-// wire-encoded extensions block (use ext() + the *Data helpers below).
+// buildClientHello synthesizes a TLS handshake record carrying a ClientHello.
+// extensions is the wire-encoded extensions block (see ext() and the *Data helpers).
 func buildClientHello(legacyVer uint16, ciphers []uint16, extensions []byte) []byte {
 	var body bytes.Buffer
 
@@ -108,8 +107,8 @@ func TestParseClientHello_Fields(t *testing.T) {
 	}
 }
 
-// An IP literal in SNI is still an SNI extension, so JA4_a stays "d".
-// The spec keys on extension presence, never on the value.
+// The spec keys on SNI extension presence, never on the value, so an IP literal
+// still yields "d".
 func TestComputeJA4_a_SNI_IPLiteral(t *testing.T) {
 	exts := ext(0x0000, sniData("192.0.2.1"))
 	ch, err := ParseClientHello(buildClientHello(0x0303, []uint16{0x1301}, exts))
@@ -150,7 +149,6 @@ func TestTLSVersionStr(t *testing.T) {
 }
 
 // The 8 hex-fallback examples from the FoxIO spec, plus the plain cases.
-// Checked end to end so the ALPN bytes travel through the parser.
 func TestComputeJA4_a_ALPNValues(t *testing.T) {
 	cases := []struct {
 		in   string
@@ -190,7 +188,7 @@ func TestBuildJA4b_EmptyCiphers(t *testing.T) {
 	}
 }
 
-// Only SNI and ALPN present: both are filtered out, so JA4_c has no values.
+// 0x0000 and 0x0010 are both filtered out, leaving JA4_c with no values.
 func TestBuildJA4c_EmptyFilteredExts(t *testing.T) {
 	if got := buildJA4c([]uint16{0x0000, 0x0010}, []uint16{0x0403}); got != "000000000000" {
 		t.Errorf("buildJA4c = %q; want 000000000000", got)
@@ -243,9 +241,8 @@ func TestParseClientHello_Errors(t *testing.T) {
 	}
 }
 
-// TestComputeJA4_KnownFixture pins the JA4 of realisticClientHello(). JA4_a is
-// human-readable. The two hashes are checked against their spec preimages,
-// independently verifiable with: printf '%s' '<preimage>' | sha256sum.
+// Hashes are checked against their spec preimages, independently verifiable
+// with: printf '%s' '<preimage>' | sha256sum.
 func TestComputeJA4_KnownFixture(t *testing.T) {
 	ch, err := ParseClientHello(realisticClientHello())
 	if err != nil {
@@ -342,8 +339,8 @@ func TestComputeJA4_a_Indicators(t *testing.T) {
 	}
 }
 
-// Both hash fields hit the no-values sentinel: no ciphers, and the only two
-// extensions are the ones JA4_c filters out.
+// Both hash fields hit the no-values sentinel: no ciphers, and every extension
+// is one JA4_c filters out.
 func TestComputeJA4_BothSentinels(t *testing.T) {
 	exts := append(ext(0x0000, sniData("example.com")), ext(0x0010, alpnData("h2"))...)
 	ch, err := ParseClientHello(buildClientHello(0x0303, nil, exts))
diff --git a/fingerprint/ja4s.go b/fingerprint/ja4s.go
@@ -136,11 +136,9 @@ func buildJA4Sa(sh *ServerHelloFields) string {
 	return fmt.Sprintf("t%s%02d%s", ver, nn, ja4sALPNCode(sh.ALPN))
 }
 
-// ja4sALPNCode encodes the negotiated ALPN as the JA4 two-character code:
-// first and last byte of the value. If either end byte is not ASCII
-// alphanumeric, hex-encode the whole value and take the first and last hex
-// characters instead. Duplicated from the JA4 side on purpose so the two
-// fingerprints stay independently editable.
+// ja4sALPNCode encodes the negotiated ALPN as the JA4 two-character code: first
+// and last byte of the value. If either end byte is not ASCII alphanumeric,
+// hex-encode the whole value and take the first and last hex characters instead.
 func ja4sALPNCode(v string) string {
 	if v == "" {
 		return "00"
@@ -158,8 +156,8 @@ func ja4sIsAlnum(c byte) bool {
 	return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
 }
 
-// buildJA4Sc hashes ServerHello extension types in wire order (no sort).
-// This is the difference from JA4_c, which sorts.
+// buildJA4Sc hashes ServerHello extension types in wire order. JA4_c sorts, this
+// must not.
 func buildJA4Sc(exts []uint16) string {
 	if len(exts) == 0 {
 		return "000000000000"
diff --git a/fingerprint/ja4s_test.go b/fingerprint/ja4s_test.go
@@ -7,11 +7,8 @@ import (
 	"testing"
 )
 
-// buildServerHello synthesizes a minimal TLS handshake record carrying a
-// ServerHello, returned as raw bytes ready for ParseServerHello.
-//
-// extensions is the wire-encoded extensions block (type + len + data tuples),
-// pre-built so each test can choose exactly what to include and in what order.
+// buildServerHello synthesizes a TLS handshake record carrying a ServerHello.
+// extensions is the wire-encoded extensions block (type + len + data tuples).
 func buildServerHello(legacyVer uint16, sessionIDLen int, cipher uint16, extensions []byte) []byte {
 	var body bytes.Buffer
 
@@ -46,8 +43,6 @@ func ext(t uint16, data []byte) []byte {
 }
 
 func TestParseServerHello_TLS13_Minimal(t *testing.T) {
-	// TLS 1.3 ServerHello: supported_versions(0x002b) carries the chosen 0x0304,
-	// plus key_share(0x0033) with empty data.
 	var exts []byte
 	exts = append(exts, ext(0x002b, []byte{0x03, 0x04})...) // chosen version
 	exts = append(exts, ext(0x0033, []byte{})...)           // key_share (data irrelevant)
@@ -74,7 +69,6 @@ func TestParseServerHello_TLS13_Minimal(t *testing.T) {
 }
 
 func TestParseServerHello_TLS13_WithALPN_h2(t *testing.T) {
-	// ALPN extension carrying a single chosen protocol "h2".
 	alpnList := []byte{0x00, 0x03, 0x02, 'h', '2'}
 
 	var exts []byte
@@ -94,8 +88,7 @@ func TestParseServerHello_TLS13_WithALPN_h2(t *testing.T) {
 }
 
 func TestParseServerHello_TLS12_NoSupportedVersions(t *testing.T) {
-	// TLS 1.2 ServerHello: legacy_version is the chosen version. No
-	// supported_versions extension. Often has session_ticket(0x0023) etc.
+	// Pre-1.3, legacy_version is the chosen version.
 	exts := ext(0x0023, []byte{}) // session_ticket, empty
 	raw := buildServerHello(0x0303, 32, 0xc02f, exts)
 
@@ -115,7 +108,6 @@ func TestParseServerHello_TLS12_NoSupportedVersions(t *testing.T) {
 }
 
 func TestParseServerHello_GREASEFiltered(t *testing.T) {
-	// GREASE extension type 0x0a0a should not appear in the parsed list.
 	var exts []byte
 	exts = append(exts, ext(0x0a0a, []byte{})...) // GREASE
 	exts = append(exts, ext(0x002b, []byte{0x03, 0x04})...)
@@ -165,8 +157,7 @@ func TestParseServerHello_Errors(t *testing.T) {
 }
 
 func TestComputeJA4S_Shape(t *testing.T) {
-	// Synthesize a typical TLS 1.3 ServerHello with [supported_versions, key_share]
-	// and assert the output shape: t<ver><nn><alpn>_<cipher>_<12hex>
+	// Shape: t<ver><nn><alpn>_<cipher>_<12hex>
 	sh := &ServerHelloFields{
 		LegacyVersion:    0x0303,
 		CipherSuite:      0x1301,
diff --git a/fingerprint/tls.go b/fingerprint/tls.go
@@ -8,7 +8,7 @@ import (
 	utls "github.com/bogdanfinn/utls"
 )
 
-// NameTable maps human-readable profile names to utls ClientHelloID presets.
+// NameTable maps profile names to utls ClientHelloID presets.
 var NameTable = map[string]utls.ClientHelloID{
 	"chrome-120":        utls.HelloChrome_120,
 	"chrome-120-pq":     utls.HelloChrome_120_PQ,
@@ -21,6 +21,10 @@ var NameTable = map[string]utls.ClientHelloID{
 	"edge-106":          utls.HelloEdge_106,
 	"opera-91":          utls.HelloOpera_91,
 	"android-11-okhttp": utls.HelloAndroid_11_OkHttp,
+	"ipad-15":           utls.HelloIPad_15_6,
+	"qq-11":             utls.HelloQQ_11_1,
+	"360-7":             utls.Hello360_7_5,
+	"360-11":            utls.Hello360_11_0,
 }
 
 type TLSFingerprint struct {
diff --git a/fingerprint/tls_test.go b/fingerprint/tls_test.go
@@ -41,6 +41,8 @@ func TestNameTable_HasExpectedProfiles(t *testing.T) {
 		"edge-85", "edge-106",
 		"opera-91",
 		"android-11-okhttp",
+		"ipad-15",
+		"qq-11", "360-7", "360-11",
 	}
 	for _, name := range expected {
 		if _, ok := fingerprint.NameTable[name]; !ok {
diff --git a/proxy/ca.go b/proxy/ca.go
@@ -53,8 +53,6 @@ func LoadOrGenerateCA(certPath, keyPath string) (*LocalCA, error) {
 	if certPath == "" && keyPath == "" {
 		return GenerateCA()
 	}
-	// A half-specified pair used to silently return an ephemeral CA that was
-	// never written, so callers told users to import a file that never existed.
 	if certPath == "" || keyPath == "" {
 		return nil, fmt.Errorf("CA cert path and key path must both be set or both be empty")
 	}
@@ -147,22 +145,21 @@ func (ca *LocalCA) Save(certPath, keyPath string) error {
 		return err
 	}
 	defer kf.Close()
-	// O_CREATE's mode is ignored when the file already exists, and the mode of a
-	// new file is masked by umask, so set it explicitly either way.
+	// O_CREATE's mode is ignored for an existing file and masked by umask for a
+	// new one, so set 0600 explicitly.
 	if err := kf.Chmod(0o600); err != nil {
 		return err
 	}
 	return pem.Encode(kf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
 }
 
-// CertPEM returns the CA certificate as PEM, suitable for curl --cacert or
-// importing into a system trust store.
+// CertPEM returns the CA certificate as PEM for curl --cacert or a trust store.
 func (ca *LocalCA) CertPEM() []byte {
 	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.derCert})
 }
 
-// issueCert returns a leaf certificate for host signed by the CA, issuing a new
-// one when the host is unseen or its cached leaf is at or near expiry.
+// issueCert returns a cached leaf for host, re-issuing when it is unseen or
+// near expiry.
 func (ca *LocalCA) issueCert(host string) (tls.Certificate, error) {
 	ca.mu.Lock()
 	defer ca.mu.Unlock()
diff --git a/proxy/ca_test.go b/proxy/ca_test.go
@@ -107,8 +107,8 @@ func TestIssueCert_Caches(t *testing.T) {
 	}
 }
 
-// A MitM leaf must never be usable as a CA. This is the property that lets an
-// attacker with the leaf key mint certs for any host, so assert it directly.
+// A MitM leaf must never be usable as a CA: the leaf key could then mint certs
+// for any host.
 func TestIssueCert_LeafIsNotACA(t *testing.T) {
 	ca, err := GenerateCA()
 	if err != nil {
diff --git a/proxy/https.go b/proxy/https.go
@@ -10,13 +10,10 @@ import (
 )
 
 // Bounds the CONNECT header read only, never the proxied stream.
-// Tunable: tests lower it to keep the suite fast.
 var headerReadTimeout = 10 * time.Second
 
-// parseAuthority extracts the server name and a dial-ready "host:port" from
-// a CONNECT request's authority. If the input lacks an explicit port (rare,
-// since CONNECT requires one), 443 is assumed. IPv6 literals in brackets are
-// handled (e.g. "[::1]:443" -> "::1", "[::1]:443").
+// parseAuthority splits a CONNECT authority into a server name and a dial-ready
+// "host:port", assuming port 443 and stripping IPv6 brackets when needed.
 func parseAuthority(authority string) (server, hostPort string) {
 	if h, _, err := net.SplitHostPort(authority); err == nil {
 		return h, authority
@@ -64,10 +61,8 @@ func HttpsHandler(clientConn net.Conn, p *Proxy) {
 			return
 		}
 
-		// Mirror the ALPN negotiated with the target so the client uses the same
-		// application protocol. Without this, Chrome-fingerprint uTLS often
-		// negotiates h2 with the target while the client sends HTTP/1.1, causing
-		// an immediate protocol error.
+		// Mirror the ALPN negotiated with the target: otherwise uTLS often
+		// negotiates h2 upstream while the client speaks HTTP/1.1, which fails.
 		nextProtos := []string{"http/1.1"}
 		if proto := targetConn.ConnectionState().NegotiatedProtocol; proto != "" {
 			nextProtos = []string{proto}
diff --git a/proxy/integration_test.go b/proxy/integration_test.go
@@ -76,9 +76,7 @@ func writeTempCertKey(t *testing.T) (certFile, keyFile string, pool *x509.CertPo
 	return certFile, keyFile, pool
 }
 
-// TestClientFront_Integration tests the full client-front (HTTP CONNECT) flow:
-//
-//	client → TCP CONNECT → proxy → uTLS(Chrome120) → httptest.TLSServer
+// client → TCP CONNECT → proxy → uTLS(Chrome120) → httptest.TLSServer
 func TestClientFront_Integration(t *testing.T) {
 	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Connection", "close")
@@ -116,8 +114,8 @@ func TestClientFront_Integration(t *testing.T) {
 		t.Fatalf("CONNECT status: %d; want 200", resp.StatusCode)
 	}
 
-	// The proxy has already done uTLS to the target; raw bytes we send here are
-	// wrapped in that TLS connection on our behalf.
+	// The proxy already handshaked uTLS to the target, so these raw bytes travel
+	// inside that TLS connection.
 	fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", targetAddr)
 
 	resp, err = http.ReadResponse(reader, nil)
@@ -133,9 +131,7 @@ func TestClientFront_Integration(t *testing.T) {
 	t.Logf("response body: %s", body)
 }
 
-// TestServerFront_Integration tests the server-front flow:
-//
-//	client → TLS → proxy (terminates TLS) → uTLS(Chrome120) → httptest.TLSServer
+// client → TLS → proxy (terminates TLS) → uTLS(Chrome120) → httptest.TLSServer
 func TestServerFront_Integration(t *testing.T) {
 	backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Connection", "close")
@@ -174,8 +170,6 @@ func TestServerFront_Integration(t *testing.T) {
 	defer tlsConn.Close()
 	tlsConn.SetDeadline(time.Now().Add(testTimeout))
 
-	// The proxy decrypts our TLS and re-encrypts with the configured fingerprint
-	// before forwarding to the backend.
 	fmt.Fprintf(tlsConn, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
 
 	reader := bufio.NewReader(tlsConn)
diff --git a/proxy/ja4_integration_test.go b/proxy/ja4_integration_test.go
@@ -15,10 +15,11 @@ import (
 )
 
 // Values measured empirically via cmd/probe against tlsinfo.me.
-// chrome-120-pq and chrome-131 are absent: they emit the same JA4 as chrome-120
-// because JA4 does not distinguish the X25519MLKEM768 key share.
+// chrome-120, chrome-120-pq and chrome-131 share a hash. JA4 ignores key share entries.
 var expectedJA4 = map[string]string{
 	"chrome-120":        "t13d1516h2_8daaf6152771_02713d6af862",
+	"chrome-120-pq":     "t13d1516h2_8daaf6152771_02713d6af862",
+	"chrome-131":        "t13d1516h2_8daaf6152771_02713d6af862",
 	"chrome-133":        "t13d1516h2_8daaf6152771_d8a2da3f94cd",
 	"firefox-120":       "t13d1715h2_5b57614c22b0_5c2c66f702b0",
 	"safari-16":         "t13d2014h2_a09f3c656075_14788d8d241b",
@@ -27,12 +28,13 @@ var expectedJA4 = map[string]string{
 	"edge-106":          "t13d1516h2_8daaf6152771_e5627efa2ab1",
 	"opera-91":          "t13d1516h2_8daaf6152771_e5627efa2ab1",
 	"android-11-okhttp": "t12d120700_d34a8e72043a_036209cd1ead",
+	"ipad-15":           "t13d2014h2_a09f3c656075_14788d8d241b",
+	"qq-11":             "t13d1516h2_8daaf6152771_e5627efa2ab1",
+	"360-7":             "t12d2010s2_0bf03fa604e3_736b2a1ed4d3",
+	"360-11":            "t13d1616h2_46e7e9700bed_4551aecd7b38",
 }
 
-// TestClientFront_JA4 verifies that each fingerprint profile produces the
-// expected JA4 on outbound connections in client-front (HTTP CONNECT) mode.
-//
-//	test client → TCP CONNECT → proxy (HttpsHandler) → uTLS(profile) → captureJA4Server
+// test client → TCP CONNECT → proxy (HttpsHandler) → uTLS(profile) → captureJA4Server
 func TestClientFront_JA4(t *testing.T) {
 	for name, want := range expectedJA4 {
 		name, want := name, want
@@ -60,7 +62,6 @@ func TestClientFront_JA4(t *testing.T) {
 				t.Fatalf("CONNECT: status=%d err=%v", status, err)
 			}
 
-			// Send a plain HTTP request through the uTLS tunnel the proxy established.
 			fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", captureAddr)
 			io.Copy(io.Discard, conn) //nolint:errcheck
 
@@ -71,10 +72,7 @@ func TestClientFront_JA4(t *testing.T) {
 	}
 }
 
-// TestServerFront_JA4 verifies that each fingerprint profile produces the
-// expected JA4 on outbound connections in server-front (TLS termination) mode.
-//
-//	test client → TLS → proxy (ServerFrontHandler) → uTLS(profile) → captureJA4Server
+// test client → TLS → proxy (ServerFrontHandler) → uTLS(profile) → captureJA4Server
 func TestServerFront_JA4(t *testing.T) {
 	for name, want := range expectedJA4 {
 		name, want := name, want
diff --git a/proxy/ja4capture_test.go b/proxy/ja4capture_test.go
@@ -16,8 +16,8 @@ import (
 	"github.com/djnnvx/mic/fingerprint"
 )
 
-// teeConn tees all bytes read from it into an io.Writer, letting us capture
-// the raw ClientHello while tls.Server reads from the connection normally.
+// teeConn copies every byte read into an io.Writer, capturing the raw
+// ClientHello while tls.Server reads normally.
 type teeConn struct {
 	net.Conn
 	r io.Reader
@@ -25,15 +25,10 @@ type teeConn struct {
 
 func (tc *teeConn) Read(b []byte) (int, error) { return tc.r.Read(b) }
 
-// captureJA4Server starts a TLS listener that captures the JA4 fingerprint
-// from each incoming connection's ClientHello and sends it on the returned channel.
-// The server also serves a minimal HTTP/1.1 200 response so callers can complete
-// their request without stalling.
-//
-// The returned addr is "localhost:PORT" (not "127.0.0.1:PORT") so that
-// dialTarget uses a hostname SNI rather than an IP address. This matches the
-// "d" (domain) indicator in the stored JA4 fingerprint table, which was
-// measured against tlsinfo.me, also a hostname.
+// captureJA4Server starts a TLS listener that computes the JA4 of each incoming
+// ClientHello and sends it on the returned channel. It answers HTTP/1.1 200 so
+// callers do not stall. addr is "localhost:PORT" so dialTarget sends a hostname
+// SNI, matching the "d" (domain) indicator in the stored JA4 table.
 func captureJA4Server(t *testing.T) (addr string, certPool *x509.CertPool, ja4s <-chan string) {
 	t.Helper()
 
diff --git a/proxy/ja4s_capture_test.go b/proxy/ja4s_capture_test.go
@@ -10,9 +10,8 @@ import (
 	"github.com/djnnvx/mic/fingerprint"
 )
 
-// teeWriteConn tees every Write into a buffer. As soon as the buffer contains
-// a complete ServerHello it computes the JA4S and emits it on ch (once). The
-// JA4S is what mic itself writes out: the server-side fingerprint.
+// teeWriteConn tees every Write into a buffer and emits the JA4S once the
+// buffer holds a complete ServerHello.
 type teeWriteConn struct {
 	net.Conn
 	mu   sync.Mutex
@@ -24,10 +23,8 @@ type teeWriteConn struct {
 func (t *teeWriteConn) Write(p []byte) (int, error) {
 	t.mu.Lock()
 	if !t.sent {
-		// Only start collecting once we see the first TLS handshake record.
-		// In client-front MitM the proxy writes a plaintext "HTTP/1.1 200" line
-		// through the same conn before the TLS handshake; those bytes are not
-		// part of the ServerHello and must not poison the buffer.
+		// In client-front MitM the proxy writes a plaintext "HTTP/1.1 200" line on
+		// this conn first, so only collect from the first TLS handshake record.
 		if t.buf.Len() > 0 || (len(p) >= 2 && p[0] == 0x16 && p[1] == 0x03) {
 			t.buf.Write(p)
 			if ja4s, err := captureToJA4S(t.buf.Bytes()); err == nil {
@@ -51,9 +48,8 @@ func captureToJA4S(raw []byte) (string, error) {
 	return fingerprint.ComputeJA4S(sh), nil
 }
 
-// captureJA4SListener wraps every conn accepted on ln so that the ServerHello
-// mic writes back to the client is intercepted and emitted on the returned
-// channel.
+// captureJA4SListener wraps each accepted conn so the ServerHello mic writes
+// back is captured and emitted on the returned channel.
 func captureJA4SListener(ln net.Listener, p *Proxy, h Handler) <-chan string {
 	ch := make(chan string, 16)
 	go func() {
diff --git a/proxy/ja4s_integration_test.go b/proxy/ja4s_integration_test.go
@@ -20,18 +20,15 @@ import (
 // JA4S well-formed shape: t<2-digit ver><2-digit ext-count><alpn>_<4-hex cipher>_<12-hex hash>
 var ja4sShape = regexp.MustCompile(`^t\d{2}\d{2}[a-z0-9]{2}_[0-9a-f]{4}_[0-9a-f]{12}$`)
 
-// Baselines: the JA4S mic emits today using stdlib crypto/tls (Go 1.25).
-// These are observation pins. If they drift, either stdlib's TLS behavior
-// changed or our parser broke. Verify with the captured value before updating.
+// Baselines pinned from stdlib crypto/tls (Go 1.25). Drift means stdlib TLS
+// behavior changed or the parser broke. Verify the captured value before
+// updating.
 const (
 	baselineJA4S_ServerFront = "t130200_1301_a56c5b993250"
 	baselineJA4S_ClientFront = "t130200_1301_a56c5b993250"
 )
 
-// TestServerFront_JA4S_Baseline measures the JA4S mic emits when terminating
-// TLS as a server (server-front mode) using stdlib crypto/tls. No spoofing.
-//
-//	test client → TLS → mic (ServerFrontHandler, tls.Server) → httptest backend
+// test client → TLS → mic (ServerFrontHandler, tls.Server) → httptest backend
 func TestServerFront_JA4S_Baseline(t *testing.T) {
 	backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Connection", "close")
@@ -86,11 +83,7 @@ func TestServerFront_JA4S_Baseline(t *testing.T) {
 	}
 }
 
-// TestClientFront_JA4S_Baseline measures the JA4S mic emits when serving the
-// MitM cert in client-front mode (the proxy presents an issued leaf cert to
-// the client after the CONNECT).
-//
-//	test client → TCP CONNECT → mic (HttpsHandler, tls.Server with issued cert) → upstream
+// test client → TCP CONNECT → mic (HttpsHandler, tls.Server with issued cert) → upstream
 func TestClientFront_JA4S_Baseline(t *testing.T) {
 	upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Connection", "close")
@@ -119,8 +112,6 @@ func TestClientFront_JA4S_Baseline(t *testing.T) {
 
 	// Pin a deterministic upstream fingerprint so dialTarget never picks a
 	// randomized ClientHello with curves the test backend can't negotiate.
-	// The choice is irrelevant to JA4S, which only measures the ServerHello
-	// mic writes back to the test client.
 	fp, err := fingerprint.ByName("chrome-120")
 	if err != nil {
 		t.Fatalf("fingerprint.ByName: %v", err)
@@ -145,8 +136,6 @@ func TestClientFront_JA4S_Baseline(t *testing.T) {
 			return
 		}
 
-		// Now do the TLS handshake with mic; mic will emit a ServerHello with
-		// the per-host cert it just issued.
 		tlsConn := tls.Client(conn, &tls.Config{
 			ServerName: "127.0.0.1",
 			RootCAs:    micCAPool,
diff --git a/proxy/proxy.go b/proxy/proxy.go
@@ -32,8 +32,6 @@ type Proxy struct {
 
 type Handler func(conn net.Conn, p *Proxy)
 
-// dialTarget dials host (host:port) and returns a uTLS connection after a
-// successful handshake. Falls back to HelloRandomized when no fingerprint is set.
 func (p *Proxy) dialTarget(host string) (*utls.UConn, error) {
 	tcpConn, err := net.DialTimeout("tcp", host, dialTimeout)
 	if err != nil {
@@ -64,9 +62,8 @@ func (p *Proxy) dialTarget(host string) (*utls.UConn, error) {
 	return uconn, nil
 }
 
-// bufferedConn pairs a net.Conn with a bufio.Reader that may already contain
-// data read ahead from the underlying conn (e.g. after http.ReadRequest).
-// Reads come from the buffer; writes and Close go to the conn.
+// bufferedConn reads from a bufio.Reader that may already hold bytes read ahead
+// from the conn (e.g. by http.ReadRequest). Writes and Close go to the conn.
 type bufferedConn struct {
 	r *bufio.Reader
 	net.Conn
@@ -88,8 +85,8 @@ type halfCloser interface {
 	CloseWrite() error
 }
 
-// closeWrite shuts down only the write side so the peer still sees the data
-// already sent. Falls back to a full Close when half-close is unavailable.
+// closeWrite shuts down only the write side so the peer still receives the data
+// already sent.
 func closeWrite(c io.Closer) {
 	if hc, ok := c.(halfCloser); ok && hc.CloseWrite() == nil {
 		return
@@ -97,10 +94,9 @@ func closeWrite(c io.Closer) {
 	c.Close()
 }
 
-// pipe copies bidirectionally between a and b. A finished direction only
-// half-closes its write side: a client that shuts down its write end after
-// sending a request must still receive the response. Both ends are fully
-// closed once both directions are done.
+// pipe copies both directions. A finished direction only half-closes its write
+// side: a client that shut its write end after sending a request must still
+// receive the response.
 func pipe(a, b io.ReadWriteCloser) {
 	var wg sync.WaitGroup
 	wg.Add(2)
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
@@ -127,7 +127,6 @@ func TestDialTarget_FallbackToRandomized(t *testing.T) {
 	conn, err := p.dialTarget(ln.Addr().String())
 	if err != nil {
 		// HelloRandomized sometimes picks a curve the local TLS stack rejects.
-		// That is the only tolerated failure, anything else is a real bug.
 		if !strings.Contains(err.Error(), "CurvePreferences includes unsupported curve") {
 			t.Fatalf("dialTarget with randomized preset: %v", err)
 		}
diff --git a/proxy/serverfront.go b/proxy/serverfront.go
@@ -22,7 +22,6 @@ func ServerFrontHandler(certFile, keyFile string) (Handler, error) {
 	return func(clientConn net.Conn, p *Proxy) {
 		defer clientConn.Close()
 
-		// Terminate incoming TLS with stdlib crypto/tls.
 		tlsConn := tls.Server(clientConn, tlsCfg)
 		if err := tlsConn.Handshake(); err != nil {
 			log.Printf("ServerFront: TLS handshake with client failed: %v", err)