diff --git a/stacks/wgtunnel/.env.example b/stacks/wgtunnel/.env.example new file mode 100644 index 0000000..558de3b --- /dev/null +++ b/stacks/wgtunnel/.env.example @@ -0,0 +1,19 @@ +# wgtunnel server stack — host env template. +# Copy to `.env` next to compose.yaml on ana-docker and adjust as needed. +# This template is committed; the real `.env` is gitignored (it is not secret +# today, but the pattern keeps host-specific values out of git). + +# Pinned erebe/wstunnel image tag. NEVER :latest. (v10.6.2 verified as the current +# stable release, and `server --restrict-to ` confirmed against its --help.) +WGTUNNEL_IMAGE_TAG=v10.6.2 + +# The WireGuard endpoint wstunnel unwraps to (ana-wg). `--restrict-to` enforces +# that the server forwards only here — it is not an open relay. +WG_TARGET=10.250.50.252:31337 + +# Public hostname — a deliberately innocuous SNI. DNS-only CNAME to the colo edge +# (ana-srv1.phasefinal.com); never Cloudflare-proxied. +TUNNEL_HOST=boring.phasefinal.com + +# Internal plain-WebSocket port the container listens on; traefik forwards here. +WSTUNNEL_PORT=8080 diff --git a/stacks/wgtunnel/README.md b/stacks/wgtunnel/README.md new file mode 100644 index 0000000..aec5e40 --- /dev/null +++ b/stacks/wgtunnel/README.md @@ -0,0 +1,33 @@ +# wgtunnel — obfuscated WireGuard front (server stack) + +The server-side stack for **wgtunnel**: a WireGuard tunnel that survives captive-portal / +DPI networks (airplane, hotel) by wrapping WG in WebSocket-over-TLS on `:443`. + +This is the **`erebe/wstunnel` server** behind traefik on **ana-docker**. traefik terminates +TLS for `boring.phasefinal.com` (Mode A, `anaprod` Let's Encrypt cert) and forwards the +WebSocket to this container, which unwraps it to UDP and delivers it to the WireGuard server +at `ana-wg` (`10.250.50.252:31337`). `--restrict-to` pins the only forwarding target, so it +is **not an open relay**. + +- **Host:** ana-docker (`10.250.50.70`), on the external `traefik-net`. +- **Public path:** `boring.phasefinal.com` (DNS-only CNAME → `ana-srv1.phasefinal.com` → + `38.120.12.44`, never Cloudflare-proxied) → traefik `:443` → this container → `ana-wg:31337`. +- **Image:** `ghcr.io/erebe/wstunnel` (pinned via `.env`, never `:latest`). +- **Healthcheck:** greps `/proc/net/tcp` for the listener (the image has no `nc`/`curl` and + dash lacks `/dev/tcp`). + +## Deploy + +```bash +# canonical: stacks/wgtunnel/ -> /opt/docker/compose/wgtunnel/ on ana-docker +scripts/deploy-stack.sh ana-docker wgtunnel # diffs vs live, prompts y/N +# on the host: cp .env.example .env (adjust), then: docker compose up -d +``` + +## Full project + +The complete wgtunnel project (client-side bring-up, captive-portal handling, design docs) +lives in its own repo, **`vh/wgtunnel`** (`~/development/wgtunnel`). This directory mirrors +only the deployed server stack for fleet-convention drift tracking. + +Author: Vuong Hoang. diff --git a/stacks/wgtunnel/compose.yaml b/stacks/wgtunnel/compose.yaml new file mode 100644 index 0000000..55cfc31 --- /dev/null +++ b/stacks/wgtunnel/compose.yaml @@ -0,0 +1,38 @@ +version: "3.8" + +services: + wstunnel: + image: ghcr.io/erebe/wstunnel:${WGTUNNEL_IMAGE_TAG} + restart: unless-stopped + # The image ENTRYPOINT is dumb-init; we override the command with the full + # binary path so dumb-init execs it. `--restrict-to` is not in the image's + # default CMD, so a full override is required. + command: + - /home/app/wstunnel + - server + - --restrict-to + - ${WG_TARGET} + - ws://0.0.0.0:${WSTUNNEL_PORT} + networks: + - traefik-net + healthcheck: + # The image is dash + grep/sed/cat only — no nc/wget/curl and dash has no + # /dev/tcp (contract R5.4 "no usable probe binary" case). Verify the listener + # via /proc/net/tcp instead: port 8080 = 0x1F90 (update if WSTUNNEL_PORT changes). + test: ["CMD-SHELL", "grep -qi ':1F90 ' /proc/net/tcp /proc/net/tcp6 2>/dev/null || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + labels: + - traefik.enable=true + - traefik.docker.network=traefik-net + - "traefik.http.routers.wgtunnel.rule=Host(`${TUNNEL_HOST}`)" + - traefik.http.routers.wgtunnel.entrypoints=websecure + - traefik.http.routers.wgtunnel.tls=true + - traefik.http.routers.wgtunnel.tls.certresolver=anaprod + - traefik.http.services.wgtunnel.loadbalancer.server.port=${WSTUNNEL_PORT} + +networks: + traefik-net: + external: true