Production Headscale terminates TLS for both the control plane (via the TS2021 HTTP CONNECT upgrade endpoint) and the embedded DERP relay. Without TLS, the daemon could only talk to plain-HTTP test stacks. - New crate::tls module: shared TlsMode (Verify | InsecureSkipVerify) + tls_wrap helper. webpki roots in Verify mode; an explicit ServerCertVerifier that accepts any cert in InsecureSkipVerify (test-only). - Cargo.toml: add tokio-rustls, webpki-roots, rustls-pemfile. - noise/handshake: perform_handshake is now generic over the underlying stream and takes an explicit `host_header` argument instead of using `peer_addr`. Lets callers pass either a TcpStream or a TLS-wrapped stream. - noise/stream: NoiseStream<S> is generic over the underlying transport with `S = TcpStream` as the default. The AsyncRead+AsyncWrite impls forward to whatever S provides. - control/client: ControlClient::connect detects `https://` in coordination_url and TLS-wraps the TCP stream before the Noise handshake. fetch_server_key now also TLS-wraps when needed. Both honor the new derp_tls_insecure config flag (which is misnamed but controls all TLS verification, not just DERP). - derp/client: DerpClient::connect_with_tls accepts a TlsMode and uses the shared tls::tls_wrap helper instead of duplicating it. The client struct's inner Framed is now generic over a Box<dyn DerpTransport> so it can hold either a plain or TLS-wrapped stream. - daemon/lifecycle: derive the DERP URL scheme from coordination_url (https → https) and pass derp_tls_insecure through. - config.rs: new `derp_tls_insecure: bool` field on VpnConfig. - src/vpn_cmds.rs: pass `derp_tls_insecure: false` for production. Two bug fixes found while wiring this up: - proxy/engine: bridge_connection used to set remote_done on any smoltcp recv error, including the transient InvalidState that smoltcp returns while a TCP socket is still in SynSent. That meant the engine gave up on the connection before the WG handshake even finished. Distinguish "not ready yet" (returns Ok(0)) from "actually closed" (returns Err) inside tcp_recv, and only mark remote_done on the latter. - proxy/engine: the connection's "done" condition required local_read_done, but most clients (curl, kubectl) keep their write side open until they read EOF. The engine never closed its local TCP, so clients sat in read_to_end forever. Drop the connection as soon as the remote side has finished and we've drained its buffer to the local socket — the local TcpStream drop closes the socket and the client sees EOF.
142 KiB
142 KiB