package sniproxy import ( "bufio" "crypto/tls" "errors" "io" "net" "testing" "time" "go.uber.org/zap" ) // startEchoBackend creates a TCP server that echoes the first 1024 bytes // it reads, then closes. Returns the listener and a chan that receives // the bytes the server saw. func startEchoBackend(t *testing.T) (net.Listener, <-chan []byte) { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } got := make(chan []byte, 4) go func() { for { conn, err := ln.Accept() if err != nil { return } go func(c net.Conn) { defer c.Close() _ = c.SetReadDeadline(time.Now().Add(2 * time.Second)) buf := make([]byte, 1024) n, _ := c.Read(buf) got <- append([]byte(nil), buf[:n]...) }(conn) } }() return ln, got } func TestServer_routes_TLS_to_correct_backend(t *testing.T) { turnLn, turnGot := startEchoBackend(t) defer turnLn.Close() caddyLn, caddyGot := startEchoBackend(t) defer caddyLn.Close() router := NewRouter(Backend{Network: "tcp", Addr: caddyLn.Addr().String()}) router.Replace([]Route{ {Match: "turn.example.com", Backend: Backend{Network: "tcp", Addr: turnLn.Addr().String()}}, }, router.Fallback()) srv := NewServer(router, Config{}, zap.NewNop()) defer srv.Close() frontLn, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer frontLn.Close() go func() { _ = srv.Serve(frontLn) }() // Client A: SNI=turn.example.com -> goes to turnLn dialAndStartTLS(t, frontLn.Addr().String(), "turn.example.com") // Client B: SNI=other.example.com -> falls through to caddyLn dialAndStartTLS(t, frontLn.Addr().String(), "other.example.com") select { case b := <-turnGot: if len(b) == 0 { t.Error("turn backend received empty bytes") } case <-time.After(3 * time.Second): t.Error("turn backend did not receive bytes") } select { case b := <-caddyGot: if len(b) == 0 { t.Error("caddy fallback received empty bytes") } case <-time.After(3 * time.Second): t.Error("caddy fallback did not receive bytes") } } // dialAndStartTLS opens a TLS handshake (which produces a ClientHello) // against the given address with the given SNI. Returns immediately — // the test only needs the proxy to forward the bytes; it doesn't // require handshake completion. func dialAndStartTLS(t *testing.T, addr, sni string) { t.Helper() conn, err := net.Dial("tcp", addr) if err != nil { t.Fatal(err) } go func() { defer conn.Close() _ = conn.SetDeadline(time.Now().Add(2 * time.Second)) c := tls.Client(conn, &tls.Config{ServerName: sni, InsecureSkipVerify: true}) _ = c.Handshake() // expected to fail (echo backend isn't TLS) }() } func TestServer_no_backend_drops_connection(t *testing.T) { router := NewRouter(Backend{}) // empty fallback, empty Addr -> dropped srv := NewServer(router, Config{}, zap.NewNop()) defer srv.Close() frontLn, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer frontLn.Close() go func() { _ = srv.Serve(frontLn) }() conn, err := net.Dial("tcp", frontLn.Addr().String()) if err != nil { t.Fatal(err) } defer conn.Close() c := tls.Client(conn, &tls.Config{ServerName: "x.example.com", InsecureSkipVerify: true}) // Handshake should fail because connection is closed by proxy. go func() { _ = c.Handshake() }() // Reader should see EOF quickly. _ = conn.SetDeadline(time.Now().Add(2 * time.Second)) br := bufio.NewReader(conn) _, err = br.ReadByte() if err == nil { t.Error("expected connection drop") } if !errors.Is(err, io.EOF) { // "use of closed network connection" is also fine. t.Logf("acceptable read error: %v", err) } }