Repository navigation
use WebSocketConfig's TLS configuration for wss:// connections (#190) - #191
Conversation
…ade request (LabOverWire#165) WebSocketTransport::connect built the upgrade request from a fixed set of headers and always offered Sec-WebSocket-Protocol: mqtt, so with_header, with_subprotocol(s) and with_user_agent never reached the server. Request construction moves to WebSocketConfig::build_handshake_request, which starts from tungstenite's into_client_request (fixing a Host header that dropped non-default ports) and adds the configured subprotocols, user agent and custom headers. Invalid, reserved or case-duplicated headers and non-token subprotocols are rejected before dialing. WebSocketConfig's Debug no longer prints header values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LabOverWire#165) Content-Length, Transfer-Encoding and Sec-WebSocket-Accept are now rejected as custom handshake headers alongside the existing reserved names. Custom headers are validated and sent in name order, so the request and the reported error do not depend on HashMap iteration. The integration test checks Host against the exact bound address, and the CHANGELOG calls out the changes that can affect existing configurations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erWire#190) With server-certificate verification on, WebSocketTransport::connect used tokio-tungstenite's own client config built from the platform roots, so a configured CA, client certificate, use_system_roots and ALPN protocols were ignored. With verification off, the client certificate and ALPN were dropped. The rustls client config construction moves from TlsTransport to a public TlsConfig::client_config, and a wss:// connection with a tls_config now passes that config to tokio-tungstenite in both cases. Without a tls_config, the platform roots are still used. The WSS module's duplicate NoVerifier is replaced by the shared NoVerification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S config over wss (LabOverWire#190) with_tls_auto parsed the URL's host and port as a socket address, so it and the with_ca_cert_* and with_client_auth_* methods that call it failed for any host name. A wss:// connection takes its server and certificate name from the URL, so a host name now gets the unspecified address with the URL's port. MqttClient's wss:// path built its WebSocketConfig from the URL alone and ignored the TLS configuration stored by set_tls_config; it now applies it as connect_tls does. New tests cover a host name URL, the URL host being verified rather than TlsConfig::hostname, and MqttClient presenting its stored client certificate, and two rejection tests now assert UnknownIssuer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fabracht
left a comment
There was a problem hiding this comment.
The root store change is the one thing I'd change. With a tls_config set and use_system_roots true, wss now uses webpki-roots instead of the platform store, so anything signed by a CA installed on the machine stops working. A TlsConfig that only sets ALPN, or MqttClient::set_tls_config(cert, key, None), connects to such a server on main and on #189, and gets UnknownIssuer here. The CLI's --cert/--key without --cafile goes through the same path.
rustls-native-certs is already compiled in through tokio-tungstenite's rustls-tls-native-roots, so using it directly doesn't add anything to the build. Please load the native roots for wss when use_system_roots is set. TCP+TLS can stay as it is for now.
Minor:
- Our own wss listener only advertises
http/1.1, so a stored config with MQTT ALPN (e.g. afterconnect_with_tls) now fails overwss://withNoApplicationProtocol. Please mention it in the CHANGELOG. - For a host-name URL,
with_tls_autoputs0.0.0.0:portinTlsConfig::addr. If someone takes that config and uses it withTlsTransport, it connects to localhost instead of failing. A doc note onaddris enough. client_configis public now and still has theexpecton the key clone.PrivateKeyDer::clone_key()gets rid of it.
These came from throwaway tests on our side, so please double check before changing anything:
- Root store:
SSL_CERT_FILEstands in for a platform-installed CA, since rustls-native-certs loads that file instead of the OS store. Run yourwss_serverfromwebsocket_tls_integration.rswithSSL_CERT_FILE=test_certs/ca.pemand connect with onlyTlsConfig::new(addr, "localhost").with_alpn_protocols(&["mqtt-test"]). That connects on main and #189 and fails withUnknownIssuerhere. With notls_configit connects on all three, which shows the variable is being picked up. - ALPN: start an
MqttBrokerwithwith_websocket_tlsand connect withwith_verify_server_cert(false)pluswith_alpn_protocols(&["mqtt"]). Main and #189 connect, this branch fails withNoApplicationProtocol. addr:WebSocketConfig::new("wss://broker.does-not-exist.invalid:8883")?.with_tls_auto()?, thentake_tls_config()hasaddrset to0.0.0.0:8883.
…headers (LabOverWire#165) Review follow-up. The configured subprotocols are now followed by "mqtt" when the list does not include it, as MQTT-6.0.0-3 requires: the broker in this crate only selects a subprotocol the client offers, so a config preferring mqttv5.0 failed with "Server sent no subprotocol". A subprotocol listed twice is rejected (RFC 6455 §4.1), and TE, Trailer, Keep-Alive, Proxy-Connection and Expect join the reserved headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
LabOverWire#190) Review follow-up. With a tls_config and use_system_roots, wss:// now loads the platform's root certificates through rustls-native-certs, as tokio-tungstenite already did without a tls_config, instead of the bundled webpki-roots, so a CA installed on the machine keeps working for an ALPN-only config or MqttClient::set_tls_config(cert, key, None). TlsTransport keeps webpki-roots; roots are only loaded when the server certificate is verified. MqttClient's wss:// path no longer offers the stored config's ALPN protocols, which select MQTT directly over TLS; a WebSocketConfig's own ALPN is still offered, and the CHANGELOG notes that a server advertising only http/1.1 rejects an MQTT one. TlsConfig::addr and with_tls_auto document the 0.0.0.0 placeholder, and the private key is copied with clone_key() rather than an expect. The wss test server moves to a shared module for the new SSL_CERT_FILE test binary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. I reproduced each point first: the
Fork CI: bryan-ashby#2. |
fabracht
left a comment
There was a problem hiding this comment.
Thanks, Bryan. Keep the ALPN change, dropping the stored ALPN on wss is the right call. I tried it against our own wss listener with connect_with_tls storing mqtt as the ALPN: it connects on this branch and fails with NoApplicationProtocol if I put the stored ALPN back.
The platform roots work too. With SSL_CERT_FILE pointing at the test CA, your native roots tests pass, and switching wss back to SystemRoots::Bundled makes the two that rely on the platform CA fail while the with_system_roots(false) one still passes.
#189 is in, so I merged main into your branch to sort out the conflicts it left in websocket.rs and the CHANGELOG. Nothing changed in your code, and your entries stay under [Unreleased]. Nothing left for you to do here, merging now.
Fixes #190. Builds on #189; until that merges, this diff includes #189's commits (merged in, not rebased). The changes here are this PR's three non-merge commits.
wss://connections ignoredWebSocketConfig::tls_config. With verification on,connectused tokio-tungstenite's own rustls config built from the platform roots; with verification off, it built a bare config with no client certificate or ALPN. This PR uses the configured TLS settings in both cases, keeping the platform roots foruse_system_roots.Changes
TlsConfig::client_config()(new, public) builds the rustlsClientConfigaTlsConfigdescribes, with the bundledwebpki-rootsforuse_system_roots. It is the body ofTlsTransport::build_tls_config, moved;TlsTransportnow calls it. The private key is copied withclone_key()(here and inTlsConfig'sClone), so there's noexpect, and roots are only loaded when the server certificate is verified.client_config_with(SystemRoots::Native), which takesuse_system_rootsfrom the platform store viarustls-native-certs, loaded the way tokio-tungstenite loads it without atls_config: load errors are logged, and only finding no certificate at all, with no custom roots, is an error.rustls-native-certswas already in the build through tokio-tungstenite'srustls-tls-native-roots; it's now a direct optional dependency oftransport-websocket. TCP+TLS is unchanged.WebSocketTransport::connectnow has one path. Forwss://with atls_config, it passes aConnector::Rustlsbuilt from it toconnect_async_tls_with_config. Without atls_config, it passes no connector, so tokio-tungstenite keeps verifying against the platform's native roots exactly as before.NoVerifieris removed. Verification-off goes throughcrate::crypto::NoVerification, likeTlsTransport.with_tls_autoaccepts a host name. It parsed"{host}:{port}"as aSocketAddr, so it failed for any non-IP URL, and so did thewith_ca_cert_*andwith_client_auth_*methods that call it. Since WSS no longer usesTlsConfig::addr, a host name now gets the unspecified address0.0.0.0with the URL's port, the same placeholderMqttClient::set_tls_configalready uses.TlsConfig::addrandwith_tls_autodocument that such a config needs a real address before use withTlsTransport, which would otherwise dial the local host.MqttClientuses its stored TLS config forwss://.connect_websocket_secureignored the config fromset_tls_config, and withset_insecure_tls(true)it replaced it with a bare verification-off config. It now applies the stored config asconnect_tlsdoes: same address and hostname override, verification frominsecure_tls. Its ALPN protocols are left out: they select MQTT directly over TLS (e.g.x-amzn-mqtt-caafterconnect_with_tls), and a WebSocket server negotiates HTTP; this crate's ownwss://listener advertises onlyhttp/1.1and would reject them withNoApplicationProtocol.with_tls_configdocuments which fields WSS uses:use_system_roots(platform roots), verification, client certificate, ALPN. An explicitly configured MQTT ALPN is offered as given, so the docs and CHANGELOG note theNoApplicationProtocola server advertising onlyhttp/1.1returns;addr,hostnameandconnect_timeout, which come from the URL andwith_timeout.Tests
tests/websocket_tls_integration.rsruns a loopback WSS server (tokio-rustls plusaccept_hdr_async) with the certificates fromscripts/generate_test_certs.sh, and checks what the server observed:with_ca_cert_from_fileis trustedUnknownIssuerwith_client_auth_from_filesis presented to a server requiring oneUnknownIssuerCertificateRequiredmqtt-test)UnknownIssuermqtt-testtls_config, a private-CA server is not trustedwith_ca_cert_from_fileon awss://localhost:…URL connectsInvalid host/port combinationTlsConfig::hostname(set towrong.example)UnknownIssuerMqttClientwithset_tls_configpresents its client certificate overwss://MqttClientdoesn't offer the stored config's ALPN (mqtt, fromconnect_with_tls) to a server advertising onlyhttp/1.1tests/websocket_tls_native_roots.rsis its own binary because it setsSSL_CERT_FILE, which rustls-native-certs loads instead of the platform store, to stand in for a platform-installed CA:webpki-rootsfor WSSTlsConfig(no CA) trusts the platform CAUnknownIssuerMqttClient::set_tls_config(cert, key, None)to a server requiring a client certificatewith_system_roots(false)doesn't trust the platform CAUnknownIssuerUnknownIssuerThe wss test server moved to
tests/wss_test_server/mod.rsso both binaries share it.The rejection tests are guards: they pass before and after and assert
UnknownIssuerexactly. Each other test fails as shown without the fix.Locally, on the pinned 1.98.0 toolchain:
cargo doc -D warningspasses;wss_alpn_integrationand thetransport::unit tests;tokio,transport-websocketorbroker;main: the firstTestBrokerin some binaries (another service holds 127.0.0.1:20000) and the external-broker tests (a local broker holds 127.0.0.1:1883).Upstream's Rust CI and Conformance Suite run on the fork: bryan-ashby#2.
🤖 Generated with Claude Code