mic~ proxy for JA4-TLS fingerprinting evasion |
| git clone https://github.com/djnnvx/mic.git |
| log | files | refs | archive | README | LICENSE |
ja4s_integration_test.go (4547B)
1 //go:build integration 2 3 package proxy 4 5 import ( 6 "bufio" 7 "crypto/tls" 8 "crypto/x509" 9 "fmt" 10 "net" 11 "net/http" 12 "net/http/httptest" 13 "regexp" 14 "testing" 15 "time" 16 17 "github.com/djnnvx/mic/fingerprint" 18 ) 19 20 // JA4S well-formed shape: t<2-digit ver><2-digit ext-count><alpn>_<4-hex cipher>_<12-hex hash> 21 var ja4sShape = regexp.MustCompile(`^t\d{2}\d{2}[a-z0-9]{2}_[0-9a-f]{4}_[0-9a-f]{12}$`) 22 23 // Baselines pinned from stdlib crypto/tls (Go 1.25). Drift means stdlib TLS 24 // behavior changed or the parser broke. Verify the captured value before 25 // updating. 26 const ( 27 baselineJA4S_ServerFront = "t130200_1301_a56c5b993250" 28 baselineJA4S_ClientFront = "t130200_1301_a56c5b993250" 29 ) 30 31 // test client → TLS → mic (ServerFrontHandler, tls.Server) → httptest backend 32 func TestServerFront_JA4S_Baseline(t *testing.T) { 33 backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 w.Header().Set("Connection", "close") 35 w.WriteHeader(http.StatusOK) 36 fmt.Fprintln(w, "ok") 37 })) 38 defer backend.Close() 39 40 backendPool := backend.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs 41 certFile, keyFile, proxyPool := writeTempCertKey(t) 42 43 handler, err := ServerFrontHandler(certFile, keyFile) 44 if err != nil { 45 t.Fatalf("ServerFrontHandler: %v", err) 46 } 47 48 ln, err := net.Listen("tcp", "127.0.0.1:0") 49 if err != nil { 50 t.Fatalf("listen: %v", err) 51 } 52 t.Cleanup(func() { ln.Close() }) 53 54 p := &Proxy{BackendAddr: backend.Listener.Addr().String(), CAPool: backendPool} 55 ja4sCh := captureJA4SListener(ln, p, handler) 56 57 go func() { 58 tlsConn, err := tls.DialWithDialer( 59 &net.Dialer{Timeout: testTimeout}, 60 "tcp", ln.Addr().String(), 61 &tls.Config{RootCAs: proxyPool, ServerName: "localhost"}, 62 ) 63 if err != nil { 64 return 65 } 66 defer tlsConn.Close() 67 fmt.Fprintf(tlsConn, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") 68 _, _ = bufio.NewReader(tlsConn).ReadString('\n') 69 }() 70 71 select { 72 case got := <-ja4sCh: 73 t.Logf("server-front JA4S: %s", got) 74 if !ja4sShape.MatchString(got) { 75 t.Errorf("JA4S = %q; does not match expected shape", got) 76 } 77 if got != baselineJA4S_ServerFront { 78 t.Errorf("JA4S drift: got %q, baseline %q (update baseline if stdlib behavior changed intentionally)", 79 got, baselineJA4S_ServerFront) 80 } 81 case <-time.After(testTimeout): 82 t.Fatal("timeout waiting for JA4S") 83 } 84 } 85 86 // test client → TCP CONNECT → mic (HttpsHandler, tls.Server with issued cert) → upstream 87 func TestClientFront_JA4S_Baseline(t *testing.T) { 88 upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 89 w.Header().Set("Connection", "close") 90 w.WriteHeader(http.StatusOK) 91 fmt.Fprintln(w, "ok") 92 })) 93 defer upstream.Close() 94 95 upstreamPool := upstream.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs 96 97 ca, err := GenerateCA() 98 if err != nil { 99 t.Fatalf("GenerateCA: %v", err) 100 } 101 102 micCAPool := x509.NewCertPool() 103 if !micCAPool.AppendCertsFromPEM(ca.CertPEM()) { 104 t.Fatalf("failed to append mic CA to pool") 105 } 106 107 ln, err := net.Listen("tcp", "127.0.0.1:0") 108 if err != nil { 109 t.Fatalf("listen: %v", err) 110 } 111 t.Cleanup(func() { ln.Close() }) 112 113 // Pin a deterministic upstream fingerprint so dialTarget never picks a 114 // randomized ClientHello with curves the test backend can't negotiate. 115 fp, err := fingerprint.ByName("chrome-120") 116 if err != nil { 117 t.Fatalf("fingerprint.ByName: %v", err) 118 } 119 120 p := &Proxy{CAPool: upstreamPool, LocalCA: ca, Fingerprint: fp} 121 ja4sCh := captureJA4SListener(ln, p, HttpsHandler) 122 123 go func() { 124 conn, err := net.DialTimeout("tcp", ln.Addr().String(), testTimeout) 125 if err != nil { 126 return 127 } 128 defer conn.Close() 129 conn.SetDeadline(time.Now().Add(testTimeout)) 130 131 upstreamAddr := upstream.Listener.Addr().String() 132 fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", upstreamAddr, upstreamAddr) 133 134 reader := bufio.NewReader(conn) 135 if _, err := http.ReadResponse(reader, nil); err != nil { 136 return 137 } 138 139 tlsConn := tls.Client(conn, &tls.Config{ 140 ServerName: "127.0.0.1", 141 RootCAs: micCAPool, 142 }) 143 _ = tlsConn.Handshake() 144 }() 145 146 select { 147 case got := <-ja4sCh: 148 t.Logf("client-front JA4S: %s", got) 149 if !ja4sShape.MatchString(got) { 150 t.Errorf("JA4S = %q; does not match expected shape", got) 151 } 152 if got != baselineJA4S_ClientFront { 153 t.Errorf("JA4S drift: got %q, baseline %q (update baseline if stdlib behavior changed intentionally)", 154 got, baselineJA4S_ClientFront) 155 } 156 case <-time.After(testTimeout): 157 t.Fatal("timeout waiting for JA4S") 158 } 159 }