mic.git


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 }