mic~ proxy for JA4-TLS fingerprinting evasion |
| git clone https://github.com/djnnvx/mic.git |
| log | files | refs | archive | README | LICENSE |
ja4capture_test.go (2216B)
1 //go:build integration 2 3 package proxy 4 5 import ( 6 "bufio" 7 "bytes" 8 "crypto/tls" 9 "crypto/x509" 10 "fmt" 11 "io" 12 "net" 13 "testing" 14 "time" 15 16 "github.com/djnnvx/mic/fingerprint" 17 ) 18 19 // teeConn copies every byte read into an io.Writer, capturing the raw 20 // ClientHello while tls.Server reads normally. 21 type teeConn struct { 22 net.Conn 23 r io.Reader 24 } 25 26 func (tc *teeConn) Read(b []byte) (int, error) { return tc.r.Read(b) } 27 28 // captureJA4Server starts a TLS listener that computes the JA4 of each incoming 29 // ClientHello and sends it on the returned channel. It answers HTTP/1.1 200 so 30 // callers do not stall. addr is "localhost:PORT" so dialTarget sends a hostname 31 // SNI, matching the "d" (domain) indicator in the stored JA4 table. 32 func captureJA4Server(t *testing.T) (addr string, certPool *x509.CertPool, ja4s <-chan string) { 33 t.Helper() 34 35 pool, tlsCert := generateSelfSignedCert(t) 36 tlsCfg := &tls.Config{Certificates: []tls.Certificate{tlsCert}} 37 38 ln, err := net.Listen("tcp", "127.0.0.1:0") 39 if err != nil { 40 t.Fatalf("captureJA4Server: listen: %v", err) 41 } 42 t.Cleanup(func() { ln.Close() }) 43 44 _, port, _ := net.SplitHostPort(ln.Addr().String()) 45 46 ch := make(chan string, 16) 47 go func() { 48 for { 49 conn, err := ln.Accept() 50 if err != nil { 51 return 52 } 53 go serveCapture(conn, tlsCfg, ch) 54 } 55 }() 56 57 return "localhost:" + port, pool, ch 58 } 59 60 func serveCapture(conn net.Conn, cfg *tls.Config, ch chan<- string) { 61 defer conn.Close() 62 conn.SetDeadline(time.Now().Add(10 * time.Second)) //nolint:errcheck 63 64 var buf bytes.Buffer 65 tc := &teeConn{Conn: conn, r: io.TeeReader(conn, &buf)} 66 67 srv := tls.Server(tc, cfg) 68 if err := srv.Handshake(); err != nil { 69 if ja4, err2 := captureToJA4(buf.Bytes()); err2 == nil { 70 ch <- ja4 71 } 72 return 73 } 74 75 if ja4, err := captureToJA4(buf.Bytes()); err == nil { 76 ch <- ja4 77 } 78 79 reader := bufio.NewReader(srv) 80 for { 81 line, err := reader.ReadString('\n') 82 if err != nil || line == "\r\n" { 83 break 84 } 85 } 86 fmt.Fprintf(srv, "HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nOK\n") 87 } 88 89 func captureToJA4(raw []byte) (string, error) { 90 ch, err := fingerprint.ParseClientHello(raw) 91 if err != nil { 92 return "", err 93 } 94 return fingerprint.ComputeJA4(ch), nil 95 }