WireGuard Tunnel โ€” CT100 โ†’ UltraCC All Docker outbound exits via UltraCC NL

Deployed April 29, 2026 on CT100. Replaces the per-app SOCKS5 proxy approach for outbound privacy on torrent indexers, Hardcover lookups, etc.

What this does and why

Before: Shelfmark and the *arr stack used a SOCKS5 proxy (autossh tunnel to seedbox at port 1080) for outbound traffic that needed to look non-residential. Each app had to be individually configured.

After: A WireGuard interface on CT100 (wg0) routes all outbound from every container through UltraCC’s NL endpoint. Apps don’t need any per-service proxy config; they just talk to the internet normally and the tunnel handles the rest.

Tunnel endpoint UltraCC NL โ€” 45.86.221.26:13012
Server pubkey bOBo86jrNKBikLqOFdksL41bTnQHWWHynM3/v3yq2Vg=
Client (CT100) IP 10.13.13.2/32
Listen port 51820 (CT100 side)
AllowedIPs 0.0.0.0/0 (full tunnel)
MTU 1420
Public IP from CT100 45.86.221.26 (NL exit)

Private key + PSK for this tunnel were passed through chat โ€” rotation pending. Replace the peer in the UltraCC panel and re-import the config to rotate.

Topology
Container (e.g., sonarr)
    โ”‚ HTTPS to indexer.com
    โ–ผ
Docker bridge 172.16.x.0/24
    โ”‚ (MASQUERADE in nat POSTROUTING)
    โ–ผ
CT100 routing table
    โ”‚ default via wg0 (table 51820, fwmark 0xca6c)
    โ–ผ
wg0 โ€” WireGuard interface
    โ”‚ encrypted UDP to 45.86.221.26:13012
    โ–ผ
CT100 eth0 โ†’ OPNsense โ†’ ISP โ†’ UltraCC NL
    โ”‚
    โ–ผ
Public internet (source IP appears as 45.86.221.26)

LAN traffic (192.168.8.0/24, 172.16.0.0/12, 100.123.0.0/16) bypasses the tunnel. Only outbound to the public internet goes through wg0.

Kill-switch

The pre-existing CT100 firewall acts as the kill-switch โ€” if wg0 drops, the OUTPUT chain has no rules permitting public-internet egress, so all packets are dropped (with explicit LOG+DROP rule at the end). No traffic leaks to the ISP if the tunnel fails.

Permitted egress (whitelist in /etc/iptables/rules.v4):

  • 192.168.8.0/24 โ€” LAN
  • 172.16.0.0/12 โ€” Docker bridges
  • 100.123.0.0/16 โ€” NetBird overlay
  • DNS to 192.168.8.53 โ€” Pi-hole
  • SSH to 46.232.210.50:22 โ€” seedbox (for autossh tunnels still in use)
  • UDP to 45.86.221.26:13012 โ€” WireGuard handshake to UltraCC
  • Anything via wg0 (the tunnel itself)

Everything else: LOG + DROP.

MSS clamping (critical)

WireGuard’s MTU (1420) is smaller than Docker bridges (1500). Without MSS clamping, TCP from containers fails on TLS handshake โ€” packets get fragmented or silently dropped, connections hang. Symptoms: ping works (small packets) but curl https://... times out.

Fix is in the mangle table:

iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN \
    -j TCPMSS --clamp-mss-to-pmtu
iptables -t mangle -A POSTROUTING -o wg0 -p tcp --tcp-flags SYN,RST SYN \
    -j TCPMSS --clamp-mss-to-pmtu

Both rules are persisted in /etc/iptables/rules.v4.

Systemd & boot persistence
# Enabled on CT100:
systemctl is-enabled wg-quick@wg0    # โ†’ enabled
systemctl status wg-quick@wg0        # โ†’ active

/etc/wireguard/wg0.conf holds the peer config (chmod 600, root-only). On boot, wg-quick@wg0.service brings up the interface, applies the iptables rules in /etc/iptables/rules.v4 via iptables-restore, and the kill-switch + MSS clamp + tunnel are all live.

Health checks & diagnostics

Verify tunnel from CT100:

pct exec 100 -- wg show wg0
# Expect: handshake within last 2-3 minutes, transfer counters incrementing.

pct exec 100 -- curl -s https://api.ipify.org
# Expect: 45.86.221.26 (NOT your home WAN IP).

Verify from a container (e.g., Sonarr):

pct exec 100 -- docker exec sonarr sh -c \
    'curl -sS -o /dev/null -w "%{http_code} via %{remote_ip}" https://api.ipify.org'
# Expect: 200 via <some-ipify-IP>; the response body shows 45.86.221.26

Uptime Kuma monitor (id 76, “WireGuard Exit IP (UltraCC)”) does this exact check every 5 minutes โ€” keyword 45.86.221.26 against https://api.ipify.org. Alerts via Gotify if the tunnel drops or routing changes.

If TCP fails but ping works: MSS clamp rules are missing (see MSS clamping).

If the whole tunnel is dead: check for handshake errors. Often the cure is systemctl restart wg-quick@wg0. If UltraCC re-keyed, you’ll need a fresh config from their panel.

Containers using the tunnel

All of them โ€” every CT100 container’s outbound traffic exits via 45.86.221.26. This is by design.

The WireGuard tunnel replaced the SOCKS5 proxy (autossh-socks / seedbox-socks) for outbound privacy routing โ€” apps no longer need per-service proxy configuration. However, the SSH RPC tunnels are NOT redundant and remain actively required:

Tunnel Purpose Status
transmission-tunnel.service (hpve) SSH tunnel :13010 โ†’ seedbox Transmission RPC. Used by Sonarr/Radarr/Lidarr for download management Active โ€” required
autossh-transmission.service (CT100) AutoSSH tunnel :13010 โ†’ seedbox Transmission RPC Active โ€” required
autossh-nzbget.service (CT100) AutoSSH tunnel :16789 โ†’ seedbox NZBGet Active โ€” required
autossh-socks.service (CT100) SOCKS5 :1080 โ†’ seedbox NL exit Active โ€” legacy, replaced by WireGuard for most apps

WireGuard handles privacy routing (making outbound traffic appear from NL); it does not replace the RPC control tunnels that let the *arr apps communicate with download clients on the seedbox.

Backup of installation state

When the tunnel was deployed, a snapshot was taken at:

/root/wg-deploy-backup-20260429-150327/
  rules.v4.before    # iptables before WG
  iptables.live.before

To roll back: iptables-restore < /root/wg-deploy-backup-*/rules.v4.before && systemctl stop wg-quick@wg0 && systemctl disable wg-quick@wg0.