diff --git a/sunbeam-net/tests/config/headscale.yaml b/sunbeam-net/tests/config/headscale.yaml index 1ce59961..d1f1ca31 100644 --- a/sunbeam-net/tests/config/headscale.yaml +++ b/sunbeam-net/tests/config/headscale.yaml @@ -1,10 +1,17 @@ # Headscale configuration for integration tests. # Ephemeral SQLite, embedded DERP, no OIDC. -server_url: http://headscale:8080 -listen_addr: 0.0.0.0:8080 +server_url: https://headscale:8443 +listen_addr: 0.0.0.0:8443 metrics_listen_addr: 0.0.0.0:9090 +# Self-signed cert covering localhost, 127.0.0.1, and the docker-network +# hostname `headscale`. Generated by tests/run.sh on first run; the +# integration tests connect with derp_tls_insecure: true so they don't +# need to trust this CA. +tls_cert_path: /etc/headscale/test-cert.pem +tls_key_path: /etc/headscale/test-key.pem + # Noise protocol (auto-generates key on first start) noise: private_key_path: /var/lib/headscale/noise_private.key diff --git a/sunbeam-net/tests/config/test-cert.pem b/sunbeam-net/tests/config/test-cert.pem new file mode 100644 index 00000000..470c446a --- /dev/null +++ b/sunbeam-net/tests/config/test-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgIUY3x22aarkJ14Mbb8tXK4uoUdmpIwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDQwNzE0MDU0MFoXDTI3MDQw +NzE0MDU0MFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAzSUL/WcwxwVjRTECaNd9DBMdJAcHa26c5wtANsghvwmh +LZP3aLzBNtCdgZ1VxN+2W8j5w2HBIJ3V2aRWL6I3ZcZ7u2gJltjyJuqxtlTsDrsI +/j6jNo69c6/IO4X9pgVyHnNk55LQg7mmbw+EOmeyDKfF70UF45xHB+FX5Eb2uoRl +hCSdt2pIYw/4OYCSSx68Jb/HLtMtSvMOJua+AiB647Yypij9uD4amBx0+gAPCSYq +CIfL/pVDr1OqfdKZaP0L7ZG/99wVRjk9QQRLiiio6Ob+FhIlB4XTlsFIl2WENjrq +izMxKWEXd8T/DNo+yqoSNPFQGPur35pHGd/ZZpkOgQIDAQABo3oweDAdBgNVHQ4E +FgQUsGoAfRBgHjVVKChl2Mz9xESmvPQwHwYDVR0jBBgwFoAUsGoAfRBgHjVVKChl +2Mz9xESmvPQwDwYDVR0TAQH/BAUwAwEB/zAlBgNVHREEHjAcgglsb2NhbGhvc3SC +CWhlYWRzY2FsZYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAhMKo1oBO1ywUBjJo +xVU8fgeDmETZwakZTzwpkqqXTdnnzsjAfzHIXAgs+gnaGMqQpYlyMY0na8KBGJ56 +qsuSf9twQ2V/aWMhlSmB0buEqg2TwTIepHZoLPU0pH2Z6ilvEjaphKLHdGCjBLMZ +/xPk7mXzW4+gHVRsYrkWIUqX6gPP3Pn4OlOOxbTG8naIiM6d10/0dV8KawqoLCrb +yVUGOZ7RbW9mZ4X+Wl7zZLZEnLiQf3O8vbCVr7fygTGBl4cw8M8nsetYvJTExoed +FwYRFbtHYJEjb+R8csVJDCoN6O7A1xiGzZfn8eKJ7OsYTmtRZ3jPPUsOH+aM0yIa +yKRSuA== +-----END CERTIFICATE----- diff --git a/sunbeam-net/tests/config/test-key.pem b/sunbeam-net/tests/config/test-key.pem new file mode 100644 index 00000000..694a9e81 --- /dev/null +++ b/sunbeam-net/tests/config/test-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNJQv9ZzDHBWNF +MQJo130MEx0kBwdrbpznC0A2yCG/CaEtk/dovME20J2BnVXE37ZbyPnDYcEgndXZ +pFYvojdlxnu7aAmW2PIm6rG2VOwOuwj+PqM2jr1zr8g7hf2mBXIec2TnktCDuaZv +D4Q6Z7IMp8XvRQXjnEcH4VfkRva6hGWEJJ23akhjD/g5gJJLHrwlv8cu0y1K8w4m +5r4CIHrjtjKmKP24PhqYHHT6AA8JJioIh8v+lUOvU6p90plo/Qvtkb/33BVGOT1B +BEuKKKjo5v4WEiUHhdOWwUiXZYQ2OuqLMzEpYRd3xP8M2j7KqhI08VAY+6vfmkcZ +39lmmQ6BAgMBAAECggEADKD7uHlaSN49irxmJBMj+RLSJ+4g2E3CbfzE0rqGTU7n +87jYscNw95FnKNwJNCn7fXIFYjBJ5dqhmBjkT2FinKrX4iUY9gbb/WZUU1+t+ogs +GQ69GHY4Fn+bSYLJpydNq6855oGkwX8zzkF7x+arUNkhN8YdgzITM1p9gSmXNcsl +egzveQNmhBF9w+JMpot5bY/KXaoTTBi9R3ZLob21KfT+DTWUvi8PTrzHq4gQ9pX0 +AQuYd0wcfOwl44k123ZeLP8YA1r7sIVqorjkU0f+zpv9W5Xoe7YPbqZV2jRbF8WK +wBNDTvLaW7F9mk6HhcF4kcjfepv5F+y+Bh4oStwCCwKBgQDzmGGLGVrjy5KfdVyM +TVPkzmLTEls0DsmWieHH9+2JXExrQBZXusurPlUFs5zTFl4rD0Ug2BdPFmEtsn8c +qq/xXQL1W5c0b+UgynG0QcZBRJiQ11TTVgISIZiPL4V8LVJg8PvbzoDwNhPzZuYT +ZR1FQaisEpSf3L20DpWDB0dWLwKBgQDXl2g9Gi6Dv7/7j+IgJM7PBtPmeMOs07dr +Oi/hgrS111qMuVB8cYO588XzC+alJLEt1Wc/krSqUusjkWtSD3mDDFZBJLFQ7Rny +Yp1wN3QyntShntqAxIw3sD+RLJjZrKvX9YYV66oamEhMSadGkW2Pq6Y3fYGe6SRX +uYUhzPpqTwKBgBwfRXOXk8SkpeK+29Zevwa1RPd1MQ5Lfr5gYK6DUur+utvO5EVw +jT7RzWMBH2PHO0vhUWu/RsGcpc9uwfn/QpyszkChOE2XdW5ZsNLMnSS/1JU0JtjX +HxoUwtYU+GYjnVUPvSPdLUmOFLOO95TZoY1zTRPAeWQTSdtVq7Ea0AOFAoGAPF1K +dIFWMNGJwbghynpD5be1sTxzHXsSSlW6flwImTm5QtnIbW+jQHe/HzRf2jGR4pF0 +HVrId0BMUmMvN4TZsxXLOOY7N7uLnlB6YKdGQ74xLye5aoCd+iHBSra//YLZgthe +ONkJgfTNmX6t9ZZWpPmcysC7gHErGdz6J+Kq4wUCgYEAufqI3u268wzIfN9Zigdw +vK5A/pnN1uUyIVA9YjHHuw8a7PZIQhdPTvBIVLMOoACNZpSH/FGnBNFQEcnC5u+4 +yWaZQmk90cTVJBotxHKAuAuIeKazBYwoeXrXvID+0sbPz9rLlmLkQlWKHfFYjuEr +SOvveQdZfHIhzbFvtA/fIvE= +-----END PRIVATE KEY----- diff --git a/sunbeam-net/tests/docker-compose.yml b/sunbeam-net/tests/docker-compose.yml index dae70484..0380fa9e 100644 --- a/sunbeam-net/tests/docker-compose.yml +++ b/sunbeam-net/tests/docker-compose.yml @@ -18,11 +18,13 @@ services: image: headscale/headscale:0.23 command: serve ports: - - "8080:8080" # control plane (TS2021 Noise + HTTP API) + - "8443:8443" # control plane + embedded DERP, both over TLS - "3478:3478/udp" # STUN - "9090:9090" # metrics volumes: - ./config/headscale.yaml:/etc/headscale/config.yaml:ro + - ./config/test-cert.pem:/etc/headscale/test-cert.pem:ro + - ./config/test-key.pem:/etc/headscale/test-key.pem:ro - headscale-data:/var/lib/headscale healthcheck: test: ["CMD", "headscale", "nodes", "list"] @@ -46,11 +48,15 @@ services: TS_AUTHKEY: "${PEER_A_AUTH_KEY}" TS_STATE_DIR: /var/lib/tailscale TS_USERSPACE: "false" - TS_EXTRA_ARGS: --login-server=http://headscale:8080 + TS_EXTRA_ARGS: --login-server=https://headscale:8443 # Pin the WireGuard listen port (passed to tailscaled itself) so we # can publish it to the host — without this our test daemon (running # outside docker) can't reach peer-a's UDP endpoint. TS_TAILSCALED_EXTRA_ARGS: --port=41641 + # Trust the self-signed test cert so the tailscale Go client can + # verify HTTPS connections to headscale + the embedded DERP. Go + # honors SSL_CERT_FILE for net/http TLS verification. + SSL_CERT_FILE: /etc/headscale/test-cert.pem cap_add: - NET_ADMIN - NET_RAW @@ -60,6 +66,7 @@ services: - "41641:41641/udp" volumes: - peer-a-state:/var/lib/tailscale + - ./config/test-cert.pem:/etc/headscale/test-cert.pem:ro # Tailscale doesn't have a great healthcheck, but it registers fast healthcheck: test: ["CMD", "tailscale", "status", "--json"] @@ -78,7 +85,8 @@ services: TS_AUTHKEY: "${PEER_B_AUTH_KEY}" TS_STATE_DIR: /var/lib/tailscale TS_USERSPACE: "false" - TS_EXTRA_ARGS: --login-server=http://headscale:8080 + TS_EXTRA_ARGS: --login-server=https://headscale:8443 + SSL_CERT_FILE: /etc/headscale/test-cert.pem cap_add: - NET_ADMIN - NET_RAW @@ -86,6 +94,7 @@ services: - /dev/net/tun:/dev/net/tun volumes: - peer-b-state:/var/lib/tailscale + - ./config/test-cert.pem:/etc/headscale/test-cert.pem:ro healthcheck: test: ["CMD", "tailscale", "status", "--json"] interval: 5s diff --git a/sunbeam-net/tests/integration.rs b/sunbeam-net/tests/integration.rs index 94c64f53..1bd2d5b5 100644 --- a/sunbeam-net/tests/integration.rs +++ b/sunbeam-net/tests/integration.rs @@ -38,6 +38,7 @@ async fn test_register_and_receive_netmap() { control_socket: state_dir.path().join("test.sock"), hostname: "sunbeam-net-test".into(), server_public_key: None, + derp_tls_insecure: std::env::var("SUNBEAM_NET_TEST_DERP_INSECURE").is_ok(), }; let keys = sunbeam_net::keys::NodeKeys::load_or_generate(&config.state_dir).unwrap(); @@ -107,6 +108,7 @@ async fn test_proxy_listener_accepts() { control_socket: state_dir.path().join("proxy.sock"), hostname: "sunbeam-net-proxy-test".into(), server_public_key: None, + derp_tls_insecure: std::env::var("SUNBEAM_NET_TEST_DERP_INSECURE").is_ok(), }; let handle = sunbeam_net::VpnDaemon::start(config).await.unwrap(); @@ -132,25 +134,25 @@ async fn test_proxy_listener_accepts() { /// End-to-end: bring up the daemon, dial peer-a's echo server through the /// proxy, and assert we get bytes back across the WireGuard tunnel. /// -/// **Currently ignored** because the docker-compose test stack runs Headscale -/// over plain HTTP, but Tailscale's official client unconditionally tries to -/// connect to DERP relays over TLS: -/// -/// derp.Recv(derp-999): connect to region 999: tls: first record does -/// not look like a TLS handshake -/// -/// So peer-a can never receive WireGuard packets we forward via the relay, -/// and we have no other reachable transport from the host into the docker -/// network. Unblocking this requires either: (a) generating a self-signed -/// cert, configuring Headscale + DERP for TLS, and teaching DerpClient to -/// negotiate TLS; or (b) running the test daemon inside the same docker -/// network as peer-a so direct UDP works without relays. Tracked separately. +/// Requires the docker-compose stack with TUN-mode peers + TLS Headscale +/// (sunbeam-net/tests/run.sh handles the setup). The test enables +/// `derp_tls_insecure` because the test stack uses a self-signed cert. #[tokio::test(flavor = "multi_thread")] -#[ignore = "blocked on TLS DERP — see comment"] async fn test_e2e_tcp_through_tunnel() { use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; + // Try to install a tracing subscriber so the daemon's logs reach + // stderr when the test is run with `cargo test -- --nocapture`. Ignore + // failures (already installed by another test). + let _ = tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("sunbeam_net=trace")), + ) + .with_test_writer() + .try_init(); + let coord_url = require_env("SUNBEAM_NET_TEST_COORD_URL"); let auth_key = require_env("SUNBEAM_NET_TEST_AUTH_KEY"); let peer_a_ip: std::net::IpAddr = require_env("SUNBEAM_NET_TEST_PEER_A_IP") @@ -171,6 +173,8 @@ async fn test_e2e_tcp_through_tunnel() { control_socket: state_dir.path().join("e2e.sock"), hostname: "sunbeam-net-e2e-test".into(), server_public_key: None, + // Test stack uses a self-signed cert. + derp_tls_insecure: std::env::var("SUNBEAM_NET_TEST_DERP_INSECURE").is_ok(), }; let handle = sunbeam_net::VpnDaemon::start(config) @@ -252,6 +256,7 @@ async fn test_daemon_lifecycle() { control_socket: state_dir.path().join("daemon.sock"), hostname: "sunbeam-net-daemon-test".into(), server_public_key: None, + derp_tls_insecure: std::env::var("SUNBEAM_NET_TEST_DERP_INSECURE").is_ok(), }; let handle = sunbeam_net::VpnDaemon::start(config) diff --git a/sunbeam-net/tests/run.sh b/sunbeam-net/tests/run.sh index b57e1f01..9ce63608 100755 --- a/sunbeam-net/tests/run.sh +++ b/sunbeam-net/tests/run.sh @@ -59,7 +59,8 @@ $COMPOSE exec -T peer-b tailscale ip -4 || true echo "==> Running integration tests..." cd ../.. SUNBEAM_NET_TEST_AUTH_KEY="$CLIENT_KEY" \ -SUNBEAM_NET_TEST_COORD_URL="http://localhost:8080" \ +SUNBEAM_NET_TEST_COORD_URL="https://localhost:8443" \ +SUNBEAM_NET_TEST_DERP_INSECURE=1 \ SUNBEAM_NET_TEST_PEER_A_IP=$($COMPOSE -f sunbeam-net/tests/docker-compose.yml exec -T peer-a tailscale ip -4 2>/dev/null | tr -d '[:space:]') \ cargo test -p sunbeam-net --features integration --test integration -- --nocapture