# Home network configuration # (2.5GbE clients) # | | # +----------+ +----------+ # | | | | | | (1GbE clients) # | | | | | +|-|-|-|-|+ # +---+----+ +-+-+-+-+-+ |5 4 3 2 1| # |upstream| | 1 2 3 4 | |TL-SG105 | # +--------+ | shinobu | +---------+ # +---------+ # # It consists of shinobu as a router (this configuration), # connected to a TP-LINK TL-SG105E “smart managed” (i.e., it can do VLANs) 5-port switch. # The upstream comes (for now) from a PŸUR “WLAN-Kabelbox” (Compal CH7467CE). # Sadly, I could not enable bridge mode on it, so the packets now go through (at least) four layers of NAT: # device → NAT on shinobu (→ NAT on plastic router → PŸUR CGNAT) → NAT on VPN # # Because the switch only supports GbE, # the two clients I currently have with support for 2.5GbE are connected # directly to the two remaining network interfaces on shinobu. # Once I have more devices with support for 2.5GbE # or I find a good deal on a matching switch, # I will change this. # # Wireless is configured by providing the whole hostapd configuration file as a secret. # Once nixpkgs PR 222536 is merged, I will migrate to using the NixOS module. # Thanks to Intel’s wisdom, it’s not possible to use 5GHz in AP mode. { config, lib, pkgs, ... }: let domain = "home.sbruder.de"; vpnBypassFwMark = 10000; in { sops.secrets.wg-upstream-private-key = { owner = config.users.users.systemd-network.name; sopsFile = ../secrets.yaml; }; sops.secrets.hostapd-config = { sopsFile = ../secrets.yaml; }; boot.kernel.sysctl = { "net.ipv4.conf.all.forwarding" = true; "net.ipv6.conf.all.forwarding" = true; }; networking = { # networkd handles this useDHCP = false; nftables = { enable = true; ruleset = '' define NAT_LAN_IFACES = { "br-lan" } define NAT_WAN_IFACES = { "wg-upstream" } define PHYSICAL_WAN = "enp1s0" define MASQUERADE_IFACES = { $NAT_WAN_IFACES, $PHYSICAL_WAN } define VUEKO_V4 = 168.119.176.53 define VUEKO_V6 = 2a01:4f8:c012:2f4::1 define VUEKO_PORT = 51820 define WG_UPSTREAM_ENDPOINT = ${lib.elemAt (lib.splitString ":" (lib.elemAt config.systemd.network.netdevs.wg-upstream.wireguardPeers 0).wireguardPeerConfig.Endpoint) 0} define PLASTIC_ROUTER_V4 = 192.168.0.1 define VPN_BYPASS_MARK = ${toString vpnBypassFwMark} table inet filter { chain forward { type filter hook forward priority filter; policy drop # Use MSS clamping # to avoid too large packets from client on the lan # not going through the tunnel. iifname wg-upstream tcp flags syn / syn,rst tcp option maxseg size set rt mtu oifname wg-upstream tcp flags syn / syn,rst tcp option maxseg size set rt mtu iifname $NAT_LAN_IFACES oifname $NAT_WAN_IFACES counter accept iifname $NAT_WAN_IFACES oifname $NAT_LAN_IFACES ct state established,related counter accept # accept responses on physical wan iifname $PHYSICAL_WAN oifname $NAT_LAN_IFACES ct state established,related counter accept # allow selected destinations via physical wan # plastic router iifname $NAT_LAN_IFACES oifname $PHYSICAL_WAN ip daddr $PLASTIC_ROUTER_V4 counter accept # all destinations configured via policy based routing oifname $PHYSICAL_WAN mark $VPN_BYPASS_MARK counter accept } } table inet nat { chain prerouting { type nat hook prerouting priority filter; policy accept } chain postrouting { type nat hook postrouting priority filter; policy accept oifname $MASQUERADE_IFACES masquerade } } # Bypass VPN by setting mark. # This acts in two places that are handled separatly by nftables: # Packets from the local host (output hook) and forwared packets (prerouting hook). # To simplify the handling, # there is a single chain that handles both, # which is jumped to from the specific chains. table inet vpn-bypass { # This must be of type route, otherwise no route lookup will be performed chain output { type route hook output priority mangle jump common } # This does not need to be of type route chain prerouting { type filter hook prerouting priority mangle jump common } chain common { ip daddr $VUEKO_V4 udp dport $VUEKO_PORT mark set $VPN_BYPASS_MARK counter ip6 daddr $VUEKO_V6 udp dport $VUEKO_PORT mark set $VPN_BYPASS_MARK counter } } # Only allow select connections from and to (physical) wan, # overriding NixOS firewall in some cases. table inet restrict-wan { # Priorities must be higher than filter (0), # which the NixOS firewall uses. chain input { type filter hook input priority -50; policy accept # accept responses iifname $PHYSICAL_WAN ct state established,related counter accept # accept icmpv6 iifname $PHYSICAL_WAN icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept # drop everything else iifname $PHYSICAL_WAN counter drop } # This handles all packets (local and forwarded) chain postrouting { type filter hook postrouting priority 0; policy accept # accept connections to plastic router oifname $PHYSICAL_WAN ip daddr $PLASTIC_ROUTER_V4 counter accept # accept icmpv6 oifname $PHYSICAL_WAN icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept # accept connections to selected endpoints # VPN (wg-upstream) oifname $PHYSICAL_WAN ip daddr $WG_UPSTREAM_ENDPOINT counter accept # only this is used # destinations configured in VPN bypass oifname $PHYSICAL_WAN mark $VPN_BYPASS_MARK counter accept # drop all other packets oifname $PHYSICAL_WAN counter drop } } # Tracing infrastructure, can be used for debugging (nft monitor trace) table inet trace { chain prerouting { type filter hook prerouting priority raw - 1 jump common } chain output { type filter hook output priority raw - 1 jump common } chain common { # Add tracing rule here # … meta nftrace set 1 # DO NOT COMMIT ANY TRACING RULES } } ''; }; }; systemd.network = { enable = true; # not all interfaces need to be up wait-online.extraArgs = [ "--any" ]; netdevs = { br-lan = { netdevConfig = { Name = "br-lan"; Kind = "bridge"; }; }; wg-upstream = { netdevConfig = { Kind = "wireguard"; Name = "wg-upstream"; }; wireguardConfig = { PrivateKeyFile = config.sops.secrets.wg-upstream-private-key.path; FirewallMark = 51820; }; wireguardPeers = lib.singleton { wireguardPeerConfig = { Endpoint = "193.32.248.71:51820"; PublicKey = "eprzkkkSbXCANngQDo305DIAvkKAnZaN71IpTNaOoTk="; AllowedIPs = [ "0.0.0.0/0" "::0/0" ]; PersistentKeepalive = 25; }; }; }; }; networks = { wan = { name = "enp1s0"; DHCP = "ipv4"; networkConfig = { IPv6AcceptRA = "yes"; }; dhcpV4Config = { UseDNS = "no"; }; ipv6AcceptRAConfig = { # Only use RA DHCPv6Client = false; UseDNS = "no"; }; }; lan1 = { name = "enp2s0"; bridge = [ "br-lan" ]; }; lan2 = { name = "enp3s0"; bridge = [ "br-lan" ]; }; lan3 = { name = "enp4s0"; bridge = [ "br-lan" ]; }; br-lan = { name = "br-lan"; domains = [ domain ]; address = [ "10.80.1.1/24" "fd00:80:1::1/64" ]; }; wg-upstream = { name = "wg-upstream"; address = [ "10.66.208.88/32" "fc00:bbbb:bbbb:bb01::3:d057/128" ]; routingPolicyRules = [ { routingPolicyRuleConfig = { Family = "both"; # default is only ipv4 FirewallMark = 51820; InvertRule = "yes"; Table = 51820; Priority = 10; #SuppressPrefixLength = 0; # can’t be used here (forwarding does not work with it) }; } # FIXME: those two shouldn’t be necessary # It should automatically detect those routes existing and prioritise them # LAN (v4) { routingPolicyRuleConfig = { To = "10.80.1.0/24"; Priority = 9; }; } # LAN (v6) { routingPolicyRuleConfig = { To = "fd00:80:1::/64"; Priority = 9; }; } # wg-home { routingPolicyRuleConfig = { To = "10.80.0.0/24"; Priority = 9; }; } # VPN bypass { routingPolicyRuleConfig = { Family = "both"; # welcome in the year 2023, where ipv4 is the default FirewallMark = vpnBypassFwMark; Priority = 9; }; } # plastic router { routingPolicyRuleConfig = { To = "192.168.0.0/24"; Priority = 9; }; } ]; routes = [ { routeConfig = { Gateway = "0.0.0.0"; # point-to-point connection Table = 51820; }; } { routeConfig = { Gateway = "::"; Table = 51820; }; } ]; }; }; }; services.resolved.enable = false; services.dnsmasq = { enable = true; settings = { bogus-priv = true; # do not forward revese lookups of internal addresses domain-needed = true; # do not forward names without domain interface = "br-lan"; # only respond to queries from lan no-hosts = true; # do not resolve hosts from /etc/hosts no-resolv = true; # only use explicitly configured resolvers cache-size = 10000; inherit domain; # Allow resolving the router interface-name = [ "${config.networking.hostName}.${domain},br-lan" "${config.networking.hostName},br-lan" ]; # DHCPv4 dhcp-range = [ "10.80.1.20,10.80.1.150,12h" # DHCPv4 "fd00:80:1::,ra-stateless,ra-names" # SLAAC (for addresses) / DHCPv6 (for DNS) ]; dhcp-option = [ "option:router,10.80.1.1" "option6:dns-server,fd00:80:1::1" ]; # Despite its name, the switch does not have a “smart” configuration, # that would allow me to tell it not to get DHCP from wan, # but from lan instead. # So it has to use static configuration. host-record = "switchviech,switchviech.${domain},10.80.1.19"; server = [ "127.0.0.1#5053" ]; }; }; systemd.services.dnsmasq.after = [ "systemd-networkd.service" ]; services.prometheus.exporters.dnsmasq = { enable = true; listenAddress = config.sbruder.wireguard.home.address; leasesPath = "/var/lib/dnsmasq/dnsmasq.leases"; }; networking.firewall.allowedUDPPorts = [ 53 67 ]; networking.firewall.allowedTCPPorts = [ 53 ]; # Wireless boot.kernelModules = [ "nl80211" ]; environment.systemPackages = with pkgs; [ iw wirelesstools ]; # The service is mostly taken from nixpkgs pr 222536. systemd.services.hostapd = { path = with pkgs; [ hostapd ]; after = [ "sys-subsystem-net-devices-wlp5s0.device" ]; bindsTo = [ "sys-subsystem-net-devices-wlp5s0.device" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${pkgs.hostapd}/bin/hostapd ${config.sops.secrets.hostapd-config.path}"; Restart = "always"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; RuntimeDirectory = "hostapd"; # Hardening LockPersonality = true; MemoryDenyWriteExecute = true; DevicePolicy = "closed"; DeviceAllow = "/dev/rfkill rw"; NoNewPrivileges = true; PrivateUsers = false; # hostapd requires true root access. PrivateTmp = true; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProcSubset = "pid"; ProtectSystem = "strict"; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ]; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ]; UMask = "0077"; }; }; services.https-dns-proxy = { enable = true; provider = { kind = "custom"; ips = [ "9.9.9.9" "149.112.112.112" ]; url = "https://dns.quad9.net/dns-query"; }; }; }