Repository navigation
fix(websocket): report the real dial failure and close the socket (#1554) - #1556
Conversation
…bp2p#1554) _create_direct_connection handed the Swarm's background nursery to trio_websocket.connect_websocket_url, which starts its reader task there and then waits on an Event that is only ever set on success. Anything failing after the TCP connect — a certificate the client refuses, a server that accepts and never answers the upgrade — never reached the dialing task. The dial waited out handshake_timeout, raised "WebSocket handshake timeout", and the failed connection kept the socket open for the lifetime of the Swarm. Open the stream and, for wss, complete the TLS handshake in the dialing task, then hand the established stream to wrap_client_stream so the background nursery still runs the connection's background tasks. Close the stream on every failure path, shielded, because the cancellation from fail_after would otherwise cancel the close itself and leak the descriptor precisely when the handshake timed out. trio surfaces a refused certificate as BrokenResourceError with the ssl error as its cause, so unwrap the chain and report the cause: "certificate verify failed" is the actionable part. OpenConnectionError raised with a cause of its own is no longer re-wrapped, and the generic handler now chains, since str(e) is empty for some ssl and trio errors. A wss dial against a self-signed certificate now fails in milliseconds naming the certificate instead of after the full timeout naming nothing. Adds the first end-to-end wss test: every existing integration test uses plain /ws, so the TLS dial path had no coverage at all.
|
@aojea — this is the bug behind a symptom you already produced: This one is independent of #1550/#1555 (different function, no overlap), so it can go in on its own. It also shrinks #1551: the stream is now built in the dialing task, which is exactly what honouring cc @acul71 for review. Worth noting for reviewers: this adds the first end-to-end Side effect worth having: the websocket suite runs about 3× faster (~16s → ~5s), because tests that expect a dial to fail no longer sit out the full |
|
Ready for review — CI green (38/38), current with @acul71 @sumanjeet0012 for review. Short version of the change: a failed WebSocket dial used to report It also adds the first end-to-end |
Keep the new wss coverage discoverable when the file is run as a script and match the conventional layout of the rest of the suite. Co-authored-by: Cursor <cursoragent@cursor.com>
acul71
left a comment
There was a problem hiding this comment.
Approval
Solid fix for #1554. Moving TCP/TLS handshake into the dialing task and using wrap_client_stream for background work is the right ownership model: refused certificates and unanswered upgrades now surface promptly with their real cause, and failed dials close the socket (shielded against fail_after cancellation).
Strengths
- Correct root-cause fix matching the issue write-up
- Shielded
aclose_forcefullyon failure paths - Strong regressions (
test_failed_dial_closes_the_tcp_connection,test_wss_dial_reports_the_certificate_error) plus the first end-to-end verifyingwssecho test - Valid
newsfragments/1554.bugfix.rst
Nits addressed before merge
- PR body now uses
Fixes #1554 - WSS helpers/tests moved above
if __name__ == "__main__"
No blockers. Approving.
After rebasing onto main (libp2p#1556), also document new_host trust/opt-out paths and note that dns_* timeouts apply only to /dnsaddr. Co-authored-by: Cursor <cursoragent@cursor.com>
After rebasing onto main (libp2p#1556), also document new_host trust/opt-out paths and note that dns_* timeouts apply only to /dnsaddr. Co-authored-by: Cursor <cursoragent@cursor.com>
…1555) * fix(websocket): verify the server certificate on wss dials (#1550) A wss dial with no explicit TLS client configuration built its context with verify_mode = CERT_NONE and check_hostname = False, so new_host( enable_websocket=True) accepted any certificate from any server. go-libp2p dials with a zero tls.Config and js-libp2p uses the platform TLS stack; both verify against the system roots and check the hostname. Default to an unmodified ssl.create_default_context(). The previous behaviour stays reachable through WebsocketConfig(insecure_skip_verify=True), which logs a warning, and an explicit tls_client_config still wins over both. The flag name matches the one WebSocketTLSConfig already uses. The insecure default is why a name-based dial appeared to work while the transport resolved names to an IP first: hostname verification could not have succeeded against a certificate issued for the name. Stacked on #1549, which dials /dns, /dns4 and /dns6 by name; verification on its own would otherwise break those dials. libp2p's own handshake authenticates the peer inside the WebSocket, so this was never a stream integrity or confidentiality hole; what an unverified outer TLS allowed was terminating the connection on the path unnoticed. * fix(websocket): preserve insecure_skip_verify in combine_configs After rebasing onto main (#1556), also document new_host trust/opt-out paths and note that dns_* timeouts apply only to /dnsaddr. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: acul71 <34693171+acul71@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
What was wrong?
Fixes #1554
_create_direct_connectionhanded the Swarm's background nursery totrio_websocket.connect_websocket_url, which starts its reader task there and thendoes
await connection._open_handshake.wait()— an Event that is only ever set onsuccess. So anything failing after the TCP connect never reached the dialing task:
handshake_timeoutand raisedWebSocket handshake timeout, whatever the real cause was;the lifetime of the Swarm.
Measured before this PR, with
handshake_timeout = 3.0:wssWebSocket handshake timeout— never mentions the certificateWebSocket handshake timeout, socket left openHow was it fixed?
Open the stream and, for
wss, complete the TLS handshake in the dialing task,then hand the established stream to
wrap_client_streamso the background nurserystill runs the connection's background tasks — which is what it is actually needed
for.
cancellation from
fail_afterwould otherwise cancel the close itself and leakthe descriptor precisely when the handshake timed out.
BrokenResourceErrorwith thesslerroras its cause, so the cause is unwrapped and reported: "certificate verify failed"
is the actionable part,
BrokenResourceError()is not.OpenConnectionErrorthat already describes its cause is no longer re-wrapped,and the generic handler chains with
from e(str(e)is empty for severalssland trio errors).
A
wssdial against a self-signed certificate now fails in milliseconds naming thecertificate, instead of after the full timeout naming nothing.
Tests
Two regression tests, both confirmed to fail on
mainand pass here:test_failed_dial_closes_the_tcp_connection— the server reads again after theupgrade request and checks that the client hung up. On main:
AssertionError: the server never saw the client disconnect: the failed dial left its TCP socket open.test_wss_dial_reports_the_certificate_error— asserts the message names thecertificate, that
__cause__isssl.SSLCertVerificationError, and that itraises in well under the 10s timeout.
Plus
test_wss_echo_round_trip_with_a_verified_certificate, the firstend-to-end
wsstest in the suite: every existing integration test uses plain/ws, so the TLS dial path had no coverage, and this PR rewires exactly that path.It uses a certificate with an IP subjectAltName so the client verifies it with
hostname checking on, and it passes on
maintoo — it is coverage, not aregression test.
The websocket suite also got faster (about 16s to 5s), because failing dials no
longer sit out their timeouts.
Note
make prreports 4 failures intests/core/kad_dht/andtests/examples/test_dht_chat.pyon this branch. They fail on a clean checkout ofmaintoo and pass when rerun serially — a pre-existing race under-n auto,unrelated to this change.
This is independent of #1550 / #1555 (different function, no overlap) and does not
address #1551, which proposes
wrap_client_streamfor a different reason — thatchange becomes smaller after this one, since the stream is now built here.
To-Do
docstring explains the nursery reasoning inline
Cute Animal Picture