tailscale-proxy: add the missing IKEv2 VPN front, and correct the stale exit node
This file has been unsafe to deploy for two separate reasons. Both are fixed here, and the result is now byte-identical in effective config to what actually runs on the client-02 host (verified by diffing `docker compose config` on both). 1. ts-vpn + ikev2 were missing entirely. The IKEv2 VPN front has run on the host since 2026-09-05, but was never mirrored here — this file defined only ts-proxy, proxy and telemt. Deploying it as-is would have taken the VPN down for the Windows and iOS clients. Added both services, plus ikev2-egress.sh, which the ikev2 service mounts and cannot start without. ts-vpn is a SECOND Tailscale node in kernel/TUN mode (unlike ts-proxy, which is userspace): it forwards raw IP packets rather than TCP streams, so a userspace SOCKS5 is useless for it. It owns the network namespace that ikev2 joins via network_mode, which is also why the ports and sysctls live on ts-vpn and not on ikev2 — Docker forbids net.* sysctls on a network_mode:service container. 2. ts-proxy still pointed at the pre-migration exit node. TS_EXTRA_ARGS --exit-node=100.121.234.85 -> --exit-node=100.106.197.99 TS_HOSTNAME / hostname technohim-lan-proxy -> 02-tekhnohim-lan-proxy 100.121.234.85 has not existed since the 2026-08-27 tailnet migration. This is worse than a dead setting: with the exit node absent this stack does NOT fail closed — it silently egresses through the local ISP, so a redeploy from this repo would have turned a censorship-avoiding proxy into a plain local-egress one while still reporting healthy. This drift was recorded as an open issue in the migration write-up on 2026-08-27; these are the exact substitutions it prescribed. Not included, deliberately: ts-vpn.env and ikev2.env, which hold TS_AUTHKEY and the VPN credentials. Like every other config in this stack they are staged on the host by hand — a git push does not deploy config here, and these must never enter git. Both extracted files were scanned for secrets before being added.
This commit is contained in:
@@ -2,15 +2,15 @@ services:
|
|||||||
ts-proxy:
|
ts-proxy:
|
||||||
image: tailscale/tailscale:v1.98.9
|
image: tailscale/tailscale:v1.98.9
|
||||||
container_name: ts-proxy
|
container_name: ts-proxy
|
||||||
hostname: technohim-lan-proxy
|
hostname: 02-tekhnohim-lan-proxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file: ["/mnt/containers/tailscale-proxy/ts.env"]
|
env_file: ["/mnt/containers/tailscale-proxy/ts.env"]
|
||||||
environment:
|
environment:
|
||||||
TS_HOSTNAME: technohim-lan-proxy
|
TS_HOSTNAME: 02-tekhnohim-lan-proxy
|
||||||
TS_STATE_DIR: /var/lib/tailscale
|
TS_STATE_DIR: /var/lib/tailscale
|
||||||
TS_USERSPACE: "true"
|
TS_USERSPACE: "true"
|
||||||
TS_ACCEPT_DNS: "false"
|
TS_ACCEPT_DNS: "false"
|
||||||
TS_EXTRA_ARGS: "--exit-node=100.121.234.85 --reset"
|
TS_EXTRA_ARGS: "--exit-node=100.106.197.99 --reset"
|
||||||
TS_TAILSCALED_EXTRA_ARGS: "--socks5-server=0.0.0.0:1055 --outbound-http-proxy-listen=0.0.0.0:1056"
|
TS_TAILSCALED_EXTRA_ARGS: "--socks5-server=0.0.0.0:1055 --outbound-http-proxy-listen=0.0.0.0:1056"
|
||||||
volumes: ["/mnt/containers/tailscale-proxy/state:/var/lib/tailscale:z"]
|
volumes: ["/mnt/containers/tailscale-proxy/state:/var/lib/tailscale:z"]
|
||||||
networks: [proxynet]
|
networks: [proxynet]
|
||||||
@@ -74,6 +74,72 @@ services:
|
|||||||
# critical service. Prometheus metrics are scraped by Zabbix on :9092.
|
# critical service. Prometheus metrics are scraped by Zabbix on :9092.
|
||||||
networks: [proxynet]
|
networks: [proxynet]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# IKEv2 VPN front for iOS/macOS/Windows/Android (added 2026-09-05).
|
||||||
|
#
|
||||||
|
# ts-vpn is a SECOND Tailscale exit-node client, in KERNEL (TUN) mode - unlike
|
||||||
|
# ts-proxy it must forward raw IP packets, not TCP streams, so a userspace
|
||||||
|
# SOCKS5 is useless here. It owns the network namespace that the IKEv2 server
|
||||||
|
# (ikev2) joins via network_mode. Inside that namespace Tailscale's policy
|
||||||
|
# routing sends the default route into the tunnel (route table 52), so VPN
|
||||||
|
# clients egress from the Hetzner exit node.
|
||||||
|
# --netfilter-mode=off : Tailscale installs NO nft chains here; NAT + MSS
|
||||||
|
# clamp for the VPN subnet are done by ikev2-egress.sh (same netns).
|
||||||
|
# sysctls live here (Docker forbids net.* sysctls on a network_mode:service
|
||||||
|
# container) - hwdsl2 wants ip_forward=1 and rp_filter/redirects off.
|
||||||
|
# Ports are published HERE (the namespace owner), LAN-bound; the router
|
||||||
|
# already forwards WAN udp/500 + udp/4500 to 192.168.0.35.
|
||||||
|
# If ts-vpn is ever restarted alone, restart ikev2 too - a network_mode:service
|
||||||
|
# container keeps a stale namespace otherwise (`docker compose up -d`).
|
||||||
|
ts-vpn:
|
||||||
|
image: tailscale/tailscale:v1.98.9
|
||||||
|
container_name: ts-vpn
|
||||||
|
hostname: 02-tekhnohim-lan-vpn
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: ["/mnt/containers/tailscale-proxy/ts-vpn.env"] # TS_AUTHKEY=... 0600, NOT in git
|
||||||
|
environment:
|
||||||
|
TS_HOSTNAME: 02-tekhnohim-lan-vpn
|
||||||
|
TS_STATE_DIR: /var/lib/tailscale
|
||||||
|
TS_USERSPACE: "false"
|
||||||
|
TS_ACCEPT_DNS: "false"
|
||||||
|
TS_EXTRA_ARGS: "--exit-node=100.106.197.99 --netfilter-mode=off --reset"
|
||||||
|
cap_add: [NET_ADMIN]
|
||||||
|
devices: ["/dev/net/tun:/dev/net/tun"]
|
||||||
|
sysctls:
|
||||||
|
net.ipv4.ip_forward: "1"
|
||||||
|
net.ipv4.conf.all.rp_filter: "0"
|
||||||
|
net.ipv4.conf.default.rp_filter: "0"
|
||||||
|
net.ipv4.conf.all.send_redirects: "0"
|
||||||
|
net.ipv4.conf.default.send_redirects: "0"
|
||||||
|
net.ipv4.conf.all.accept_redirects: "0"
|
||||||
|
net.ipv4.conf.default.accept_redirects: "0"
|
||||||
|
volumes: ["/mnt/containers/tailscale-proxy/ts-vpn-state:/var/lib/tailscale:z"]
|
||||||
|
ports:
|
||||||
|
- "192.168.0.35:500:500/udp"
|
||||||
|
- "192.168.0.35:4500:4500/udp"
|
||||||
|
networks: [proxynet]
|
||||||
|
|
||||||
|
# hwdsl2 IPsec server in IKEv2-only mode (no PSK/L2TP/XAuth exposure). It
|
||||||
|
# generates ready-to-import client profiles: .mobileconfig (iOS/macOS),
|
||||||
|
# .p12 (Windows/Linux), .sswan (Android). Certificates/clients persist in the
|
||||||
|
# ikev2/ volume. ikev2-egress.sh runs alongside run.sh and keeps
|
||||||
|
# - VPN subnet -> tailscale0 MASQUERADE + MSS clamp (exit-node egress),
|
||||||
|
# - IKE/ESP replies on the docker bridge (ip rules ahead of Tailscale's),
|
||||||
|
# - the office LAN reachable from the VPN (route via the bridge gateway).
|
||||||
|
ikev2:
|
||||||
|
image: hwdsl2/ipsec-vpn-server@sha256:2e939ffe5913c9a34a3f5b72d75a97e9a2f16824d223bf042b88a567ffeabe9b # :latest of 2026-08-28
|
||||||
|
container_name: ts-lan-ikev2
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on: [ts-vpn]
|
||||||
|
network_mode: "service:ts-vpn"
|
||||||
|
env_file: ["/mnt/containers/tailscale-proxy/ikev2.env"]
|
||||||
|
cap_add: [NET_ADMIN]
|
||||||
|
volumes:
|
||||||
|
- "/mnt/containers/tailscale-proxy/ikev2:/etc/ipsec.d:z"
|
||||||
|
- "/lib/modules:/lib/modules:ro"
|
||||||
|
- "/mnt/containers/tailscale-proxy/ikev2-egress.sh:/usr/local/bin/ikev2-egress.sh:ro,z"
|
||||||
|
entrypoint: ["/bin/sh", "-c", "/usr/local/bin/ikev2-egress.sh & exec /opt/src/run.sh"]
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
proxynet:
|
proxynet:
|
||||||
name: tailscale-proxynet
|
name: tailscale-proxynet
|
||||||
|
|||||||
Executable
+76
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ikev2-egress.sh - runs inside ts-lan-ikev2, which shares ts-vpn's network
|
||||||
|
# namespace. Tailscale (kernel mode, --netfilter-mode=off) only installs the
|
||||||
|
# policy routing (default -> tailscale0 via table 52); this loop owns the rest
|
||||||
|
# and is idempotent, so a container restart or a late tailscale0 is harmless.
|
||||||
|
#
|
||||||
|
# 1. ip rules ahead of Tailscale's 52xx rules: replies of the IKE/NAT-T/ESP
|
||||||
|
# server itself must leave via the docker bridge (main table), otherwise
|
||||||
|
# the SYN-ACK-equivalent goes into the tunnel and no client ever connects.
|
||||||
|
# 2. FORWARD accepts for VPN<->tailscale0 ahead of hwdsl2's final -j DROP, and
|
||||||
|
# MASQUERADE everything leaving tailscale0 to the node's own 100.x address -
|
||||||
|
# the exit node's WireGuard AllowedIPs drops any other source.
|
||||||
|
# 3. MSS clamp to the tunnel PMTU (tailscale0 is MTU 1280; hwdsl2 clamps 1360).
|
||||||
|
# 4. Pin the Libreswan conn to eth0's address: hwdsl2 writes left=%defaultroute,
|
||||||
|
# which resolves through Tailscale's policy routing to the tailscale0 address,
|
||||||
|
# so IKE packets arriving on the bridge match no conn (NO_PROPOSAL_CHOSEN).
|
||||||
|
# 5. LAN access: one ip rule sends LAN_NET out the docker bridge (main table)
|
||||||
|
# instead of table 52, plus FORWARD accepts and a MASQUERADE to the bridge
|
||||||
|
# address - LAN hosts have no route back to the VPN pool. Scoped to LAN_NET
|
||||||
|
# only: every other destination, including the docker bridges themselves,
|
||||||
|
# still falls through to table 52 = tailscale0. Set LAN_NET empty to restore
|
||||||
|
# the original egress-only behaviour.
|
||||||
|
# The LAN block sits OUTSIDE the tailscale0 guard on purpose: LAN access does not
|
||||||
|
# depend on the exit node, so a client still reaches the office if the tunnel is
|
||||||
|
# down (internet then fails closed, which is what we want).
|
||||||
|
TS_IF=tailscale0
|
||||||
|
LAN_IF=eth0
|
||||||
|
VPN_NET="192.168.43.0/24" # hwdsl2 IKEv2 address pool (rightaddresspool)
|
||||||
|
LAN_NET="192.168.0.0/22" # client-02 head office (Kirochnaya). Branch sites
|
||||||
|
# are NOT routed here - see the deployment note.
|
||||||
|
log() { echo "[ikev2-egress] $*"; }
|
||||||
|
|
||||||
|
ensure_ipt() { # ensure_ipt <table> <chain> <rule...>
|
||||||
|
t=$1; c=$2; shift 2
|
||||||
|
iptables -w -t "$t" -C "$c" "$@" 2>/dev/null || { iptables -w -t "$t" -A "$c" "$@" && log "iptables -t $t -A $c $*"; }
|
||||||
|
}
|
||||||
|
ensure_ipt_first() { # like ensure_ipt but inserts at the top (hwdsl2 ends FORWARD with -j DROP)
|
||||||
|
t=$1; c=$2; shift 2
|
||||||
|
iptables -w -t "$t" -C "$c" "$@" 2>/dev/null || { iptables -w -t "$t" -I "$c" 1 "$@" && log "iptables -t $t -I $c $*"; }
|
||||||
|
}
|
||||||
|
ensure_rule() { # ensure_rule <pref> <selector...>
|
||||||
|
p=$1; shift
|
||||||
|
ip rule list | grep -q "^$p:" || { ip rule add pref "$p" "$@" && log "ip rule $p: $*"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_left() {
|
||||||
|
conf=/etc/ipsec.d/ikev2.conf
|
||||||
|
[ -f "$conf" ] || return 0
|
||||||
|
ip4=$(ip -4 -o addr show dev eth0 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
||||||
|
[ -n "$ip4" ] || return 0
|
||||||
|
grep -q "^ left=$ip4\$" "$conf" && return 0
|
||||||
|
sed -i "s|^ left=.*| left=$ip4|" "$conf" && log "ikev2.conf: left=$ip4 (was %defaultroute -> tailscale0)"
|
||||||
|
if ipsec auto --replace ikev2-cp >/dev/null 2>&1; then log "ipsec auto --replace ikev2-cp"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
fix_left
|
||||||
|
if [ -n "$LAN_NET" ] && ip link show "$LAN_IF" >/dev/null 2>&1; then
|
||||||
|
ensure_rule 5010 to "$LAN_NET" lookup main
|
||||||
|
ensure_ipt_first filter FORWARD -s "$VPN_NET" -d "$LAN_NET" -o "$LAN_IF" -j ACCEPT
|
||||||
|
ensure_ipt_first filter FORWARD -d "$VPN_NET" -s "$LAN_NET" -i "$LAN_IF" \
|
||||||
|
-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||||
|
ensure_ipt nat POSTROUTING -s "$VPN_NET" -d "$LAN_NET" -o "$LAN_IF" -j MASQUERADE
|
||||||
|
fi
|
||||||
|
if ip link show "$TS_IF" >/dev/null 2>&1; then
|
||||||
|
ensure_rule 5001 ipproto udp sport 500 lookup main
|
||||||
|
ensure_rule 5002 ipproto udp sport 4500 lookup main
|
||||||
|
ensure_rule 5003 ipproto 50 lookup main
|
||||||
|
ensure_ipt_first filter FORWARD -s "$VPN_NET" -o "$TS_IF" -j ACCEPT
|
||||||
|
ensure_ipt_first filter FORWARD -d "$VPN_NET" -i "$TS_IF" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||||
|
ensure_ipt nat POSTROUTING -o "$TS_IF" -j MASQUERADE
|
||||||
|
ensure_ipt mangle FORWARD -o "$TS_IF" -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
|
||||||
|
fi
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user