mic.git


mic

~ proxy for JA4-TLS fingerprinting evasion
git clone https://github.com/djnnvx/mic.git
log | files | refs | archive | README | LICENSE

integration_test.go (4846B)

      1 //go:build integration
      2 
      3 package proxy
      4 
      5 import (
      6 	"bufio"
      7 	"crypto/tls"
      8 	"crypto/x509"
      9 	"encoding/pem"
     10 	"fmt"
     11 	"io"
     12 	"net"
     13 	"net/http"
     14 	"net/http/httptest"
     15 	"os"
     16 	"testing"
     17 	"time"
     18 
     19 	"github.com/djnnvx/mic/fingerprint"
     20 )
     21 
     22 const testTimeout = 10 * time.Second
     23 
     24 func serveHandler(t *testing.T, p *Proxy, h Handler) string {
     25 	t.Helper()
     26 	ln, err := net.Listen("tcp", "127.0.0.1:0")
     27 	if err != nil {
     28 		t.Fatalf("listen: %v", err)
     29 	}
     30 	t.Cleanup(func() { ln.Close() })
     31 
     32 	go func() {
     33 		for {
     34 			conn, err := ln.Accept()
     35 			if err != nil {
     36 				return
     37 			}
     38 			go h(conn, p)
     39 		}
     40 	}()
     41 
     42 	return ln.Addr().String()
     43 }
     44 
     45 func writeTempCertKey(t *testing.T) (certFile, keyFile string, pool *x509.CertPool) {
     46 	t.Helper()
     47 
     48 	pool, tlsCert := generateSelfSignedCert(t)
     49 
     50 	cf, err := os.CreateTemp("", "mic-cert-*.pem")
     51 	if err != nil {
     52 		t.Fatalf("creating cert temp file: %v", err)
     53 	}
     54 	pem.Encode(cf, &pem.Block{Type: "CERTIFICATE", Bytes: tlsCert.Certificate[0]})
     55 	cf.Close()
     56 	certFile = cf.Name()
     57 
     58 	// PKCS8 format is required: tls.LoadX509KeyPair accepts "PRIVATE KEY" blocks.
     59 	keyBytes, err := x509.MarshalPKCS8PrivateKey(tlsCert.PrivateKey)
     60 	if err != nil {
     61 		t.Fatalf("marshaling private key: %v", err)
     62 	}
     63 	kf, err := os.CreateTemp("", "mic-key-*.pem")
     64 	if err != nil {
     65 		t.Fatalf("creating key temp file: %v", err)
     66 	}
     67 	pem.Encode(kf, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
     68 	kf.Close()
     69 	keyFile = kf.Name()
     70 
     71 	t.Cleanup(func() {
     72 		os.Remove(certFile)
     73 		os.Remove(keyFile)
     74 	})
     75 
     76 	return certFile, keyFile, pool
     77 }
     78 
     79 // client → TCP CONNECT → proxy → uTLS(Chrome120) → httptest.TLSServer
     80 func TestClientFront_Integration(t *testing.T) {
     81 	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
     82 		w.Header().Set("Connection", "close")
     83 		w.WriteHeader(http.StatusOK)
     84 		fmt.Fprintln(w, "client-front integration OK")
     85 	}))
     86 	defer ts.Close()
     87 
     88 	caPool := ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs
     89 
     90 	fp, err := fingerprint.ByName("chrome-120")
     91 	if err != nil {
     92 		t.Fatalf("fingerprint.ByName: %v", err)
     93 	}
     94 
     95 	p := &Proxy{CAPool: caPool, Fingerprint: fp}
     96 	proxyAddr := serveHandler(t, p, HttpsHandler)
     97 
     98 	conn, err := net.DialTimeout("tcp", proxyAddr, testTimeout)
     99 	if err != nil {
    100 		t.Fatalf("dial proxy: %v", err)
    101 	}
    102 	defer conn.Close()
    103 	conn.SetDeadline(time.Now().Add(testTimeout))
    104 
    105 	targetAddr := ts.Listener.Addr().String()
    106 	fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetAddr, targetAddr)
    107 
    108 	reader := bufio.NewReader(conn)
    109 	resp, err := http.ReadResponse(reader, nil)
    110 	if err != nil {
    111 		t.Fatalf("reading CONNECT response: %v", err)
    112 	}
    113 	if resp.StatusCode != http.StatusOK {
    114 		t.Fatalf("CONNECT status: %d; want 200", resp.StatusCode)
    115 	}
    116 
    117 	// The proxy already handshaked uTLS to the target, so these raw bytes travel
    118 	// inside that TLS connection.
    119 	fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", targetAddr)
    120 
    121 	resp, err = http.ReadResponse(reader, nil)
    122 	if err != nil {
    123 		t.Fatalf("reading GET response: %v", err)
    124 	}
    125 	defer resp.Body.Close()
    126 
    127 	if resp.StatusCode != http.StatusOK {
    128 		t.Errorf("GET status: %d; want 200", resp.StatusCode)
    129 	}
    130 	body, _ := io.ReadAll(resp.Body)
    131 	t.Logf("response body: %s", body)
    132 }
    133 
    134 // client → TLS → proxy (terminates TLS) → uTLS(Chrome120) → httptest.TLSServer
    135 func TestServerFront_Integration(t *testing.T) {
    136 	backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    137 		w.Header().Set("Connection", "close")
    138 		w.WriteHeader(http.StatusOK)
    139 		fmt.Fprintln(w, "server-front integration OK")
    140 	}))
    141 	defer backend.Close()
    142 
    143 	backendCAPool := backend.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs
    144 	certFile, keyFile, proxyCertPool := writeTempCertKey(t)
    145 
    146 	fp, err := fingerprint.ByName("chrome-120")
    147 	if err != nil {
    148 		t.Fatalf("fingerprint.ByName: %v", err)
    149 	}
    150 
    151 	p := &Proxy{
    152 		BackendAddr: backend.Listener.Addr().String(),
    153 		CAPool:      backendCAPool,
    154 		Fingerprint: fp,
    155 	}
    156 	handler, err := ServerFrontHandler(certFile, keyFile)
    157 	if err != nil {
    158 		t.Fatalf("ServerFrontHandler: %v", err)
    159 	}
    160 	proxyAddr := serveHandler(t, p, handler)
    161 
    162 	tlsConn, err := tls.DialWithDialer(
    163 		&net.Dialer{Timeout: testTimeout},
    164 		"tcp", proxyAddr,
    165 		&tls.Config{RootCAs: proxyCertPool, ServerName: "localhost"},
    166 	)
    167 	if err != nil {
    168 		t.Fatalf("tls.Dial to proxy: %v", err)
    169 	}
    170 	defer tlsConn.Close()
    171 	tlsConn.SetDeadline(time.Now().Add(testTimeout))
    172 
    173 	fmt.Fprintf(tlsConn, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
    174 
    175 	reader := bufio.NewReader(tlsConn)
    176 	resp, err := http.ReadResponse(reader, nil)
    177 	if err != nil {
    178 		t.Fatalf("reading response: %v", err)
    179 	}
    180 	defer resp.Body.Close()
    181 
    182 	if resp.StatusCode != http.StatusOK {
    183 		t.Errorf("status: %d; want 200", resp.StatusCode)
    184 	}
    185 	body, _ := io.ReadAll(resp.Body)
    186 	t.Logf("response body: %s", body)
    187 }