mic.git


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 }