Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ untracked Go source for the known disallowed proxy application-module import.
directions. Only then does raw byte relay begin.
6. EOF is half-closed when possible; hard errors abort both copy directions.

Native QUIC and TLS fallback relays also own destination cleanup when their
physical connection or server is shut down. Cancellation closes the TCP target
to interrupt idle reads as well as pending writes, then joins both relay pumps
and any in-progress close callback before releasing stream admission. The
owner is the physical QUIC connection or TLS server lifetime, never the dial
timer or QUIC send-side stream context. An ordinary response FIN therefore
still permits upload, and client half-close still permits a delayed response.
This does not impose a new timeout on healthy idle destination reads.

QUIC streams are independent. Canceling a slow open or one flow does not close
sibling flows. A per-connection pacer sees aggregate QUIC statistics, so
concurrent streams share one path estimate rather than each overestimating the
Expand Down
18 changes: 18 additions & 0 deletions internal/tunnel/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ type streamRequestOptions struct {
Response protocol.Response
Relay deadlineConn
Handled bool
// RelayOwner ends the whole relay, not just dialing or one stream direction.
// Its owner must also interrupt ingress I/O when canceled. A QUIC send-side
// stream context is unsuitable: normal response FIN cancels that context.
RelayOwner context.Context
}

type streamRequestHandler func(context.Context, deadlineConn, protocol.Request) streamRequestOptions
Expand Down Expand Up @@ -235,6 +239,20 @@ func (s *serverCore) handleStream(
}
upstream = s.boundDestinationWrites(upstream)
defer upstream.Close()
if options.RelayOwner != nil {
closed := make(chan struct{})
stop := context.AfterFunc(options.RelayOwner, func() {
_ = upstream.Close()
close(closed)
})
defer func() {
// Stop does not join a callback that has already started. Keep the
// stream lease until its owned destination Close has returned.
if !stop() {
<-closed
}
}()
}
options.Response.Status = protocol.StatusOK
if err := protocol.WriteResponse(stream, options.Response); err != nil {
return
Expand Down
2 changes: 1 addition & 1 deletion internal/tunnel/quic.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func (s *QUICServer) serveConnection(conn *quic.Conn, sourceKey string) {
if request.Network == protocol.NetworkUDP {
return datagrams.handleRequest(ctx, requestStream, request, response)
}
return streamRequestOptions{Response: response, Relay: requestStream}
return streamRequestOptions{Response: response, Relay: requestStream, RelayOwner: conn.Context()}
}
s.core.handleStream(stream.Context(), wrapped, markAuthenticated, handler)
}()
Expand Down
Loading
Loading