diff --git a/servers/bwh-us-dc1/frontdoor/.env.example b/servers/bwh-us-dc1/frontdoor/.env.example new file mode 100644 index 0000000..7ec3327 --- /dev/null +++ b/servers/bwh-us-dc1/frontdoor/.env.example @@ -0,0 +1,2 @@ +# HAProxy owns public TCP 443. Hysteria2 owns public UDP 443 separately. +HAPROXY_PUBLIC_PORT=443 diff --git a/servers/bwh-us-dc1/frontdoor/README.md b/servers/bwh-us-dc1/frontdoor/README.md new file mode 100644 index 0000000..ecee034 --- /dev/null +++ b/servers/bwh-us-dc1/frontdoor/README.md @@ -0,0 +1,12 @@ +# DC1 TLS front door + +HAProxy inspects TCP SNI without terminating TLS: + +- `dc1-tls-676b11.opfo.xyz` -> sing-box Trojan on internal TCP 34795 +- `dc1-any-9a4fad.opfo.xyz` -> sing-box AnyTLS on internal TCP 40887 +- unmatched SNI -> Caddy fallback serving `dc1.opfo.xyz` + +HAProxy publishes host TCP 443. Hysteria2 owns host UDP 443. Caddy publishes +host TCP 80 and receives HTTPS from HAProxy over the project network. Trojan +and AnyTLS have no high-numbered host port. Both projects join the external +`dc1-proxy` network. diff --git a/servers/bwh-us-dc1/frontdoor/caddy/Caddyfile b/servers/bwh-us-dc1/frontdoor/caddy/Caddyfile new file mode 100644 index 0000000..bd73605 --- /dev/null +++ b/servers/bwh-us-dc1/frontdoor/caddy/Caddyfile @@ -0,0 +1,22 @@ +{ + email tnki.per@gmail.com + servers { + protocols h1 h2 + } +} + +dc1.opfo.xyz { + encode zstd gzip + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + Referrer-Policy no-referrer + } + respond < + +Service Status +

Service online

The requested service is available.

+ +HTML +} diff --git a/servers/bwh-us-dc1/frontdoor/compose.yaml b/servers/bwh-us-dc1/frontdoor/compose.yaml new file mode 100644 index 0000000..49af3b4 --- /dev/null +++ b/servers/bwh-us-dc1/frontdoor/compose.yaml @@ -0,0 +1,44 @@ +services: + haproxy: + image: haproxy:3.4.4-alpine3.24 + container_name: dc1-haproxy + restart: unless-stopped + ports: + - "${HAPROXY_PUBLIC_PORT:-443}:8443/tcp" + depends_on: + - decoy-caddy + networks: + - default + - proxy + volumes: + - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + logging: + driver: json-file + options: + max-size: 200k + max-file: "2" + + decoy-caddy: + image: caddy:2.11.4 + container_name: dc1-decoy-caddy + restart: unless-stopped + ports: + - "80:80/tcp" + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + logging: + driver: json-file + options: + max-size: 200k + max-file: "2" + +volumes: + caddy-data: + caddy-config: + +networks: + proxy: + external: true + name: dc1-proxy diff --git a/servers/bwh-us-dc1/frontdoor/haproxy/haproxy.cfg b/servers/bwh-us-dc1/frontdoor/haproxy/haproxy.cfg new file mode 100644 index 0000000..ebabbf4 --- /dev/null +++ b/servers/bwh-us-dc1/frontdoor/haproxy/haproxy.cfg @@ -0,0 +1,32 @@ +global + log stdout format raw local0 + maxconn 4096 + +defaults + log global + mode tcp + option tcplog + timeout connect 5s + timeout client 24h + timeout server 24h + +frontend tls_443 + bind 0.0.0.0:8443 + tcp-request inspect-delay 5s + tcp-request content accept if { req.ssl_hello_type 1 } + + acl sni_trojan req.ssl_sni -i dc1-tls-676b11.opfo.xyz + acl sni_anytls req.ssl_sni -i dc1-any-9a4fad.opfo.xyz + + use_backend singbox_trojan if sni_trojan + use_backend singbox_anytls if sni_anytls + default_backend decoy_https + +backend singbox_trojan + server trojan dc1-sing-box:34795 + +backend singbox_anytls + server anytls dc1-sing-box:40887 + +backend decoy_https + server caddy decoy-caddy:443 diff --git a/servers/bwh-us-dc1/sing-box/.env.example b/servers/bwh-us-dc1/sing-box/.env.example new file mode 100644 index 0000000..fd6e20f --- /dev/null +++ b/servers/bwh-us-dc1/sing-box/.env.example @@ -0,0 +1,18 @@ +DOMAIN=dc1-tls-676b11.opfo.xyz +ACME_EMAIL=admin@example.com +LISTEN_PORT=34795 +USER1_PASSWORD=change-me +CLOUDFLARE_API_TOKEN=change-me + +ANYTLS_DOMAIN=dc1-any-9a4fad.opfo.xyz +ANYTLS_PORT=40887 +ANYTLS_PASSWORD=change-me +HY2_PASSWORD=change-me +HY2_OBFS_PASSWORD=change-me +HY2_PORT=443 + +# sing-box uses version 5 for the Snell v4-compatible TCP wire protocol. +SNELL_V4_PORT=31385 +SNELL_V4_PSK=change-me +SNELL_V6_PORT=27276 +SNELL_V6_PSK=change-me-at-least-12-bytes diff --git a/servers/bwh-us-dc1/sing-box/README.md b/servers/bwh-us-dc1/sing-box/README.md new file mode 100644 index 0000000..7b77426 --- /dev/null +++ b/servers/bwh-us-dc1/sing-box/README.md @@ -0,0 +1,15 @@ +# DC1 sing-box + +The managed sing-box service provides: + +- Trojan on internal TCP 34795, exposed only on TCP 443 through HAProxy +- AnyTLS on internal TCP 40887, exposed only on TCP 443 through HAProxy +- Hysteria2 on UDP 443 +- Snell v4-compatible on TCP 31385 (`version: 5` server wire format) +- Snell v6 on TCP 27276 + +Credentials and the Cloudflare API token live only in the Arcane +Environment-local `.env`. Reality is intentionally omitted because it did not +pass repeat interoperability tests on the existing HK and DC9 nodes. The ACME +provider is ZeroSSL; sing-box automatically obtains ZeroSSL EAB credentials +from the configured account email. diff --git a/servers/bwh-us-dc1/sing-box/compose.yaml b/servers/bwh-us-dc1/sing-box/compose.yaml new file mode 100644 index 0000000..4f15714 --- /dev/null +++ b/servers/bwh-us-dc1/sing-box/compose.yaml @@ -0,0 +1,62 @@ +services: + config-render: + image: alpine:3.24 + environment: + DOMAIN: ${DOMAIN} + ACME_EMAIL: ${ACME_EMAIL} + LISTEN_PORT: ${LISTEN_PORT} + USER1_PASSWORD: ${USER1_PASSWORD} + CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN} + ANYTLS_DOMAIN: ${ANYTLS_DOMAIN} + ANYTLS_PORT: ${ANYTLS_PORT} + ANYTLS_PASSWORD: ${ANYTLS_PASSWORD} + HY2_PASSWORD: ${HY2_PASSWORD} + HY2_OBFS_PASSWORD: ${HY2_OBFS_PASSWORD} + HY2_PORT: ${HY2_PORT} + SNELL_V4_PORT: ${SNELL_V4_PORT} + SNELL_V4_PSK: ${SNELL_V4_PSK} + SNELL_V6_PORT: ${SNELL_V6_PORT} + SNELL_V6_PSK: ${SNELL_V6_PSK} + volumes: + - ./etc/sing-box/config.json.template:/template/config.json.template:ro + - sing-box-config:/output + command: + - /bin/sh + - -ec + - | + apk add --no-cache gettext + envsubst < /template/config.json.template > /output/config.json + chmod 600 /output/config.json + restart: "no" + + sing-box: + image: ghcr.io/sagernet/sing-box:v1.14.0 + container_name: dc1-sing-box + restart: unless-stopped + depends_on: + config-render: + condition: service_completed_successfully + volumes: + - sing-box-config:/etc/sing-box:ro + - sing-box-data:/var/lib/sing-box + command: -D /var/lib/sing-box -C /etc/sing-box/ run + ports: + - "443:443/udp" + - "31385:31385/tcp" + - "27276:27276/tcp" + networks: + - proxy + logging: + driver: json-file + options: + max-size: 200k + max-file: "2" + +volumes: + sing-box-config: + sing-box-data: + +networks: + proxy: + external: true + name: dc1-proxy diff --git a/servers/bwh-us-dc1/sing-box/etc/sing-box/config.json.template b/servers/bwh-us-dc1/sing-box/etc/sing-box/config.json.template new file mode 100644 index 0000000..e9a17f8 --- /dev/null +++ b/servers/bwh-us-dc1/sing-box/etc/sing-box/config.json.template @@ -0,0 +1,109 @@ +{ + "certificate_providers": [ + { + "type": "acme", + "tag": "public-cert", + "provider": "zerossl", + "domain": [ + "${DOMAIN}", + "${ANYTLS_DOMAIN}" + ], + "email": "${ACME_EMAIL}", + "dns01_challenge": { + "provider": "cloudflare", + "api_token": "${CLOUDFLARE_API_TOKEN}" + } + } + ], + "inbounds": [ + { + "type": "trojan", + "tag": "trojan-in", + "listen": "::", + "listen_port": ${LISTEN_PORT}, + "users": [ + { + "name": "user1", + "password": "${USER1_PASSWORD}" + } + ], + "tls": { + "enabled": true, + "server_name": "${DOMAIN}", + "certificate_provider": "public-cert" + }, + "multiplex": { + "enabled": true + } + }, + { + "type": "anytls", + "tag": "anytls-in", + "listen": "::", + "listen_port": ${ANYTLS_PORT}, + "users": [ + { + "name": "user1", + "password": "${ANYTLS_PASSWORD}" + } + ], + "tls": { + "enabled": true, + "server_name": "${ANYTLS_DOMAIN}", + "certificate_provider": "public-cert" + } + }, + { + "type": "hysteria2", + "tag": "hysteria2-in", + "listen": "0.0.0.0", + "listen_port": ${HY2_PORT}, + "obfs": { + "type": "salamander", + "password": "${HY2_OBFS_PASSWORD}" + }, + "users": [ + { + "name": "user1", + "password": "${HY2_PASSWORD}" + } + ], + "tls": { + "enabled": true, + "server_name": "${ANYTLS_DOMAIN}", + "alpn": [ + "h3" + ], + "certificate_provider": "public-cert" + } + }, + { + "type": "snell", + "tag": "snell-v4-compatible-in", + "listen": "::", + "listen_port": ${SNELL_V4_PORT}, + "version": 5, + "psk": "${SNELL_V4_PSK}", + "obfs_mode": "none" + }, + { + "type": "snell", + "tag": "snell-v6-in", + "listen": "::", + "listen_port": ${SNELL_V6_PORT}, + "version": 6, + "psk": "${SNELL_V6_PSK}", + "mode": "default" + } + ], + "outbounds": [ + { + "type": "direct" + } + ], + "log": { + "disabled": false, + "level": "info", + "timestamp": true + } +}