mic.git


mic

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

commit 4bf9c1fc7960c4e114e05359efbce195cfa20461
parent 7184eee9743ad85dd4fdd0cf64760a5050a09085
Author: djnn <[email protected]>
Date: Fri, 20 Feb 2026 11:12:44 +0900

refactor(fingerprint): switch from JA4 lookup to named profiles

Diffstat:
Mfingerprint/fingerprint.go | 8--------
Mfingerprint/tls.go | 34++++++++++++++++++++--------------
Mfingerprint/tls_test.go | 65++++++++++++++++++++++++++++++++---------------------------------
Mproxy/integration_test.go | 13+++++--------
Mproxy/proxy_test.go | 4++--

5 files changed, 59 insertions(+), 65 deletions(-)

diff --git a/fingerprint/fingerprint.go b/fingerprint/fingerprint.go
@@ -14,11 +14,3 @@ type TLSApplier interface {
 	Applier
 	ClientHelloID() utls.ClientHelloID
 }
-
-// TCPApplier will control TCP socket options (window size, TTL, MSS, etc.).
-// Left for a future implementation.
-//
-// type TCPApplier interface {
-//     Applier
-//     Apply(fd uintptr) error
-// }
diff --git a/fingerprint/tls.go b/fingerprint/tls.go
@@ -6,10 +6,16 @@ import (
 	utls "github.com/refraction-networking/utls"
 )
 
+// NameTable maps human-readable profile names to utls ClientHelloID presets.
+var NameTable = map[string]utls.ClientHelloID{
+	"chrome-120":    utls.HelloChrome_120,
+	"chrome-120-pq": utls.HelloChrome_120_PQ,
+	"firefox-120":   utls.HelloFirefox_120,
+	"safari-16":     utls.HelloSafari_16_0,
+	"edge-106":      utls.HelloEdge_106,
+}
+
 // Table maps known JA4-TLS fingerprint hashes to utls ClientHelloID presets.
-// JA4 hashes are computed from real traffic captures; verify against current
-// browser versions when adding new entries.
-// Table maps JA4-TLS hashes to utls ClientHelloID presets.
 //
 // Hashes are measured empirically against tlsinfo.me using cmd/probe — they
 // reflect what the utls preset actually emits, not captures from real browsers.
@@ -32,25 +38,25 @@ func Lookup(ja4 string) (utls.ClientHelloID, bool) {
 	return id, ok
 }
 
-// TLSFingerprint implements TLSApplier for a specific JA4 hash.
+// TLSFingerprint implements TLSApplier for a specific named profile.
 type TLSFingerprint struct {
-	ja4 string
-	id  utls.ClientHelloID
+	name string
+	id   utls.ClientHelloID
 }
 
-// NewTLS creates a TLSFingerprint for the given JA4 hash.
-// Returns an error if the hash is not in the built-in table.
-func NewTLS(ja4 string) (*TLSFingerprint, error) {
-	id, ok := Lookup(ja4)
+// ByName creates a TLSFingerprint for the given profile name.
+// Returns an error if the name is not in NameTable.
+func ByName(name string) (*TLSFingerprint, error) {
+	id, ok := NameTable[name]
 	if !ok {
-		return nil, fmt.Errorf("unknown JA4-TLS fingerprint %q", ja4)
+		return nil, fmt.Errorf("unknown TLS fingerprint profile %q", name)
 	}
-	return &TLSFingerprint{ja4: ja4, id: id}, nil
+	return &TLSFingerprint{name: name, id: id}, nil
 }
 
-// Name returns a human-readable identifier including the JA4 hash.
+// Name returns the profile name.
 func (f *TLSFingerprint) Name() string {
-	return "tls:" + f.ja4
+	return f.name
 }
 
 // ClientHelloID returns the utls preset for this fingerprint.
diff --git a/fingerprint/tls_test.go b/fingerprint/tls_test.go
@@ -1,62 +1,61 @@
 package fingerprint_test
 
 import (
-	"strings"
 	"testing"
 
 	"github.com/djnnvx/mic/fingerprint"
 	utls "github.com/refraction-networking/utls"
 )
 
-const knownJA4 = "t13d1516h2_8daaf6152771_b0da82dd1658"
-
-func TestLookup_Known(t *testing.T) {
-	id, ok := fingerprint.Lookup(knownJA4)
-	if !ok {
-		t.Fatalf("Lookup(%q) returned false; expected true", knownJA4)
+func TestByName_Happy(t *testing.T) {
+	fp, err := fingerprint.ByName("chrome-120")
+	if err != nil {
+		t.Fatalf("ByName(%q) returned error: %v", "chrome-120", err)
 	}
-	if id != utls.HelloChrome_120 {
-		t.Errorf("Lookup(%q) = %v; want HelloChrome_120", knownJA4, id)
+	if fp == nil {
+		t.Fatal("ByName returned nil fingerprint")
 	}
 }
 
-func TestLookup_Unknown(t *testing.T) {
-	_, ok := fingerprint.Lookup("unknown_hash_that_does_not_exist")
-	if ok {
-		t.Fatal("Lookup with unknown hash returned true; expected false")
+func TestByName_Error(t *testing.T) {
+	_, err := fingerprint.ByName("not-a-real-profile")
+	if err == nil {
+		t.Fatal("ByName with unknown name returned nil error; expected error")
 	}
 }
 
-func TestNewTLS_Happy(t *testing.T) {
-	fp, err := fingerprint.NewTLS(knownJA4)
+func TestName_ReturnsProfile(t *testing.T) {
+	fp, err := fingerprint.ByName("chrome-120")
 	if err != nil {
-		t.Fatalf("NewTLS(%q) returned error: %v", knownJA4, err)
+		t.Fatalf("ByName: %v", err)
 	}
-	if fp == nil {
-		t.Fatal("NewTLS returned nil fingerprint")
+	if fp.Name() != "chrome-120" {
+		t.Errorf("Name() = %q; want %q", fp.Name(), "chrome-120")
 	}
 }
 
-func TestNewTLS_Error(t *testing.T) {
-	_, err := fingerprint.NewTLS("not_a_real_ja4_hash")
-	if err == nil {
-		t.Fatal("NewTLS with unknown hash returned nil error; expected error")
+func TestLookup_Known(t *testing.T) {
+	id, ok := fingerprint.Lookup("t13d1516h2_8daaf6152771_02713d6af862")
+	if !ok {
+		t.Fatal("Lookup returned false for known Chrome 120 JA4 hash")
+	}
+	if id != utls.HelloChrome_120 {
+		t.Errorf("Lookup returned %v; want HelloChrome_120", id)
 	}
 }
 
-func TestName_ContainsJA4(t *testing.T) {
-	fp, err := fingerprint.NewTLS(knownJA4)
-	if err != nil {
-		t.Fatalf("NewTLS: %v", err)
-	}
-	name := fp.Name()
-	if !strings.Contains(name, knownJA4) {
-		t.Errorf("Name() = %q; does not contain JA4 hash %q", name, knownJA4)
+func TestLookup_Unknown(t *testing.T) {
+	_, ok := fingerprint.Lookup("unknown_hash_that_does_not_exist")
+	if ok {
+		t.Fatal("Lookup with unknown hash returned true; expected false")
 	}
 }
 
-func TestTable_MinEntries(t *testing.T) {
-	if len(fingerprint.Table) < 5 {
-		t.Errorf("Table has %d entries; want at least 5", len(fingerprint.Table))
+func TestNameTable_HasExpectedProfiles(t *testing.T) {
+	expected := []string{"chrome-120", "chrome-120-pq", "firefox-120", "safari-16", "edge-106"}
+	for _, name := range expected {
+		if _, ok := fingerprint.NameTable[name]; !ok {
+			t.Errorf("NameTable missing profile %q", name)
+		}
 	}
 }
diff --git a/proxy/integration_test.go b/proxy/integration_test.go
@@ -19,10 +19,7 @@ import (
 	"github.com/djnnvx/mic/fingerprint"
 )
 
-const (
-	chrome120JA4 = "t13d1516h2_8daaf6152771_b0da82dd1658"
-	testTimeout  = 10 * time.Second
-)
+const testTimeout = 10 * time.Second
 
 // serveHandler starts a TCP listener and dispatches accepted connections to h.
 // The listener is closed when the test ends.
@@ -100,9 +97,9 @@ func TestClientFront_Integration(t *testing.T) {
 	// Trust the test server's certificate.
 	caPool := ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs
 
-	fp, err := fingerprint.NewTLS(chrome120JA4)
+	fp, err := fingerprint.ByName("chrome-120")
 	if err != nil {
-		t.Fatalf("fingerprint.NewTLS: %v", err)
+		t.Fatalf("fingerprint.ByName: %v", err)
 	}
 
 	p := &Proxy{CAPool: caPool, Fingerprint: fp}
@@ -163,9 +160,9 @@ func TestServerFront_Integration(t *testing.T) {
 	// Generate a cert/key pair the proxy will use to terminate incoming client TLS.
 	certFile, keyFile, proxyCertPool := writeTempCertKey(t)
 
-	fp, err := fingerprint.NewTLS(chrome120JA4)
+	fp, err := fingerprint.ByName("chrome-120")
 	if err != nil {
-		t.Fatalf("fingerprint.NewTLS: %v", err)
+		t.Fatalf("fingerprint.ByName: %v", err)
 	}
 
 	p := &Proxy{
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
@@ -77,9 +77,9 @@ func TestDialTarget_HandshakeSucceeds(t *testing.T) {
 		io.Copy(io.Discard, conn)
 	}()
 
-	fp, err := fingerprint.NewTLS("t13d1516h2_8daaf6152771_b0da82dd1658")
+	fp, err := fingerprint.ByName("chrome-120")
 	if err != nil {
-		t.Fatalf("fingerprint.NewTLS: %v", err)
+		t.Fatalf("fingerprint.ByName: %v", err)
 	}
 
 	p := &Proxy{