Skip to content

Commit 21f6083

Browse files
committed
test(forwarder): port conflict detection, explicit half-close verification
1 parent 926011e commit 21f6083

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

internal/forwarder/forwarder_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,97 @@ func TestForwardMultipleConnections(t *testing.T) {
102102
}
103103
}
104104
}
105+
106+
func TestForwardPortConflict(t *testing.T) {
107+
echoAddr := startEchoServer(t)
108+
ctx, cancel := context.WithCancel(context.Background())
109+
defer cancel()
110+
111+
// Start the first forwarder on an ephemeral port.
112+
f1 := New("127.0.0.1:0", echoAddr)
113+
errCh1 := make(chan error, 1)
114+
go func() { errCh1 <- f1.Start(ctx) }()
115+
time.Sleep(50 * time.Millisecond)
116+
117+
// Sanity-check: the first forwarder should be working.
118+
boundAddr := f1.ListenAddr()
119+
conn, err := net.Dial("tcp", boundAddr)
120+
if err != nil {
121+
t.Fatalf("first forwarder unreachable: %v", err)
122+
}
123+
conn.Close()
124+
125+
// Start a second forwarder on the SAME address — this must fail.
126+
f2 := New(boundAddr, echoAddr)
127+
err = f2.Start(ctx)
128+
if err == nil {
129+
t.Fatal("expected error when binding to an already-used port, got nil")
130+
}
131+
t.Logf("second forwarder correctly failed: %v", err)
132+
}
133+
134+
func TestForwardHalfClose(t *testing.T) {
135+
// Server that reads ALL client data, then sends a response after
136+
// the client has already closed its write side.
137+
ln, err := net.Listen("tcp", "127.0.0.1:0")
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
t.Cleanup(func() { ln.Close() })
142+
143+
const serverReply = "server-says-hello"
144+
145+
go func() {
146+
for {
147+
conn, err := ln.Accept()
148+
if err != nil {
149+
return
150+
}
151+
go func() {
152+
defer conn.Close()
153+
// Read everything the client sends (blocks until client CloseWrite).
154+
data, _ := io.ReadAll(conn)
155+
// Now send back: the echoed data + a fixed suffix.
156+
conn.Write(data)
157+
conn.Write([]byte(serverReply))
158+
// Close write so the proxy's copy loop terminates.
159+
if tc, ok := conn.(*net.TCPConn); ok {
160+
tc.CloseWrite()
161+
}
162+
}()
163+
}
164+
}()
165+
166+
serverAddr := ln.Addr().String()
167+
168+
ctx, cancel := context.WithCancel(context.Background())
169+
defer cancel()
170+
171+
f := New("127.0.0.1:0", serverAddr)
172+
go f.Start(ctx)
173+
time.Sleep(50 * time.Millisecond)
174+
175+
conn, err := net.Dial("tcp", f.ListenAddr())
176+
if err != nil {
177+
t.Fatal(err)
178+
}
179+
defer conn.Close()
180+
181+
// Client sends data, then closes its write side.
182+
clientMsg := "half-close-test"
183+
conn.Write([]byte(clientMsg))
184+
conn.(*net.TCPConn).CloseWrite()
185+
186+
// The server only starts writing AFTER it has read everything, so
187+
// if half-close propagation is broken we would hang or get no data.
188+
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
189+
buf, err := io.ReadAll(conn)
190+
if err != nil {
191+
t.Fatalf("reading server response: %v", err)
192+
}
193+
194+
expected := clientMsg + serverReply
195+
if string(buf) != expected {
196+
t.Errorf("expected %q, got %q", expected, string(buf))
197+
}
198+
}

0 commit comments

Comments
 (0)