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.
120 lines
4.3 KiB
Rust
120 lines
4.3 KiB
Rust
//! Shared TLS helpers used by both the control client (TS2021 over
|
|
//! HTTPS) and the DERP relay client.
|
|
//!
|
|
//! Both clients accept either plain TCP (`http://...`) or TLS-wrapped
|
|
//! TCP (`https://...`). For production they verify against the system's
|
|
//! webpki roots; for tests against self-signed certs they accept any
|
|
//! cert via [`TlsMode::InsecureSkipVerify`].
|
|
|
|
use std::sync::Arc;
|
|
|
|
use tokio::net::TcpStream;
|
|
use tokio_rustls::TlsConnector;
|
|
use tokio_rustls::client::TlsStream;
|
|
use tokio_rustls::rustls::ClientConfig;
|
|
use tokio_rustls::rustls::pki_types::ServerName;
|
|
|
|
use crate::error::Error;
|
|
|
|
/// TLS verification mode shared by all clients in this crate.
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub enum TlsMode {
|
|
/// Standard verification using the system's webpki roots. Use this in
|
|
/// production.
|
|
#[default]
|
|
Verify,
|
|
/// Skip certificate verification. Only use this against a known test
|
|
/// server with a self-signed cert.
|
|
InsecureSkipVerify,
|
|
}
|
|
|
|
/// Wrap a TcpStream in a TLS connection. Honors `TlsMode`.
|
|
pub async fn tls_wrap(
|
|
tcp: TcpStream,
|
|
server_name: &str,
|
|
mode: TlsMode,
|
|
) -> crate::Result<TlsStream<TcpStream>> {
|
|
let config = match mode {
|
|
TlsMode::Verify => {
|
|
let mut roots = tokio_rustls::rustls::RootCertStore::empty();
|
|
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
|
ClientConfig::builder()
|
|
.with_root_certificates(roots)
|
|
.with_no_client_auth()
|
|
}
|
|
TlsMode::InsecureSkipVerify => ClientConfig::builder()
|
|
.dangerous()
|
|
.with_custom_certificate_verifier(Arc::new(NoCertVerification))
|
|
.with_no_client_auth(),
|
|
};
|
|
|
|
let connector = TlsConnector::from(Arc::new(config));
|
|
let dns_name = ServerName::try_from(server_name.to_string())
|
|
.map_err(|e| Error::Tls(format!("invalid TLS server name {server_name}: {e}")))?;
|
|
connector
|
|
.connect(dns_name, tcp)
|
|
.await
|
|
.map_err(|e| Error::Tls(format!("TLS handshake failed: {e}")))
|
|
}
|
|
|
|
/// Cert verifier that accepts every server certificate. Used when
|
|
/// `TlsMode::InsecureSkipVerify` is set.
|
|
#[derive(Debug)]
|
|
struct NoCertVerification;
|
|
|
|
impl tokio_rustls::rustls::client::danger::ServerCertVerifier for NoCertVerification {
|
|
fn verify_server_cert(
|
|
&self,
|
|
_end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
|
|
_intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>],
|
|
_server_name: &ServerName<'_>,
|
|
_ocsp_response: &[u8],
|
|
_now: tokio_rustls::rustls::pki_types::UnixTime,
|
|
) -> std::result::Result<
|
|
tokio_rustls::rustls::client::danger::ServerCertVerified,
|
|
tokio_rustls::rustls::Error,
|
|
> {
|
|
Ok(tokio_rustls::rustls::client::danger::ServerCertVerified::assertion())
|
|
}
|
|
|
|
fn verify_tls12_signature(
|
|
&self,
|
|
_message: &[u8],
|
|
_cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
|
|
_dss: &tokio_rustls::rustls::DigitallySignedStruct,
|
|
) -> std::result::Result<
|
|
tokio_rustls::rustls::client::danger::HandshakeSignatureValid,
|
|
tokio_rustls::rustls::Error,
|
|
> {
|
|
Ok(tokio_rustls::rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
}
|
|
|
|
fn verify_tls13_signature(
|
|
&self,
|
|
_message: &[u8],
|
|
_cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
|
|
_dss: &tokio_rustls::rustls::DigitallySignedStruct,
|
|
) -> std::result::Result<
|
|
tokio_rustls::rustls::client::danger::HandshakeSignatureValid,
|
|
tokio_rustls::rustls::Error,
|
|
> {
|
|
Ok(tokio_rustls::rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
}
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
|
|
use tokio_rustls::rustls::SignatureScheme;
|
|
vec![
|
|
SignatureScheme::RSA_PKCS1_SHA256,
|
|
SignatureScheme::RSA_PKCS1_SHA384,
|
|
SignatureScheme::RSA_PKCS1_SHA512,
|
|
SignatureScheme::ECDSA_NISTP256_SHA256,
|
|
SignatureScheme::ECDSA_NISTP384_SHA384,
|
|
SignatureScheme::ECDSA_NISTP521_SHA512,
|
|
SignatureScheme::RSA_PSS_SHA256,
|
|
SignatureScheme::RSA_PSS_SHA384,
|
|
SignatureScheme::RSA_PSS_SHA512,
|
|
SignatureScheme::ED25519,
|
|
]
|
|
}
|
|
}
|