Repository navigation
sm: close accepted connections that do not complete CER/CEA within a … - #266
Merged
Merged
Conversation
…timeout The server role had no handshake timer. A peer that connected and sent nothing was kept open indefinitely, holding a goroutine and a file descriptor, bounded only by the process's descriptor limit. A peer allowlist cannot help, because it can only run once a CER supplies an Origin-Host. RFC 6733 section 5.6.1 says the connection should be closed if no CER arrives within an implementation-defined time. Server.ReadTimeout is not a substitute: it is re-armed before every read, so it would also drop established peers that go quiet between watchdogs. sm.Settings gains HandshakeTimeout. Zero means DefaultHandshakeTimeout (30 s); a negative value disables the limit. It applies only to connections accepted by diam.Server: a StateMachine used as a client waits for its CEA on its own retransmit schedule, which can legitimately exceed 30 s. The state machine does not see connections being accepted, so diam gains an optional AcceptHandler interface in the style of ErrorReporter: HandleAccept(c Conn) (onClose func()). conn.serve calls it for connections accepted by Server.Serve, never for dialed ones, and before the TLS handshake, so a peer that never sends a ClientHello is bounded too. The returned function runs when the connection closes; the state machine uses it to stop the timer. Without that, each closed connection stayed reachable until its timer fired: 43.5 MiB retained for 5,000 connect-and-close connections, against 0.1 MiB with the timer stopped. This is a behaviour change: existing users get the 30 s limit without configuring anything. A peer sends its CER right after connecting, and the existing test suite passes with the default in effect. Tests: a silent TCP connection is closed; a silent TLS connection with no ClientHello is closed; a handshaked connection still answers a DWR after the timeout; a negative value disables the limit; zero resolves to the default; calling onClose stops the timer; HandleAccept fires once for an accepted connection, never for a dialed one, and onClose runs when the peer closes.
fiorix
approved these changes
Sep 27, 2026
fiorix
left a comment
Owner
There was a problem hiding this comment.
Reviewed at e33670c. Merging, with the default-on question settled as proposed.
The mechanics check out: the timer starts before the TLS handshake so a silent ClientHello is bounded too, the close is gated on smpeer.FromContext (the same signal handshakeOK uses), the returned stop function keeps closed connections from retaining timers, a negative value disables the limit, and dialed connections are exempt. All four behaviors have tests, and go test -race ./diam/... passes on this head.
On the two design points raised in the description:
- Default stays on (30 s). RFC 6733 section 5.6.1 sanctions closing, it only affects peers that never complete a CER, and the descriptor-exhaustion protection should not require opt-in. It will be called out in the next release notes as a behavior change; deployments with peers that legitimately connect long before sending a CER can set
HandshakeTimeout: -1. - Wrapped handlers. The type assertion missing wrappers is the same limitation
ErrorReporteralready has, and it's documented on the interface. Acceptable as-is.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
sm: close accepted connections that do not complete CER/CEA within a timeout
Branch:
kulcsartibor:feat/handshake-timeout→fiorix:mainThis PR adds public API and changes default behaviour, so it is kept separate, as requested.
Problem
The server role had no handshake timer. A peer that connected and sent nothing was kept open indefinitely, holding a goroutine and a file descriptor, bounded only by the process's descriptor limit.
A peer allowlist cannot help, because it can only run once a CER supplies an Origin-Host. RFC 6733 §5.6.1 says the connection should be closed if no CER arrives within an implementation-defined time.
Server.ReadTimeoutis not a substitute: it is re-armed before every read, so it would also drop established peers that go quiet between watchdogs.Change
sm.Settings.HandshakeTimeout: zero meansDefaultHandshakeTimeout(30 s), and a negative value disables the limit. It applies only to connections accepted bydiam.Server. AStateMachineused as a client waits for its CEA on its own retransmit schedule, which can legitimately exceed 30 s.diam.AcceptHandler: a new optional interface, in the style ofErrorReporter:HandleAccept(c Conn) (onClose func()).conn.servecalls it for connections accepted byServer.Serve, never for dialed ones.Behaviour change
Existing users get the 30 s limit without configuring anything. A well-behaved peer sends its CER right after connecting, and the existing test suite passes with the default in effect.
Open questions for review
Default on or opt-in. Default on protects existing deployments without code changes. Opt-in avoids any behaviour change for users who don't set it. Happy to switch if you prefer opt-in.
Wrapped handlers.
servelooks forAcceptHandleronly onServer.Handler. If the state machine is wrapped in middleware, such asexamples/middleware'sNewTracer(sm), the interface is hidden and the timeout is silently disabled.ErrorReporterandHandshakeNotifierhave the same limitation today. Options:Serverfield instead of an interface.Which would you prefer?
Transport setup. The hook runs before the TLS handshake, as described above. SCTP connections go through the same
conn.servepath, but I have not tested SCTP (TestServerClose/sctpdoes not run on macOS).Tests
onClosestops the timerHandleAcceptfires once for an accepted connection, never for a dialed one, andonCloseruns when the peer closes