mic.git


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 }