mic~ proxy for JA4-TLS fingerprinting evasion |
| git clone https://github.com/djnnvx/mic.git |
| log | files | refs | archive | README | LICENSE |
ja4_integration_test.go (4418B)
1 //go:build integration 2 3 package proxy 4 5 import ( 6 "bufio" 7 "crypto/tls" 8 "fmt" 9 "io" 10 "net" 11 "testing" 12 "time" 13 14 "github.com/djnnvx/mic/fingerprint" 15 ) 16 17 // Values measured empirically via cmd/probe against tlsinfo.me. 18 // chrome-120, chrome-120-pq and chrome-131 share a hash. JA4 ignores key share entries. 19 var expectedJA4 = map[string]string{ 20 "chrome-120": "t13d1516h2_8daaf6152771_02713d6af862", 21 "chrome-120-pq": "t13d1516h2_8daaf6152771_02713d6af862", 22 "chrome-131": "t13d1516h2_8daaf6152771_02713d6af862", 23 "chrome-133": "t13d1516h2_8daaf6152771_d8a2da3f94cd", 24 "firefox-120": "t13d1715h2_5b57614c22b0_5c2c66f702b0", 25 "safari-16": "t13d2014h2_a09f3c656075_14788d8d241b", 26 "ios-16": "t13d2014h2_a09f3c656075_14788d8d241b", 27 "edge-85": "t13d1515h2_8daaf6152771_de4a06bb82e3", 28 "edge-106": "t13d1516h2_8daaf6152771_e5627efa2ab1", 29 "opera-91": "t13d1516h2_8daaf6152771_e5627efa2ab1", 30 "android-11-okhttp": "t12d120700_d34a8e72043a_036209cd1ead", 31 "ipad-15": "t13d2014h2_a09f3c656075_14788d8d241b", 32 "qq-11": "t13d1516h2_8daaf6152771_e5627efa2ab1", 33 "360-7": "t12d2010s2_0bf03fa604e3_736b2a1ed4d3", 34 "360-11": "t13d1616h2_46e7e9700bed_4551aecd7b38", 35 } 36 37 // test client → TCP CONNECT → proxy (HttpsHandler) → uTLS(profile) → captureJA4Server 38 func TestClientFront_JA4(t *testing.T) { 39 for name, want := range expectedJA4 { 40 name, want := name, want 41 t.Run(name, func(t *testing.T) { 42 t.Parallel() 43 44 captureAddr, capturePool, ja4s := captureJA4Server(t) 45 46 fp, err := fingerprint.ByName(name) 47 if err != nil { 48 t.Fatalf("fingerprint.ByName(%q): %v", name, err) 49 } 50 51 proxyAddr := serveHandler(t, &Proxy{CAPool: capturePool, Fingerprint: fp}, HttpsHandler) 52 53 conn, err := net.DialTimeout("tcp", proxyAddr, testTimeout) 54 if err != nil { 55 t.Fatalf("dial proxy: %v", err) 56 } 57 defer conn.Close() 58 conn.SetDeadline(time.Now().Add(testTimeout)) //nolint:errcheck 59 60 fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", captureAddr, captureAddr) 61 if status, err := readHTTPStatus(bufio.NewReader(conn)); err != nil || status != 200 { 62 t.Fatalf("CONNECT: status=%d err=%v", status, err) 63 } 64 65 fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", captureAddr) 66 io.Copy(io.Discard, conn) //nolint:errcheck 67 68 if got := receiveJA4(t, ja4s); got != want { 69 t.Errorf("JA4 mismatch for %s:\n got %s\n want %s", name, got, want) 70 } 71 }) 72 } 73 } 74 75 // test client → TLS → proxy (ServerFrontHandler) → uTLS(profile) → captureJA4Server 76 func TestServerFront_JA4(t *testing.T) { 77 for name, want := range expectedJA4 { 78 name, want := name, want 79 t.Run(name, func(t *testing.T) { 80 t.Parallel() 81 82 captureAddr, capturePool, ja4s := captureJA4Server(t) 83 proxyCertFile, proxyKeyFile, proxyPool := writeTempCertKey(t) 84 85 fp, err := fingerprint.ByName(name) 86 if err != nil { 87 t.Fatalf("fingerprint.ByName(%q): %v", name, err) 88 } 89 90 p := &Proxy{BackendAddr: captureAddr, CAPool: capturePool, Fingerprint: fp} 91 handler, err := ServerFrontHandler(proxyCertFile, proxyKeyFile) 92 if err != nil { 93 t.Fatalf("ServerFrontHandler: %v", err) 94 } 95 proxyAddr := serveHandler(t, p, handler) 96 97 tlsConn, err := tls.DialWithDialer( 98 &net.Dialer{Timeout: testTimeout}, 99 "tcp", proxyAddr, 100 &tls.Config{RootCAs: proxyPool, ServerName: "localhost"}, 101 ) 102 if err != nil { 103 t.Fatalf("tls.Dial to proxy: %v", err) 104 } 105 defer tlsConn.Close() 106 tlsConn.SetDeadline(time.Now().Add(testTimeout)) //nolint:errcheck 107 108 fmt.Fprintf(tlsConn, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") 109 io.Copy(io.Discard, tlsConn) //nolint:errcheck 110 111 if got := receiveJA4(t, ja4s); got != want { 112 t.Errorf("JA4 mismatch for %s:\n got %s\n want %s", name, got, want) 113 } 114 }) 115 } 116 } 117 118 func readHTTPStatus(r *bufio.Reader) (int, error) { 119 line, err := r.ReadString('\n') 120 if err != nil { 121 return 0, err 122 } 123 var proto string 124 var status int 125 fmt.Sscanf(line, "%s %d", &proto, &status) 126 for { 127 h, err := r.ReadString('\n') 128 if err != nil || h == "\r\n" { 129 break 130 } 131 } 132 return status, nil 133 } 134 135 func receiveJA4(t *testing.T, ja4s <-chan string) string { 136 t.Helper() 137 select { 138 case ja4 := <-ja4s: 139 return ja4 140 case <-time.After(testTimeout): 141 t.Fatal("timed out waiting for captured JA4") 142 return "" 143 } 144 }