//go:build darwin || linux package daemon import ( "bytes" "fmt" "io" "net" "sync" "testing" "time" ) // startEchoServer starts a TCP server that echoes back everything it receives. // It returns the listener and its address. func startEchoServer(t *testing.T) net.Listener { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to start echo server: %v", err) } go func() { for { conn, err := ln.Accept() if err != nil { return } go func(c net.Conn) { defer c.Close() //nolint:errcheck // test cleanup _, _ = io.Copy(c, c) }(conn) } }() return ln } // startBlackHoleServer accepts connections but never reads/writes, then closes. func startBlackHoleServer(t *testing.T) net.Listener { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to start black hole server: %v", err) } go func() { for { conn, err := ln.Accept() if err != nil { return } _ = conn.Close() } }() return ln } func TestRelayBidirectionalForward(t *testing.T) { // Start a mock upstream (echo server) acting as the "SOCKS5 proxy". echo := startEchoServer(t) defer echo.Close() //nolint:errcheck // test cleanup echoAddr := echo.Addr().String() proxyURL := fmt.Sprintf("socks5://%s", echoAddr) relay, err := NewRelay(proxyURL, true) if err != nil { t.Fatalf("NewRelay failed: %v", err) } if err := relay.Start(); err != nil { t.Fatalf("Start failed: %v", err) } defer relay.Stop() // Connect through the relay. conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port())) if err != nil { t.Fatalf("failed to connect to relay: %v", err) } defer conn.Close() //nolint:errcheck // test cleanup // Send data and verify it echoes back. msg := []byte("hello, relay!") if _, err := conn.Write(msg); err != nil { t.Fatalf("write failed: %v", err) } buf := make([]byte, len(msg)) _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) if _, err := io.ReadFull(conn, buf); err != nil { t.Fatalf("read failed: %v", err) } if !bytes.Equal(buf, msg) { t.Fatalf("expected %q, got %q", msg, buf) } } func TestRelayMultipleMessages(t *testing.T) { echo := startEchoServer(t) defer echo.Close() //nolint:errcheck // test cleanup proxyURL := fmt.Sprintf("socks5://%s", echo.Addr().String()) relay, err := NewRelay(proxyURL, false) if err != nil { t.Fatalf("NewRelay failed: %v", err) } if err := relay.Start(); err != nil { t.Fatalf("Start failed: %v", err) } defer relay.Stop() conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port())) if err != nil { t.Fatalf("failed to connect to relay: %v", err) } defer conn.Close() //nolint:errcheck // test cleanup // Send multiple messages and verify each echoes back. for i := 0; i < 10; i++ { msg := []byte(fmt.Sprintf("message-%d", i)) if _, err := conn.Write(msg); err != nil { t.Fatalf("write %d failed: %v", i, err) } buf := make([]byte, len(msg)) _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) if _, err := io.ReadFull(conn, buf); err != nil { t.Fatalf("read %d failed: %v", i, err) } if !bytes.Equal(buf, msg) { t.Fatalf("message %d: expected %q, got %q", i, msg, buf) } } } func TestRelayUpstreamConnectionFailure(t *testing.T) { // Find a port that is not listening. ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } deadPort := ln.Addr().(*net.TCPAddr).Port _ = ln.Close() // close immediately so nothing is listening proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", deadPort) relay, err := NewRelay(proxyURL, true) if err != nil { t.Fatalf("NewRelay failed: %v", err) } if err := relay.Start(); err != nil { t.Fatalf("Start failed: %v", err) } defer relay.Stop() // Connect to the relay. The relay should accept the connection but then // fail to reach the upstream, causing the local side to be closed. conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port())) if err != nil { t.Fatalf("failed to connect to relay: %v", err) } defer conn.Close() //nolint:errcheck // test cleanup // The relay should close the connection after failing upstream dial. _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) buf := make([]byte, 1) _, readErr := conn.Read(buf) if readErr == nil { t.Fatal("expected read error (connection should be closed), got nil") } } func TestRelayConcurrentConnections(t *testing.T) { echo := startEchoServer(t) defer echo.Close() //nolint:errcheck // test cleanup proxyURL := fmt.Sprintf("socks5://%s", echo.Addr().String()) relay, err := NewRelay(proxyURL, false) if err != nil { t.Fatalf("NewRelay failed: %v", err) } if err := relay.Start(); err != nil { t.Fatalf("Start failed: %v", err) } defer relay.Stop() const numConns = 50 var wg sync.WaitGroup errors := make(chan error, numConns) for i := 0; i < numConns; i++ { wg.Add(1) go func(idx int) { defer wg.Done() conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port())) if err != nil { errors <- fmt.Errorf("conn %d: dial failed: %w", idx, err) return } defer conn.Close() //nolint:errcheck // test cleanup msg := []byte(fmt.Sprintf("concurrent-%d", idx)) if _, err := conn.Write(msg); err != nil { errors <- fmt.Errorf("conn %d: write failed: %w", idx, err) return } buf := make([]byte, len(msg)) _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second)) if _, err := io.ReadFull(conn, buf); err != nil { errors <- fmt.Errorf("conn %d: read failed: %w", idx, err) return } if !bytes.Equal(buf, msg) { errors <- fmt.Errorf("conn %d: expected %q, got %q", idx, msg, buf) } }(i) } wg.Wait() close(errors) for err := range errors { t.Error(err) } } func TestRelayMaxConnsLimit(t *testing.T) { // Use a black hole server so connections stay open. bh := startBlackHoleServer(t) defer bh.Close() //nolint:errcheck // test cleanup proxyURL := fmt.Sprintf("socks5://%s", bh.Addr().String()) relay, err := NewRelay(proxyURL, true) if err != nil { t.Fatalf("NewRelay failed: %v", err) } // Set a very low limit for testing. relay.maxConns = 2 if err := relay.Start(); err != nil { t.Fatalf("Start failed: %v", err) } defer relay.Stop() // The black hole server closes connections immediately, so the relay's // handleConn will finish quickly. Instead, use an echo server that holds // connections open to truly test the limit. // We just verify the relay starts and stops cleanly with the low limit. conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port())) if err != nil { t.Fatalf("failed to connect: %v", err) } _ = conn.Close() } func TestRelayTCPHalfClose(t *testing.T) { // Start a server that reads everything, then sends a response, then closes. ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to listen: %v", err) } defer ln.Close() //nolint:errcheck // test cleanup response := []byte("server-response-after-client-close") go func() { conn, err := ln.Accept() if err != nil { return } defer conn.Close() //nolint:errcheck // test cleanup // Read all data from client until EOF (client did CloseWrite). data, err := io.ReadAll(conn) if err != nil { return } _ = data // Now send a response back (the write direction is still open). _, _ = conn.Write(response) // Signal we're done writing. if tc, ok := conn.(*net.TCPConn); ok { _ = tc.CloseWrite() } }() proxyURL := fmt.Sprintf("socks5://%s", ln.Addr().String()) relay, err := NewRelay(proxyURL, true) if err != nil { t.Fatalf("NewRelay failed: %v", err) } if err := relay.Start(); err != nil { t.Fatalf("Start failed: %v", err) } defer relay.Stop() conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port())) if err != nil { t.Fatalf("failed to connect to relay: %v", err) } defer conn.Close() //nolint:errcheck // test cleanup // Send data to the server. clientMsg := []byte("client-data") if _, err := conn.Write(clientMsg); err != nil { t.Fatalf("write failed: %v", err) } // Half-close our write side; the server should now receive EOF and send its response. tcpConn, ok := conn.(*net.TCPConn) if !ok { t.Fatal("expected *net.TCPConn") } if err := tcpConn.CloseWrite(); err != nil { t.Fatalf("CloseWrite failed: %v", err) } // Read the server's response through the relay. _ = conn.SetReadDeadline(time.Now().Add(3 * time.Second)) got, err := io.ReadAll(conn) if err != nil { t.Fatalf("ReadAll failed: %v", err) } if !bytes.Equal(got, response) { t.Fatalf("expected %q, got %q", response, got) } } func TestRelayPort(t *testing.T) { echo := startEchoServer(t) defer echo.Close() //nolint:errcheck // test cleanup proxyURL := fmt.Sprintf("socks5://%s", echo.Addr().String()) relay, err := NewRelay(proxyURL, false) if err != nil { t.Fatalf("NewRelay failed: %v", err) } defer relay.Stop() port := relay.Port() if port <= 0 || port > 65535 { t.Fatalf("invalid port: %d", port) } } func TestNewRelayInvalidURL(t *testing.T) { tests := []struct { name string proxyURL string }{ {"missing port", "socks5://127.0.0.1"}, {"missing host", "socks5://:1080"}, {"empty", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := NewRelay(tt.proxyURL, false) if err == nil { t.Fatal("expected error, got nil") } }) } }