Files
cli/sunbeam-net/tests/docker-compose.yml
Sienna Meridian Satterwhite 94fb6155f7 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
2026-04-07 15:29:03 +01:00

119 lines
4.3 KiB
YAML

# Integration test stack for sunbeam-net VPN client.
# Spins up Headscale (coordination + embedded DERP) and two Tailscale
# peers so we can test the full TS2021 → WireGuard → DERP pipeline.
#
# Usage:
# docker compose -f sunbeam-net/tests/docker-compose.yml up -d
# docker compose -f sunbeam-net/tests/docker-compose.yml exec headscale \
# headscale preauthkeys create --user test --reusable --expiration 1h
# # Copy the key, then:
# SUNBEAM_NET_TEST_AUTH_KEY=<key> \
# SUNBEAM_NET_TEST_COORD_URL=http://localhost:8080 \
# cargo test -p sunbeam-net --features integration --test integration
# docker compose -f sunbeam-net/tests/docker-compose.yml down
services:
# ── Headscale (coordination server + embedded DERP relay) ───────────
headscale:
image: headscale/headscale:0.23
command: serve
ports:
- "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"]
interval: 5s
timeout: 5s
retries: 15
# ── Tailscale peer A (validates that Headscale is working) ──────────
# This peer registers with Headscale and stays online so our Rust
# client can discover it in the netmap and attempt WireGuard tunnels.
# Runs in TUN mode (TS_USERSPACE=false) so the host kernel actually
# routes packets to peer-a's tailnet IP — this is what makes inbound
# TCP from other tailnet members work end-to-end.
peer-a:
image: tailscale/tailscale:stable
hostname: peer-a
depends_on:
headscale:
condition: service_healthy
environment:
TS_AUTHKEY: "${PEER_A_AUTH_KEY}"
TS_STATE_DIR: /var/lib/tailscale
TS_USERSPACE: "false"
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
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "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"]
interval: 5s
timeout: 5s
retries: 20
# ── Tailscale peer B (second peer for relay/direct tests) ───────────
peer-b:
image: tailscale/tailscale:stable
hostname: peer-b
depends_on:
headscale:
condition: service_healthy
environment:
TS_AUTHKEY: "${PEER_B_AUTH_KEY}"
TS_STATE_DIR: /var/lib/tailscale
TS_USERSPACE: "false"
TS_EXTRA_ARGS: --login-server=https://headscale:8443
SSL_CERT_FILE: /etc/headscale/test-cert.pem
cap_add:
- NET_ADMIN
- NET_RAW
devices:
- /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
timeout: 5s
retries: 20
# ── Simple HTTP echo server on peer-a's tailnet IP ──────────────────
# Used to verify end-to-end TCP connectivity through the WireGuard tunnel.
# Listens on peer-a's container network; reachable via peer-a's tailnet IP.
echo:
image: hashicorp/http-echo:latest
command: -listen=:5678 -text="sunbeam-net integration test"
network_mode: "service:peer-a"
depends_on:
peer-a:
condition: service_healthy
volumes:
headscale-data:
peer-a-state:
peer-b-state: