# astrnet.nix — AstrNet stub peer "michaotic". Generated by astrnet-join.sh. # # What this host is to the mesh: identity 10.0.0.9 / 2a05:f480:2400:1cf7::9, # ASN 4200000009, two WireGuard legs that THIS host dials: # wg-mic-der -> alderamin (Helsinki) primary, pair 38 # wg-mic-bd8 -> bd8816 (Moscow) backup, pair 39 (paths come prepended) # It announces only its identity and accepts only mesh address space; it # never becomes transit and never takes a default route from the mesh. # # All values are assigned by the AstrNet admin. Do not renumber anything # here without them: the far ends are configured to match exactly. { config, lib, pkgs, ... }: let keyFile = "/var/lib/astrnet/wg.key"; id4 = "10.0.0.9"; id6 = "2a05:f480:2400:1cf7::9"; mkLeg = { listenPort, ips, publicKey, endpoint }: { inherit ips listenPort; privateKeyFile = keyFile; mtu = 1380; # Routing is BGP's job. allowedIPs 0/0 only PERMITS any source through # the tunnel; with routes off, it installs nothing. Turning routes on # would hijack this host's default route. allowedIPsAsRoutes = false; peers = [{ inherit publicKey endpoint; allowedIPs = [ "0.0.0.0/0" "::/0" ]; persistentKeepalive = 25; }]; }; in { networking.wireguard.interfaces = { # The fe80:: link-locals are required: without one FRR's IPv6 BGP # next-hop selection fails and the v6 session never leaves Connect. wg-mic-der = mkLeg { listenPort = 52038; ips = [ "10.99.0.154/30" "fd53:4023:2326:99::9a/126" "fe80::2/64" ]; publicKey = "a4FyXuoI16TxZxv407lSzJV2YFJdq6FgRAtGu+oMzEY="; endpoint = "31.77.105.157:52038"; }; wg-mic-bd8 = mkLeg { listenPort = 52039; ips = [ "10.99.0.158/30" "fd53:4023:2326:99::9e/126" "fe80::2/64" ]; publicKey = "QIvX+avvJqaHxuksGWb6Hx8fv7R6VI5wpaGqLme6Fgw="; endpoint = "195.133.60.217:52039"; }; }; # Heather's public firewall is disabled (the cloud SG is the WAN barrier). # Explicitly restrict traffic FROM AstrNet before normal Docker/host rules: # BGP from the two expected neighbors, ping to this mesh ID; no transit. # Existing outbound sessions may receive established replies. systemd.services.astrnet-guard = { description = "Restrict AstrNet tunnel ingress"; wantedBy = [ "multi-user.target" ]; requiredBy = [ "wireguard-wg-mic-der.service" "wireguard-wg-mic-bd8.service" ]; before = [ "wireguard-wg-mic-der.service" "wireguard-wg-mic-bd8.service" ]; path = [ pkgs.nftables ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' nft list table inet astrnet_guard >/dev/null 2>&1 && nft delete table inet astrnet_guard nft -f - <<'RULES' table inet astrnet_guard { chain input { type filter hook input priority -150; policy accept; iifname { "wg-mic-der", "wg-mic-bd8" } ct state established,related accept iifname "wg-mic-der" ip saddr 10.99.0.153 tcp dport 179 accept iifname "wg-mic-bd8" ip saddr 10.99.0.157 tcp dport 179 accept iifname "wg-mic-der" ip6 saddr fd53:4023:2326:99::99 tcp dport 179 accept iifname "wg-mic-bd8" ip6 saddr fd53:4023:2326:99::9d tcp dport 179 accept iifname { "wg-mic-der", "wg-mic-bd8" } ip daddr 10.0.0.9 icmp type echo-request accept iifname { "wg-mic-der", "wg-mic-bd8" } drop } chain forward { type filter hook forward priority -150; policy accept; iifname { "wg-mic-der", "wg-mic-bd8" } drop } } RULES ''; preStop = "nft delete table inet astrnet_guard || true"; }; # Mesh identity on a dummy interface. It must exist before FRR installs # routes, because those routes are pinned to it as their source address. systemd.services.astrnet-mesh0 = { description = "AstrNet mesh identity (mesh0)"; wantedBy = [ "multi-user.target" ]; before = [ "frr.service" ]; requiredBy = [ "frr.service" ]; path = [ pkgs.iproute2 ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' ip link show mesh0 >/dev/null 2>&1 || ip link add mesh0 type dummy ip link set mesh0 up ip addr replace ${id4}/32 dev mesh0 ip -6 addr replace ${id6}/128 dev mesh0 ''; preStop = "ip link del mesh0 || true"; }; # BGP only inside the tunnels, never on the public interface. networking.firewall.interfaces."wg-mic-der".allowedTCPPorts = [ 179 ]; networking.firewall.interfaces."wg-mic-bd8".allowedTCPPorts = [ 179 ]; # Two legs means replies can arrive on the leg that is not the best route # back. Strict reverse-path filtering drops those; loose keeps the spoof # check (a route must exist) without caring which leg it points at. networking.firewall.checkReversePath = lib.mkDefault "loose"; networking.networkmanager.unmanaged = lib.mkIf config.networking.networkmanager.enable [ "interface-name:wg-mic-*" "interface-name:mesh0" ]; services.frr = { bgpd.enable = true; config = '' ip prefix-list OWN-V4 seq 5 permit ${id4}/32 ipv6 prefix-list OWN-V6 seq 5 permit ${id6}/128 ! ! Accept mesh space only: never a default, never public prefixes. The ! mesh carries anycast /32s for public services (DNS resolvers etc.); ! accepting those would steer this host's own traffic into AstrNet. ! It also keeps the tunnel endpoints themselves out of the tunnels. ! Narrow to AstrNet address space; do not accept Yandex Cloud 10.130/24. ip prefix-list MESH-V4 seq 5 permit 10.0.0.0/24 ge 24 le 32 ip prefix-list MESH-V4 seq 10 permit 10.0.1.0/24 ge 24 le 32 ip prefix-list MESH-V4 seq 15 permit 10.0.2.0/24 ge 24 le 32 ip prefix-list MESH-V4 seq 20 permit 10.99.0.0/22 ge 22 le 32 ipv6 prefix-list MESH-V6 seq 5 permit 2a05:f480:2400:1cf7::/64 le 128 ipv6 prefix-list MESH-V6 seq 10 permit fd53:4023:2326::/48 le 128 ! route-map MESH-IN-V4 permit 10 match ip address prefix-list MESH-V4 exit route-map MESH-IN-V6 permit 10 match ipv6 address prefix-list MESH-V6 exit route-map MESH-OUT-V4 permit 10 match ip address prefix-list OWN-V4 exit route-map MESH-OUT-V6 permit 10 match ipv6 address prefix-list OWN-V6 exit ! ! Source mesh traffic from the identity, not from whichever tunnel /30 ! it leaves by; otherwise replies break whenever the path changes. route-map SET-MESH-SRC permit 10 set src ${id4} exit route-map SET-MESH-SRC-V6 permit 10 set src ${id6} exit ip protocol bgp route-map SET-MESH-SRC ipv6 protocol bgp route-map SET-MESH-SRC-V6 ! router bgp 4200000009 bgp router-id ${id4} no bgp ebgp-requires-policy no bgp default ipv4-unicast bgp bestpath as-path multipath-relax neighbor 10.99.0.153 remote-as 4200000012 neighbor 10.99.0.153 description alderamin-v4-wg-mic-der neighbor 10.99.0.153 update-source 10.99.0.154 neighbor 10.99.0.157 remote-as 4200000005 neighbor 10.99.0.157 description bd8816-v4-wg-mic-bd8 neighbor 10.99.0.157 update-source 10.99.0.158 neighbor fd53:4023:2326:99::99 remote-as 4200000012 neighbor fd53:4023:2326:99::99 description alderamin-v6-wg-mic-der neighbor fd53:4023:2326:99::99 interface wg-mic-der neighbor fd53:4023:2326:99::99 update-source fd53:4023:2326:99::9a neighbor fd53:4023:2326:99::9d remote-as 4200000005 neighbor fd53:4023:2326:99::9d description bd8816-v6-wg-mic-bd8 neighbor fd53:4023:2326:99::9d interface wg-mic-bd8 neighbor fd53:4023:2326:99::9d update-source fd53:4023:2326:99::9e ! address-family ipv4 unicast network ${id4}/32 neighbor 10.99.0.153 activate neighbor 10.99.0.153 route-map MESH-IN-V4 in neighbor 10.99.0.153 route-map MESH-OUT-V4 out neighbor 10.99.0.157 activate neighbor 10.99.0.157 route-map MESH-IN-V4 in neighbor 10.99.0.157 route-map MESH-OUT-V4 out exit-address-family ! address-family ipv6 unicast network ${id6}/128 neighbor fd53:4023:2326:99::99 activate neighbor fd53:4023:2326:99::99 route-map MESH-IN-V6 in neighbor fd53:4023:2326:99::99 route-map MESH-OUT-V6 out neighbor fd53:4023:2326:99::9d activate neighbor fd53:4023:2326:99::9d route-map MESH-IN-V6 in neighbor fd53:4023:2326:99::9d route-map MESH-OUT-V6 out exit-address-family exit ''; }; }