diff --git a/common.nix b/common.nix index 64ea13c..8262ddc 100644 --- a/common.nix +++ b/common.nix @@ -38,6 +38,7 @@ ./services/shelfarr.nix ./services/adguard.nix ./services/router.nix + ./services/imgur-vpn.nix ./services/crowdsec.nix ./services/service-health.nix ./services/sabnzbd.nix diff --git a/services/imgur-vpn.nix b/services/imgur-vpn.nix new file mode 100644 index 0000000..d4414ee --- /dev/null +++ b/services/imgur-vpn.nix @@ -0,0 +1,74 @@ +# services/imgur-vpn.nix — route imgur out a WireGuard tunnel, network-wide +# +# imgur geo-blocks UK IPs, so LAN clients need a non-UK exit for imgur only. +# imgur.com / i.imgur.com / api.imgur.com all resolve to two Fastly anycast +# /24s; we send just those prefixes down the tunnel and leave everything else +# on eno1. +# +# ponytail: static prefixes, not a DNS-driven nftset. Other Fastly customers +# sharing these /24s ride the tunnel too — harmless, just a longer path. If +# that ever matters, add dnsmasq `nftset=` + fwmark policy routing instead. +# +# Setup (one-off, on the mediaserver): +# 1. Fill in peerPublicKey / peerEndpoint / tunnelAddress below from your +# VPN provider's WireGuard config (pick a non-UK exit). +# 2. Write the private key, no trailing newline, readable by systemd-network: +# install -m600 -o systemd-network -g systemd-network \ +# /dev/stdin /var/secrets/wg-imgur.key <<< '' +# 3. Rebuild. Verify: curl --interface wg-imgur https://api.imgur.com/ +{ config, lib, ... }: +let + # --- provider details: fill these in --- + peerPublicKey = "@PEER_PUBLIC_KEY@"; + peerEndpoint = "@HOST_OR_IP@:51820"; + tunnelAddress = "@TUNNEL_IP@/32"; # address the provider assigned us + + # imgur's Fastly anycast prefixes. + imgurNets = [ "199.232.192.0/24" "199.232.196.0/24" ]; +in +{ + config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { + + systemd.network = { + netdevs."30-wg-imgur" = { + netdevConfig = { + Name = "wg-imgur"; + Kind = "wireguard"; + }; + # Key read at runtime — keeps flake eval pure (hosts build from Forgejo). + wireguardConfig.PrivateKeyFile = "/var/secrets/wg-imgur.key"; + wireguardPeers = [{ + PublicKey = peerPublicKey; + Endpoint = peerEndpoint; + # Doubles as the tunnel's crypto-routing scope: only imgur goes in. + AllowedIPs = imgurNets; + PersistentKeepalive = 25; + }]; + }; + + networks."30-wg-imgur" = { + matchConfig.Name = "wg-imgur"; + address = [ tunnelAddress ]; + # Plain main-table routes: destination-based, so no fwmark/ip-rule + # machinery and no rp_filter breakage on the reply path. + routes = map (n: { Destination = n; }) imgurNets; + linkConfig.RequiredForOnline = "no"; + }; + }; + + # LAN sources must be NAT'd to the tunnel address, and TCP MSS clamped — + # the tunnel's 1420 MTU otherwise black-holes full-size segments. + networking.nftables.tables.router-nat.content = lib.mkAfter '' + chain wg_imgur_postrouting { + type nat hook postrouting priority 101; policy accept; + oifname "wg-imgur" masquerade + } + ''; + networking.nftables.tables.filter.content = lib.mkAfter '' + chain wg_imgur_mss { + type filter hook forward priority 0; policy accept; + oifname "wg-imgur" tcp flags syn tcp option maxseg size set rt mtu + } + ''; + }; +}