Repository navigation
fix(lwip2transport): bound half-closed TCP relays - #633
OskarEichler wants to merge 4 commits into
Conversation
Greptile SummaryThis PR bounds half-closed lwIP TCP relays and ensures both relay connections are released when copying finishes.
Confidence Score: 5/5The PR appears safe to merge, with no concrete correctness or security failures identified. The deadline is applied to the connection read by the remaining copy direction, and deferred closure releases both relay endpoints after normal completion or timeout.
|
| Filename | Overview |
|---|---|
| network/lwip2transport/tcp.go | Adds symmetric half-close deadlines and deterministic full connection cleanup without changing the public transport contract. |
| network/lwip2transport/tcp_test.go | Verifies that a stalled half-closed relay terminates promptly and closes each endpoint once. |
Sequence Diagram
sequenceDiagram
participant Client as lwIP connection
participant Relay as TCP relay
participant Proxy as Proxy connection
Client->>Relay: FIN after request
Relay->>Proxy: CloseWrite
Relay->>Proxy: SetReadDeadline(now + 30s)
alt Proxy finishes within timeout
Proxy-->>Relay: Remaining response + FIN
Relay-->>Client: Remaining response
else Proxy remains open
Proxy--xRelay: Read deadline expires
end
Relay->>Client: Close
Relay->>Proxy: Close
Reviews (1): Last reviewed commit: "fix(lwip2transport): bound half-closed T..." | Re-trigger Greptile
fortuna
left a comment
There was a problem hiding this comment.
Thank you for the contributions. I'm sorry for the delay, it's been quite busy.
| rightConn := &testStreamConn{Conn: right} | ||
| start := time.Now() | ||
|
|
||
| relayWithHalfCloseTimeout(leftConn, rightConn, 10*time.Millisecond) |
There was a problem hiding this comment.
Please use https://go.dev/blog/synctest for time-sensitive tests.
|
Addressed all three review requests in 316fefa: the production timeout is now 60 seconds with the FreeBSD FIN_WAIT_2 rationale documented; relay takes the timeout directly, removing the wrapper; and the stalled-half-close regression uses testing/synctest with an exact virtual-time assertion rather than a wall-clock tolerance. Also merged current main. Validation: go test -race ./... passed for the root module, go tool staticcheck ./network/lwip2transport passed, and git diff --check passed. The independent x module was not changed by the fix or tested. |
fortuna
left a comment
There was a problem hiding this comment.
Looks good, but please do this one small fix
| // halfCloseTimeout limits how long a peer can leave a relay half-closed. | ||
| // Match FreeBSD's default 60-second FIN_WAIT_2 timeout for orphaned sockets: | ||
| // https://man.freebsd.org/cgi/man.cgi?query=tcp (fast_finwait2_recycle). | ||
| const halfCloseTimeout = 60 * time.Second |
There was a problem hiding this comment.
I'm afraid this is too buried here. Let's move this up the stack, into device.go (next to the MTU). Pass it down to newTCPHandler.
I want to avoid the failure mode where we change the implementation (e.g. to gvisor) and forget that this is important.
|
Addressed the configuration feedback: moved the 60-second half-close timeout next to packetMTU in device.go and pass it into newTCPHandler from both device constructors. The handler now forwards its configured timeout to the relay. Verified with go test -race ./..., targeted staticcheck, and git diff --check. |
Summary
Problem
When one copy direction reaches EOF,
copyOneWayhalf-closes the destination, butrelaywaits indefinitely for the opposite direction. If that peer never sends FIN, the application-owned proxy socket can remain inFIN_WAIT_2indefinitely.On macOS this reproduced as more than 16,000
FIN_WAIT_2sockets to one Outline server, exhausting the 16,384-port ephemeral range and causing unrelated connections to fail withEADDRNOTAVAIL.Approach
After either copy direction completes, set a read deadline on the connection used by the remaining direction. This retains half-close behavior for responses that finish within 60 seconds, uses the existing relay goroutine structure, and guarantees both connections are fully closed afterward.
Verification
go test -race ./...go tool staticcheck ./network/lwip2transport