mic.git


mic

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

commit b11226c70712291699e90011ef47f25bcb121c23
parent ff48adf9655379b4df6497123aed4af0bb48d8b5
Author: djnn <[email protected]>
Date: Wed, 26 Aug 2026 14:59:17 +0900

fix(proxy): accept backoff, half-close, deadlines

Diffstat:
Mproxy/https.go | 7+++++++
Mproxy/https_test.go | 40+++++++++++++++++++++++++++++++++++++++-
Mproxy/proxy.go | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mproxy/proxy_test.go | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---

4 files changed, 209 insertions(+), 10 deletions(-)

diff --git a/proxy/https.go b/proxy/https.go
@@ -6,8 +6,13 @@ import (
 	"log"
 	"net"
 	"net/http"
+	"time"
 )
 
+// Bounds the CONNECT header read only, never the proxied stream.
+// Tunable: tests lower it to keep the suite fast.
+var headerReadTimeout = 10 * time.Second
+
 // parseAuthority extracts the server name and a dial-ready "host:port" from
 // a CONNECT request's authority. If the input lacks an explicit port (rare,
 // since CONNECT requires one), 443 is assumed. IPv6 literals in brackets are
@@ -27,7 +32,9 @@ func HttpsHandler(clientConn net.Conn, p *Proxy) {
 	defer clientConn.Close()
 
 	reader := bufio.NewReader(clientConn)
+	clientConn.SetReadDeadline(time.Now().Add(headerReadTimeout))
 	req, err := http.ReadRequest(reader)
+	clientConn.SetReadDeadline(time.Time{})
 	if err != nil {
 		log.Printf("Failed to read client request: %v", err)
 		return
diff --git a/proxy/https_test.go b/proxy/https_test.go
@@ -1,6 +1,10 @@
 package proxy
 
-import "testing"
+import (
+	"net"
+	"testing"
+	"time"
+)
 
 func TestParseAuthority(t *testing.T) {
 	cases := []struct {
@@ -44,3 +48,37 @@ func TestParseAuthority(t *testing.T) {
 		})
 	}
 }
+
+func TestHttpsHandler_SilentPeerTimesOut(t *testing.T) {
+	orig := headerReadTimeout
+	headerReadTimeout = 200 * time.Millisecond
+	defer func() { headerReadTimeout = orig }()
+
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("net.Listen: %v", err)
+	}
+	defer ln.Close()
+
+	done := make(chan struct{})
+	go func() {
+		conn, err := ln.Accept()
+		if err != nil {
+			return
+		}
+		HttpsHandler(conn, &Proxy{})
+		close(done)
+	}()
+
+	client, err := net.Dial("tcp", ln.Addr().String())
+	if err != nil {
+		t.Fatalf("net.Dial: %v", err)
+	}
+	defer client.Close()
+
+	select {
+	case <-done:
+	case <-time.After(3 * time.Second):
+		t.Fatal("HttpsHandler did not return on a peer that sent nothing")
+	}
+}
diff --git a/proxy/proxy.go b/proxy/proxy.go
@@ -3,16 +3,24 @@ package proxy
 import (
 	"bufio"
 	"crypto/x509"
+	"errors"
 	"fmt"
 	"io"
 	"log"
 	"net"
 	"sync"
+	"time"
 
 	utls "github.com/bogdanfinn/utls"
 	"github.com/djnnvx/mic/fingerprint"
 )
 
+const (
+	dialTimeout         = 10 * time.Second
+	acceptRetryDelay    = 5 * time.Millisecond
+	acceptMaxRetryDelay = 1 * time.Second
+)
+
 type Proxy struct {
 	ListenAddr  string
 	BackendAddr string // server-front: backend host:port
@@ -27,7 +35,7 @@ type Handler func(conn net.Conn, p *Proxy)
 // dialTarget dials host (host:port) and returns a uTLS connection after a
 // successful handshake. Falls back to HelloRandomized when no fingerprint is set.
 func (p *Proxy) dialTarget(host string) (*utls.UConn, error) {
-	tcpConn, err := net.Dial("tcp", host)
+	tcpConn, err := net.DialTimeout("tcp", host, dialTimeout)
 	if err != nil {
 		return nil, err
 	}
@@ -66,8 +74,33 @@ type bufferedConn struct {
 
 func (b *bufferedConn) Read(p []byte) (int, error) { return b.r.Read(p) }
 
-// pipe copies bidirectionally between a and b. When either direction finishes
-// it closes that side so the other goroutine unblocks.
+func (b *bufferedConn) CloseWrite() error {
+	if hc, ok := b.Conn.(halfCloser); ok {
+		return hc.CloseWrite()
+	}
+	return errNoHalfClose
+}
+
+var errNoHalfClose = errors.New("proxy: connection does not support half-close")
+
+// halfCloser is implemented by *net.TCPConn, *tls.Conn and *utls.UConn.
+type halfCloser interface {
+	CloseWrite() error
+}
+
+// closeWrite shuts down only the write side so the peer still sees the data
+// already sent. Falls back to a full Close when half-close is unavailable.
+func closeWrite(c io.Closer) {
+	if hc, ok := c.(halfCloser); ok && hc.CloseWrite() == nil {
+		return
+	}
+	c.Close()
+}
+
+// pipe copies bidirectionally between a and b. A finished direction only
+// half-closes its write side: a client that shuts down its write end after
+// sending a request must still receive the response. Both ends are fully
+// closed once both directions are done.
 func pipe(a, b io.ReadWriteCloser) {
 	var wg sync.WaitGroup
 	wg.Add(2)
@@ -75,16 +108,18 @@ func pipe(a, b io.ReadWriteCloser) {
 	go func() {
 		defer wg.Done()
 		io.Copy(b, a)
-		b.Close()
+		closeWrite(b)
 	}()
 
 	go func() {
 		defer wg.Done()
 		io.Copy(a, b)
-		a.Close()
+		closeWrite(a)
 	}()
 
 	wg.Wait()
+	a.Close()
+	b.Close()
 }
 
 func (p *Proxy) Run() error {
@@ -98,12 +133,36 @@ func (p *Proxy) Run() error {
 	defer ln.Close()
 
 	log.Printf("[+] Starting proxy on %s", p.ListenAddr)
+	return p.Serve(ln)
+}
+
+// Serve accepts connections until the listener is closed. Transient accept
+// errors are retried with exponential backoff, mirroring net/http.Server.Serve.
+func (p *Proxy) Serve(ln net.Listener) error {
+	if p.Handler == nil {
+		return fmt.Errorf("proxy: Handler is required")
+	}
+
+	var delay time.Duration
 	for {
 		conn, err := ln.Accept()
 		if err != nil {
-			log.Printf("Failed to accept connection: %v", err)
+			if errors.Is(err, net.ErrClosed) {
+				return err
+			}
+			if delay == 0 {
+				delay = acceptRetryDelay
+			} else {
+				delay *= 2
+			}
+			if delay > acceptMaxRetryDelay {
+				delay = acceptMaxRetryDelay
+			}
+			log.Printf("Failed to accept connection: %v; retrying in %v", err, delay)
+			time.Sleep(delay)
 			continue
 		}
+		delay = 0
 		go p.Handler(conn, p)
 	}
 }
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
@@ -7,12 +7,15 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"errors"
 	"io"
 	"math/big"
 	"net"
+	"strings"
 	"testing"
 	"time"
 
+	utls "github.com/bogdanfinn/utls"
 	"github.com/djnnvx/mic/fingerprint"
 )
 
@@ -123,8 +126,100 @@ func TestDialTarget_FallbackToRandomized(t *testing.T) {
 
 	conn, err := p.dialTarget(ln.Addr().String())
 	if err != nil {
-		// HelloRandomized may select post-quantum curves unsupported by stdlib TLS.
-		t.Skipf("dialTarget with randomized preset: %v", err)
+		// HelloRandomized sometimes picks a curve the local TLS stack rejects.
+		// That is the only tolerated failure, anything else is a real bug.
+		if !strings.Contains(err.Error(), "CurvePreferences includes unsupported curve") {
+			t.Fatalf("dialTarget with randomized preset: %v", err)
+		}
+		return
+	}
+	defer conn.Close()
+
+	if got := conn.ClientHelloID.Str(); got != utls.HelloRandomized.Str() {
+		t.Fatalf("ClientHelloID: got %q, want %q", got, utls.HelloRandomized.Str())
+	}
+}
+
+func TestServe_ReturnsWhenListenerClosed(t *testing.T) {
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("net.Listen: %v", err)
+	}
+
+	p := &Proxy{Handler: func(conn net.Conn, _ *Proxy) { conn.Close() }}
+
+	done := make(chan error, 1)
+	go func() { done <- p.Serve(ln) }()
+
+	time.Sleep(50 * time.Millisecond)
+	ln.Close()
+
+	select {
+	case err := <-done:
+		if !errors.Is(err, net.ErrClosed) {
+			t.Fatalf("Serve returned %v, want net.ErrClosed", err)
+		}
+	case <-time.After(3 * time.Second):
+		t.Fatal("Serve did not return after the listener was closed")
+	}
+}
+
+func TestPipe_HalfCloseDeliversResponse(t *testing.T) {
+	backend, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("net.Listen backend: %v", err)
+	}
+	defer backend.Close()
+
+	go func() {
+		conn, err := backend.Accept()
+		if err != nil {
+			return
+		}
+		defer conn.Close()
+		io.Copy(io.Discard, conn)
+		time.Sleep(100 * time.Millisecond)
+		conn.Write([]byte("PONG"))
+	}()
+
+	front, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("net.Listen front: %v", err)
+	}
+	defer front.Close()
+
+	go func() {
+		conn, err := front.Accept()
+		if err != nil {
+			return
+		}
+		upstream, err := net.Dial("tcp", backend.Addr().String())
+		if err != nil {
+			conn.Close()
+			return
+		}
+		pipe(conn, upstream)
+	}()
+
+	client, err := net.Dial("tcp", front.Addr().String())
+	if err != nil {
+		t.Fatalf("net.Dial front: %v", err)
+	}
+	defer client.Close()
+
+	if _, err := client.Write([]byte("PING")); err != nil {
+		t.Fatalf("writing request: %v", err)
+	}
+	if err := client.(*net.TCPConn).CloseWrite(); err != nil {
+		t.Fatalf("CloseWrite: %v", err)
+	}
+
+	client.SetReadDeadline(time.Now().Add(5 * time.Second))
+	got, err := io.ReadAll(client)
+	if err != nil {
+		t.Fatalf("reading response: %v", err)
+	}
+	if string(got) != "PONG" {
+		t.Fatalf("response: got %q, want %q", got, "PONG")
 	}
-	conn.Close()
 }