# xlab-gateway networking
# Bond (balance-xor) → VLANs (lan254, wan99, mgmt) → MACVLAN (wan99.0)
# WireGuard tunnels with policy routing ("freedom" tables)
# NAT/masquerade on wan99.0
{ config, pkgs, ... }:
let
xlabWg = pkgs.writeShellApplication {
name = "xlab-wg";
runtimeInputs = with pkgs; [ coreutils gnugrep gawk iproute2 systemd wireguard-tools ];
text = ''
set -euo pipefail
state=/var/lib/xlab-wireguard
command=status
target=all
if (( $# >= 1 )); then command=$1; shift; fi
if (( $# >= 1 )); then target=$1; shift; fi
if (( $# != 0 )); then
echo "usage: sudo xlab-wg {status|enable|disable|restart|health} [wgnet|skyworks|all]" >&2
exit 2
fi
if (( EUID != 0 )); then
echo "xlab-wg must run as root" >&2
exit 1
fi
install -d -m 0750 "$state"
configured() {
local iface=$1 port peer
case "$iface" in
wg-to-wgnet)
port=51998
peer='H+PAPw+1MsE50Of4VMMPzMbzGG731CkNrIgaXbxcFwk='
;;
wg-to-skyworks)
port=46961
peer='yyHQ8fg1riI9BJPUjydh/C2MTiA/p0Pb1f9Hc88BuCk='
;;
*) return 2 ;;
esac
ip link show dev "$iface" >/dev/null 2>&1 \
&& [[ "$(wg show "$iface" listen-port 2>/dev/null)" == "$port" ]] \
&& [[ "$(wg show "$iface" peers 2>/dev/null)" == "$peer" ]]
}
ensure_configured() {
local iface=$1
configured "$iface" && return 0
echo "$iface is missing or incomplete; reloading networkd configuration" >&2
networkctl reload
sleep 2
networkctl reconfigure "$iface" 2>/dev/null || true
sleep 2
configured "$iface"
}
trigger_handshake() {
case "$1" in
wg-to-wgnet)
ping -q -c 1 -W 1 -I wg-to-wgnet 10.0.0.1 >/dev/null 2>&1 || true
;;
wg-to-skyworks)
ping -q -c 1 -W 1 -I wg-to-skyworks 10.239.0.1 >/dev/null 2>&1 || true
;;
esac
}
handshake_age() {
local iface=$1 now latest
now=$(date +%s)
latest=$(wg show "$iface" latest-handshakes 2>/dev/null | gawk 'NR == 1 { print $2 }')
[[ "$latest" =~ ^[0-9]+$ ]] || latest=0
if (( latest == 0 )); then
echo 2147483647
else
echo $((now - latest))
fi
}
wait_for_handshake() {
local iface=$1 age
for _ in $(seq 1 10); do
trigger_handshake "$iface"
sleep 2
age=$(handshake_age "$iface")
if (( age <= 180 )); then
return 0
fi
done
return 1
}
disabled() {
[[ -e "$state/disabled-$1" ]]
}
health_one() {
local iface=$1 now last=0 age
if disabled "$iface"; then
ip link set dev "$iface" down 2>/dev/null || true
echo "$iface is administratively disabled"
return 0
fi
ensure_configured "$iface" || {
echo "$iface configuration is incomplete after networkd reload" >&2
return 1
}
ip link set dev "$iface" up
age=$(handshake_age "$iface")
if (( age <= 180 )); then
echo "$iface is healthy; latest handshake is ''${age}s old"
return 0
fi
now=$(date +%s)
if [[ -s "$state/last-recovery-$iface" ]]; then
read -r last < "$state/last-recovery-$iface" || last=0
fi
if (( now - last < 300 )); then
echo "$iface handshake is stale and recovery is cooling down" >&2
return 1
fi
echo "$now" > "$state/last-recovery-$iface"
echo "$iface handshake is stale; cycling the link after WAN readiness" >&2
ip link set dev "$iface" down
sleep 1
ip link set dev "$iface" up
if wait_for_handshake "$iface"; then
age=$(handshake_age "$iface")
echo "$iface recovered; latest handshake is ''${age}s old"
return 0
fi
echo "$iface did not handshake after recovery" >&2
return 1
}
status_one() {
local iface=$1 desired=enabled age=unavailable
disabled "$iface" && desired=disabled
if ip link show dev "$iface" >/dev/null 2>&1; then
age=$(handshake_age "$iface")
fi
printf '%s: desired=%s handshake_age=%ss\n' "$iface" "$desired" "$age"
ip -brief link show dev "$iface" 2>/dev/null || true
wg show "$iface" 2>/dev/null || true
}
disable_one() {
local iface=$1
touch "$state/disabled-$iface"
ip link set dev "$iface" down 2>/dev/null || true
echo "$iface disabled; automatic recovery will leave it down"
}
enable_one() {
local iface=$1
rm -f "$state/disabled-$iface" "$state/last-recovery-$iface"
ensure_configured "$iface"
ip link set dev "$iface" up
trigger_handshake "$iface"
echo "$iface enabled"
}
restart_one() {
local iface=$1
if disabled "$iface"; then
echo "$iface is disabled; use 'xlab-wg enable' instead" >&2
return 1
fi
ensure_configured "$iface"
ip link set dev "$iface" down
sleep 1
ip link set dev "$iface" up
trigger_handshake "$iface"
rm -f "$state/last-recovery-$iface"
echo "$iface restarted"
}
run_for_target() {
local action=$1 rc=0
case "$target" in
wgnet|wg-to-wgnet) "$action" wg-to-wgnet || rc=1 ;;
skyworks|wg-to-skyworks) "$action" wg-to-skyworks || rc=1 ;;
all)
"$action" wg-to-wgnet || rc=1
"$action" wg-to-skyworks || rc=1
;;
*)
echo "unknown WireGuard target: $target" >&2
return 2
;;
esac
return "$rc"
}
wait_for_wan() {
for _ in $(seq 1 60); do
if ip -4 route get 166.111.17.108 2>/dev/null | grep -q 'dev wan99.0'; then
return 0
fi
sleep 2
done
echo "campus WAN route did not become ready" >&2
return 1
}
case "$command" in
status)
run_for_target status_one
;;
disable|down)
systemctl stop xlab-wireguard-readiness.service 2>/dev/null || true
run_for_target disable_one
;;
enable|up)
wait_for_wan
run_for_target enable_one
systemctl restart xlab-wireguard-readiness.service
;;
restart)
wait_for_wan
systemctl stop xlab-wireguard-readiness.service 2>/dev/null || true
run_for_target restart_one
systemctl restart xlab-wireguard-readiness.service
;;
health)
wait_for_wan
run_for_target health_one
;;
*)
echo "usage: sudo xlab-wg {status|enable|disable|restart|health} [wgnet|skyworks|all]" >&2
exit 2
;;
esac
'';
};
cnDirectSetScript = ''
set -euo pipefail
umask 027
state=/var/lib/xlab-cn-direct
delegated="$state/delegated-apnic-extended-latest"
url=https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest
v4=$(mktemp --tmpdir="$state" cn-v4.XXXXXX)
v6=$(mktemp --tmpdir="$state" cn-v6.XXXXXX)
batch=$(mktemp --tmpdir="$state" cn-sets.XXXXXX)
candidate=
trap 'rm -f "$v4" "$v6" "$batch" "$candidate"' EXIT
load_data() {
local input=$1 label=$2 bytes v4_count v6_count
bytes=$(wc -c < "$input")
if [ "$bytes" -lt 1000000 ] || [ "$bytes" -gt 67108864 ]; then
echo "Refusing $label APNIC data with unexpected size: $bytes bytes" >&2
return 1
fi
# Treat registry data as untrusted input. Only canonical IPv4 CIDRs are
# emitted, and every count must be a power of two aligned to its start.
if ! gawk -F '|' '
function reject(reason) {
printf "Invalid APNIC IPv4 record at line %d: %s\n", NR, reason > "/dev/stderr"
bad = 1
valid = 0
}
!/^#/ && $2 == "CN" && $3 == "ipv4" && ($7 == "assigned" || $7 == "allocated") {
valid = 1
parts = split($4, octet, ".")
if (parts != 4) reject("address field")
address = 0
for (i = 1; valid && i <= 4; i++) {
if (octet[i] !~ /^[0-9]+$/ || length(octet[i]) > 3 || octet[i] + 0 > 255)
reject("address octet")
else
address = address * 256 + octet[i]
}
if ($5 !~ /^[0-9]+$/ || length($5) > 10) reject("count field")
if (!valid) next
count = $5 + 0
if (count < 1 || count > 4294967296) reject("count range")
units = count
prefix = 32
while (valid && units > 1 && int(units / 2) * 2 == units) {
units = int(units / 2)
prefix--
}
if (valid && units != 1) reject("count is not a power of two")
if (valid && address - int(address / count) * count != 0)
reject("network is not count-aligned")
if (valid)
printf "%d.%d.%d.%d/%d\n", octet[1], octet[2], octet[3], octet[4], prefix
}
END { if (bad) exit 1 }
' "$input" > "$v4"; then
return 1
fi
# The character allow-list prevents command injection; nft --check
# performs the complete IPv6 syntax and network-prefix validation.
if ! gawk -F '|' '
!/^#/ && $2 == "CN" && $3 == "ipv6" && ($7 == "assigned" || $7 == "allocated") {
if (length($4) > 39 || $4 !~ /^[0-9A-Fa-f:]+$/ || index($4, ":") == 0 ||
$5 !~ /^[0-9]+$/ || length($5) > 3 || $5 + 0 > 128) {
printf "Invalid APNIC IPv6 record at line %d\n", NR > "/dev/stderr"
bad = 1
next
}
printf "%s/%d\n", tolower($4), $5 + 0
}
END { if (bad) exit 1 }
' "$input" > "$v6"; then
return 1
fi
v4_count=$(wc -l < "$v4")
v6_count=$(wc -l < "$v6")
if [ "$v4_count" -lt 8000 ] || [ "$v6_count" -lt 1500 ]; then
echo "Refusing undersized $label APNIC sets: v4=$v4_count v6=$v6_count" >&2
return 1
fi
{
echo 'flush set inet xlab_pbr cn_direct_v4'
echo 'flush set inet xlab_pbr cn_direct_v6'
gawk 'BEGIN { printf "add element inet xlab_pbr cn_direct_v4 { " } { printf "%s%s", sep, $0; sep = ", " } END { print " }" }' "$v4"
gawk 'BEGIN { printf "add element inet xlab_pbr cn_direct_v6 { " } { printf "%s%s", sep, $0; sep = ", " } END { print " }" }' "$v6"
} > "$batch"
# Both commands are transactions. A parse/check/load failure therefore
# retains the currently loaded sets and the last-known-good cache.
nft -c -f "$batch"
nft -f "$batch"
echo "Loaded $label APNIC-CN direct sets: v4=$v4_count v6=$v6_count"
}
cache_valid=false
if [ -s "$delegated" ]; then
if load_data "$delegated" cached; then
cache_valid=true
else
echo "Cached APNIC data is invalid; attempting a fresh download" >&2
fi
fi
refresh=true
if $cache_valid && [ -n "$(find "$delegated" -mmin -1440 -print -quit)" ]; then
refresh=false
fi
if $refresh; then
candidate=$(mktemp --tmpdir="$state" delegated.XXXXXX)
if curl --fail --silent --show-error --location \
--proto '=https' --proto-redir '=https' --max-filesize 67108864 \
--retry 3 --retry-delay 2 --connect-timeout 10 --max-time 180 \
"$url" --output "$candidate" && load_data "$candidate" downloaded; then
# Promotion happens only after complete schema, size, count, nft
# syntax, and live transaction validation.
mv -f "$candidate" "$delegated"
candidate=
cache_valid=true
else
echo "APNIC refresh rejected; retaining the last-known-good cache" >&2
fi
fi
if ! $cache_valid; then
echo "No validated APNIC data; leaving CN sets empty" >&2
exit 1
fi
'';
in
{
networking = {
useNetworkd = true;
useDHCP = false;
nftables = {
enable = true;
ruleset = ''
table ip filter {
set bogons_v4 {
type ipv4_addr
flags interval
elements = {
0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8,
169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24,
192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24,
224.0.0.0/4, 240.0.0.0/4
}
}
chain prerouting {
type filter hook prerouting priority raw; policy accept;
iifname "wan99.0" ip saddr @bogons_v4 drop
}
chain forward {
type filter hook forward priority filter; policy drop;
iifname "bond.lan254" ip saddr != 10.253.254.0/24 counter drop comment "Drop spoofed LAN source"
ct state established,related accept
iifname "bond.lan254" accept
iifname "wg-to-wgnet" ip saddr 10.0.0.0/8 ip daddr 10.253.254.0/24 accept comment "Internal WG clients to xlab LAN"
}
}
table inet input_filter {
chain input {
type filter hook input priority filter; policy drop;
ct state invalid drop
# Reject forged LAN/management sources before service ACLs. DHCPv4
# legitimately starts at 0.0.0.0. IPv6 DAD probes use the
# unspecified source, while normal control traffic is link-local.
iifname "bond.lan254" ip saddr != 10.253.254.0/24 ip saddr != 0.0.0.0 drop
iifname "bond.lan254" ip6 saddr :: ip6 nexthdr ipv6-icmp accept comment "Allow IPv6 duplicate-address detection"
iifname "bond.lan254" ip6 saddr != fd99:23eb:1682:1::/64 ip6 saddr != fe80::/10 drop
iifname "bond.mgmt" ip saddr != 192.168.1.0/24 drop
ct state established,related accept
iif lo accept
# The router only exposes DHCP and SSH on trusted access networks.
iifname "bond.lan254" udp dport 67 accept
iifname "bond.lan254" meta l4proto { tcp, udp } th dport 53 accept comment "LAN DNS resolver"
iifname { "bond.lan254", "bond.mgmt" } tcp dport 22 accept
iifname "wg-to-wgnet" ip saddr 10.0.0.0/8 tcp dport 22 accept
iifname "wg-to-wgnet" ip6 saddr fd99:23eb:1682::/48 tcp dport 22 accept
iifname "wg-to-skyworks" tcp dport 22 accept
# WAN — DHCP client replies (v4 + v6)
iifname "wan99.0" udp sport 67 udp dport 68 accept
iifname "wan99.0" udp sport 547 udp dport 546 accept
iifname "wan99.0" udp dport { 46961, 51998 } accept comment "WireGuard handshakes"
iifname "wan99.0" ip saddr { 166.111.17.81, 166.111.17.108 } tcp dport 22 accept comment "Break-glass SSH from server3 or main"
# ICMP/ICMPv6 for path MTU discovery and diagnostics
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
}
}
table ip6 filter {
chain forward {
type filter hook forward priority filter; policy drop;
iifname "bond.lan254" ip6 saddr != fd99:23eb:1682:1::/64 counter drop comment "Drop spoofed LAN v6 source"
ct state established,related accept
iifname "bond.lan254" accept
iifname "wg-to-wgnet" ip6 saddr fd99:23eb:1682::/48 ip6 daddr fd99:23eb:1682:1::/64 accept comment "Internal WG clients to xlab LAN v6"
}
}
# Destination policy for xlab clients. APNIC-CN sets are populated by
# xlab-cn-direct-sets.service; an empty set safely preserves the old
# behavior (traffic continues through wg-to-wgnet and is split at .1).
table inet xlab_pbr {
set cn_direct_v4 { type ipv4_addr; flags interval; }
set cn_direct_v6 { type ipv6_addr; flags interval; }
# These campus and Alibaba AS45102 ranges need the same failover
# semantics as the APNIC-CN data. Marking them selects main while a
# WAN default exists, then falls through to table 1002 when it does
# not. A throw route would skip table 1002 and blackhole an outage.
set regional_direct_v4 {
type ipv4_addr
flags interval
elements = {
166.111.0.0/16, 101.5.0.0/16, 101.6.0.0/16,
59.66.0.0/16, 183.172.0.0/16, 183.173.0.0/16,
47.246.0.0/16, 43.109.0.0/16, 163.181.0.0/16,
139.95.0.0/16
}
}
set regional_direct_v6 {
type ipv6_addr
flags interval
elements = {
2402:f000::/32, 2001:da8::/32, 2001:250::/35
}
}
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
iifname "bond.lan254" ip daddr @cn_direct_v4 counter meta mark set 0x10 comment "APNIC-CN direct via xlab WAN"
iifname "bond.lan254" ip6 daddr @cn_direct_v6 counter meta mark set 0x10 comment "APNIC-CN v6 direct via xlab WAN"
iifname "bond.lan254" ip daddr @regional_direct_v4 counter meta mark set 0x10 comment "Campus and Alibaba direct via xlab WAN"
iifname "bond.lan254" ip6 daddr @regional_direct_v6 counter meta mark set 0x10 comment "CERNET v6 direct via xlab WAN"
}
}
table inet nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "wan99.0" masquerade
oifname "bond.mgmt" masquerade
}
}
table inet mangle {
chain forward_mss {
type filter hook forward priority mangle; policy accept;
# MSS clamping for forwarded TCP, both directions. Old rule
# `& (syn|ack) == syn` ONLY matched plain SYN — SYN-ACK from
# the server came back unclamped, full-MTU segments overflowed
# the WG path, and large pages (YouTube, Microsoft) silently
# truncated. Use `& (syn|rst) == syn` so SYN-ACK still matches
# (it has SYN+ACK), only RST packets are excluded.
#
# Outbound SYN/SYN-ACK uses the tunnel route MTU. For packets
# arriving from WireGuard the egress LAN route is 1500, so clamp
# those explicitly to the 1420-byte tunnel ceilings as well.
oifname { "wg-to-wgnet", "wg-to-skyworks" } tcp flags & (syn|rst) == syn tcp option maxseg size set rt mtu
iifname { "wg-to-wgnet", "wg-to-skyworks" } meta nfproto ipv4 tcp flags & (syn|rst) == syn tcp option maxseg size set 1380
iifname { "wg-to-wgnet", "wg-to-skyworks" } meta nfproto ipv6 tcp flags & (syn|rst) == syn tcp option maxseg size set 1360
}
chain postrouting {
type filter hook postrouting priority filter; policy accept;
}
}
'';
};
firewall.enable = false; # Using nftables directly
};
# Custom routing table names
environment.etc."iproute2/rt_tables.d/skyworks.conf".text = ''
1000 freedom
1001 freedom-extra
1002 freedom-wgnet
1003 wg-to-skyworks
'';
# ===========================================================================
# NETDEVS
# ===========================================================================
systemd.network.netdevs = {
"10-bond" = {
netdevConfig = {
Name = "bond";
Kind = "bond";
Description = "Main aggregated interface";
};
bondConfig = {
Mode = "balance-xor";
MIIMonitorSec = "100ms";
TransmitHashPolicy = "layer3+4";
};
};
"20-bond-lan254" = {
netdevConfig = {
Name = "bond.lan254";
Kind = "vlan";
};
vlanConfig.Id = 10;
};
"20-bond-wan99" = {
netdevConfig = {
Name = "bond.wan99";
Kind = "vlan";
};
vlanConfig.Id = 99;
};
"20-bond-mgmt" = {
netdevConfig = {
Name = "bond.mgmt";
Kind = "vlan";
};
vlanConfig.Id = 1;
};
"30-wan99-0" = {
netdevConfig = {
Name = "wan99.0";
Kind = "macvlan";
MACAddress = "90:e2:ba:54:26:90";
};
macvlanConfig.Mode = "bridge";
};
"40-wg-to-wgnet" = {
netdevConfig = {
Name = "wg-to-wgnet";
Kind = "wireguard";
};
wireguardConfig = {
PrivateKeyFile = config.age.secrets.xlab-wg-wgnet.path;
ListenPort = 51998;
};
wireguardPeers = [
{
PublicKey = "H+PAPw+1MsE50Of4VMMPzMbzGG731CkNrIgaXbxcFwk=";
PresharedKeyFile = config.age.secrets.xlab-wg-wgnet-psk.path;
AllowedIPs = [ "0.0.0.0/0" "::/0" ];
Endpoint = "166.111.17.108:51820";
PersistentKeepalive = 16;
}
];
};
"40-wg-to-skyworks" = {
netdevConfig = {
Name = "wg-to-skyworks";
Kind = "wireguard";
};
wireguardConfig = {
PrivateKeyFile = config.age.secrets.xlab-wg-skyworks.path;
ListenPort = 46961;
};
wireguardPeers = [
{
PublicKey = "yyHQ8fg1riI9BJPUjydh/C2MTiA/p0Pb1f9Hc88BuCk=";
AllowedIPs = [
"10.0.2.144/32"
"10.74.0.0/16"
"10.239.0.0/16"
"fd5b:8249:afe:cb9a::/64"
];
# Avoid a boot-time dependency cycle: this tunnel previously waited
# indefinitely for DNS that was itself routed through wg-to-wgnet.
Endpoint = "166.111.17.81:16777";
PersistentKeepalive = 20;
}
];
};
};
# ===========================================================================
# NETWORKS
# ===========================================================================
systemd.network.networks = {
# Bond slaves
"10-bond-slaves" = {
matchConfig.Name = "enp3s0f0 enp3s0f1";
networkConfig = {
Bond = "bond";
IPv6AcceptRA = false;
LLDP = false;
};
};
# Bond master
"20-bond" = {
matchConfig.Name = "bond";
linkConfig.RequiredForOnline = "no";
networkConfig = {
VLAN = [ "bond.mgmt" "bond.wan99" "bond.lan254" ];
DHCP = "no";
IPv6AcceptRA = false;
};
};
# LAN - 10.253.254.0/24
"30-bond-lan254" = {
matchConfig.Name = "bond.lan254";
networkConfig = {
DHCP = "no";
Address = [
"10.253.254.1/24"
"fd99:23eb:1682:1::1/64"
];
IPv6AcceptRA = false;
};
routes = [
# Keep the local management VLAN outside the foreign tunnel.
{ Destination = "192.168.1.0/24"; Type = "throw"; Table = 1002; }
];
routingPolicyRules = [
# nft table xlab_pbr marks APNIC-CN destinations for the local WAN.
# If the WAN default disappears, lookup falls through to the existing
# source-based WG rule below, preserving connectivity through .1.
{
FirewallMark = "0x10";
Table = 254;
Priority = 90;
Family = "ipv4";
}
{
FirewallMark = "0x10";
Table = 254;
Priority = 90;
Family = "ipv6";
}
# LAN IPv4 → suppress default route in main table, fall through to freedom-wgnet
{
From = "10.253.254.0/24";
SuppressPrefixLength = 0;
Family = "ipv4";
Priority = 100;
}
# LAN IPv6 → same
{
From = "fd99:23eb:1682:1::/64";
SuppressPrefixLength = 0;
Family = "ipv6";
Priority = 100;
}
# LAN IPv4 → WireGuard routing table
{
From = "10.253.254.0/24";
Table = 1002;
Priority = 20000;
Family = "ipv4";
}
# LAN IPv6 → WireGuard routing table
{
From = "fd99:23eb:1682:1::/64";
Table = 1002;
Priority = 20000;
Family = "ipv6";
}
];
};
# Management - 192.168.1.0/24
"30-bond-mgmt" = {
matchConfig.Name = "bond.mgmt";
networkConfig.DHCP = "no";
addresses = [{ Address = "192.168.1.13/24"; }];
};
# WAN VLAN trunk
"30-bond-wan99" = {
matchConfig.Name = "bond.wan99";
linkConfig.RequiredForOnline = "no";
networkConfig = {
MACVLAN = [ "wan99.0" ];
DHCP = "no";
IPv6AcceptRA = false;
};
};
# WAN uplink (DHCP)
"40-wan99-0" = {
matchConfig.Name = "wan99.0";
networkConfig = {
NTP = [
"ntp.tuna.tsinghua.edu.cn"
"ntp1.aliyun.com"
"ntp.ntsc.ac.cn"
];
# Don't set link-specific DNS on the WAN. Tsinghua's resolvers
# (166.111.8.28/29) are subject to GFW DNS poisoning, and link-DNS
# would override the global services.resolved policy. Global DNS
# uses main's IPv4 and IPv6 MosDNS listeners. Numeric WireGuard
# endpoints keep tunnel bootstrap independent of DNS.
DHCP = "yes";
};
dhcpV4Config = {
RouteMTUBytes = "1500";
DUIDType = "link-layer-time";
UseDNS = false;
};
dhcpV6Config = {
DUIDType = "link-layer-time";
UseDNS = false;
};
ipv6AcceptRAConfig = {
UseAutonomousPrefix = false;
UseDNS = false;
};
};
# WireGuard: wg-to-wgnet (main tunnel for "freedom" routing)
"50-wg-to-wgnet" = {
matchConfig.Name = "wg-to-wgnet";
linkConfig = {
ActivationPolicy = "manual";
RequiredForOnline = "no";
};
addresses = [
{ Address = "10.253.1.37/32"; }
{ Address = "fd99:23eb:1682:fd::128/128"; }
];
routes = [
{ Destination = "0.0.0.0/0"; Scope = "link"; Table = 1002; }
{ Destination = "::/0"; Table = 1002; }
# Internal ULA via tunnel (main table) so gateway + LAN can reach 10.0.0.1's IPv6
{ Destination = "fd99:23eb:1682::/48"; }
# Internal networks are reached through the main gateway tunnel.
{ Destination = "10.0.0.0/16"; Scope = "link"; }
{ Destination = "10.5.0.0/24"; Scope = "link"; }
{ Destination = "10.253.0.0/24"; Scope = "link"; }
{ Destination = "10.254.0.0/24"; Scope = "link"; }
];
};
# WireGuard: wg-to-skyworks
"50-wg-to-skyworks" = {
matchConfig.Name = "wg-to-skyworks";
linkConfig = {
ActivationPolicy = "manual";
RequiredForOnline = "no";
};
addresses = [
{ Address = "10.239.0.9/16"; }
{ Address = "fd5b:8249:afe:cb9a::9/64"; }
];
routes = [
{ Destination = "10.0.2.144/32"; }
{ Destination = "10.74.0.0/16"; }
];
};
};
environment.systemPackages = [ xlabWg ];
# Networkd creates and configures the interfaces but leaves their
# administrative state alone. This unit waits for the campus route, raises
# enabled tunnels, validates real handshakes, and cycles only stale peers.
# Persistent disable markers let operators intentionally keep either path
# down without racing an automatic recovery timer.
systemd.services.xlab-wireguard-readiness = {
description = "Recover and verify xlab WireGuard handshakes";
wants = [ "systemd-networkd.service" "nftables.service" ];
after = [ "systemd-networkd.service" "nftables.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${xlabWg}/bin/xlab-wg health all";
StateDirectory = "xlab-wireguard";
StateDirectoryMode = "0750";
CapabilityBoundingSet = [ "CAP_NET_ADMIN" "CAP_NET_RAW" ];
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectKernelModules = true;
ProtectSystem = "strict";
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
};
};
systemd.timers.xlab-wireguard-readiness = {
description = "Periodically verify xlab WireGuard netdevs";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "30s";
OnUnitInactiveSec = "2min";
AccuracySec = "15s";
RandomizedDelaySec = "10s";
Persistent = true;
};
};
# Keep a local APNIC-CN destination set so xlab clients use xlab's own
# campus uplink for domestic traffic instead of hairpinning through .1.
# The cache is loaded before any network refresh, then refreshed daily.
# Propagated reloads repopulate the dynamic sets after nftables flushes them.
systemd.services.nftables.unitConfig.PropagatesReloadTo = [
"xlab-cn-direct-sets.service"
];
systemd.services.xlab-cn-direct-sets = {
description = "Populate nftables APNIC-CN direct-routing sets";
requires = [ "nftables.service" ];
after = [ "nftables.service" ];
partOf = [ "nftables.service" ];
wantedBy = [ "multi-user.target" "nftables.service" ];
path = with pkgs; [ coreutils curl findutils gawk nftables ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "xlab-cn-direct";
StateDirectoryMode = "0750";
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
NoNewPrivileges = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
RestrictSUIDSGID = true;
};
script = cnDirectSetScript;
reload = cnDirectSetScript;
};
# Timers start services rather than reload active oneshot services, so use a
# small trigger unit to run the loader's ExecReload (or restart after error).
systemd.services.xlab-cn-direct-sets-refresh = {
description = "Trigger APNIC-CN direct-routing set refresh";
after = [ "nftables.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.systemd}/bin/systemctl reload-or-restart xlab-cn-direct-sets.service";
};
};
systemd.timers.xlab-cn-direct-sets-refresh = {
description = "Refresh nftables APNIC-CN direct-routing sets";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5min";
OnUnitInactiveSec = "1h";
RandomizedDelaySec = "5min";
Persistent = true;
};
};
# Verify the complete client path independently of the loader. This catches
# stale registry data and policy loss after either nftables or networkd is
# reloaded, even when the population unit itself still reports success.
systemd.services.xlab-cn-direct-health = {
description = "Verify xlab CN-direct policy and foreign-tunnel fallback";
wants = [ "xlab-cn-direct-sets.service" ];
after = [
"nftables.service"
"systemd-networkd.service"
"xlab-cn-direct-sets.service"
];
path = with pkgs; [ coreutils findutils gnugrep iproute2 jq nftables ];
serviceConfig = {
Type = "oneshot";
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
};
script = ''
set -euo pipefail
cache=/var/lib/xlab-cn-direct/delegated-apnic-extended-latest
fail() {
echo "CN-direct health check: $*" >&2
exit 1
}
[[ -s "$cache" ]] || fail "validated APNIC cache is missing"
[[ -n "$(find "$cache" -mmin -2880 -print -quit)" ]] || fail "APNIC cache is older than 48 hours"
set_count() {
nft -j list set inet xlab_pbr "$1" \
| jq '[.nftables[] | .set?.elem? // empty | .[]] | length'
}
v4_count=$(set_count cn_direct_v4)
v6_count=$(set_count cn_direct_v6)
regional_v4_count=$(set_count regional_direct_v4)
regional_v6_count=$(set_count regional_direct_v6)
((v4_count >= 8000)) || fail "IPv4 set is undersized: $v4_count"
((v6_count >= 1500)) || fail "IPv6 set is undersized: $v6_count"
((regional_v4_count >= 10)) || fail "regional IPv4 set is undersized: $regional_v4_count"
((regional_v6_count >= 3)) || fail "regional IPv6 set is undersized: $regional_v6_count"
ip -4 rule show | grep -Eq '^90:.*fwmark 0x10.*lookup main' \
|| fail "IPv4 mark rule is missing"
ip -6 rule show | grep -Eq '^90:.*fwmark 0x10.*lookup main' \
|| fail "IPv6 mark rule is missing"
ip -4 route get 223.5.5.5 from 10.253.254.2 iif bond.lan254 mark 0x10 \
| grep -q 'dev wan99.0' || fail "marked CN IPv4 does not use the campus WAN"
ip -6 route get 2400:3200::1 from fd99:23eb:1682:1::2 iif bond.lan254 mark 0x10 \
| grep -q 'dev wan99.0' || fail "marked CN IPv6 does not use the campus WAN"
ip -4 route get 47.246.1.1 from 10.253.254.2 iif bond.lan254 mark 0x10 \
| grep -q 'dev wan99.0' || fail "Alibaba exception does not use the campus WAN"
ip -4 route get 1.1.1.1 from 10.253.254.2 iif bond.lan254 \
| grep -q 'dev wg-to-wgnet' || fail "foreign IPv4 does not use WireGuard"
ip -6 route get 2606:4700:4700::1111 from fd99:23eb:1682:1::2 iif bond.lan254 \
| grep -q 'dev wg-to-wgnet' || fail "foreign IPv6 does not use WireGuard"
echo "CN-direct healthy: APNIC v4=$v4_count v6=$v6_count; regional v4=$regional_v4_count v6=$regional_v6_count"
'';
};
systemd.timers.xlab-cn-direct-health = {
description = "Periodically verify xlab client routing policy";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "10min";
OnUnitInactiveSec = "15min";
RandomizedDelaySec = "2min";
Persistent = true;
};
};
# ===========================================================================
# DNS RESOLUTION
# ===========================================================================
# Route DNS through the network's local mosdns at 10.0.0.1 so this host
# inherits CN-aware split routing and the analytics blocking policy.
# Do not add a mainland resolver as a same-priority "secondary": resolved
# load-balances global servers rather than treating their order as failover,
# so polluted foreign answers intermittently reach clients. Both addresses
# below terminate on main's filtered MosDNS over the primary WireGuard path.
# WireGuard endpoints are numeric, so tunnel recovery has no DNS dependency.
services.resolved = {
enable = true;
settings.Resolve = {
DNS = [ "10.0.0.1" "fd99:23eb:1682::1" ];
FallbackDNS = "";
DNSStubListenerExtra = [ "10.253.254.1" "fd99:23eb:1682:1::1" ];
LLMNR = false;
MulticastDNS = false;
};
};
# ===========================================================================
# AGENIX SECRETS
# ===========================================================================
age.secrets = {
xlab-wg-wgnet = {
file = ../../secrets/xlab-wg-wgnet.age;
owner = "systemd-network";
mode = "0400";
};
xlab-wg-wgnet-psk = {
file = ../../secrets/xlab-wg-wgnet-psk.age;
owner = "systemd-network";
mode = "0400";
};
xlab-wg-skyworks = {
file = ../../secrets/xlab-wg-skyworks.age;
owner = "systemd-network";
mode = "0400";
};
};
}