test(net): TLS-enabled docker stack and active e2e test
The docker-compose stack now serves Headscale (and its embedded DERP) over TLS on port 8443 with a self-signed cert covering localhost, 127.0.0.1, and the docker-network hostname `headscale`. Tailscale peers trust the cert via SSL_CERT_FILE; our test daemon uses `derp_tls_insecure: true` (gated on the SUNBEAM_NET_TEST_DERP_INSECURE env var) since pinning a self-signed root in tests is more trouble than it's worth. With TLS DERP working, the previously-ignored `test_e2e_tcp_through_tunnel` test now passes: the daemon spawns, registers, completes a Noise handshake over TLS, opens a TLS DERP relay session, runs a real WireGuard handshake with peer-a (verified via boringtun ↔ tailscale interop), and TCP-tunnels an HTTP GET through smoltcp ↔ engine ↔ proxy ↔ test client. The 191-byte echo response round-trips and the test asserts on its body. - tests/config/headscale.yaml: tls_cert_path + tls_key_path, listen on 8443, server_url=https://headscale:8443 - tests/config/test-cert.pem + test-key.pem: 365-day self-signed RSA cert with SAN DNS:localhost, DNS:headscale, IP:127.0.0.1 - tests/docker-compose.yml: mount certs into headscale + both peers, set SSL_CERT_FILE on the peers, expose 8443 instead of 8080 - tests/run.sh: switch to https://localhost:8443, set SUNBEAM_NET_TEST_DERP_INSECURE=1 - tests/integration.rs: drop the #[ignore] on test_e2e_tcp_through_tunnel, read derp_tls_insecure from env in all four test configs
This commit is contained in:
@@ -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
|
||||
|
||||
20
sunbeam-net/tests/config/test-cert.pem
Normal file
20
sunbeam-net/tests/config/test-cert.pem
Normal file
@@ -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-----
|
||||
28
sunbeam-net/tests/config/test-key.pem
Normal file
28
sunbeam-net/tests/config/test-key.pem
Normal file
@@ -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-----
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user