diff --git a/machines/shinobu/configuration.nix b/machines/shinobu/configuration.nix index 383cc45..558c67b 100644 --- a/machines/shinobu/configuration.nix +++ b/machines/shinobu/configuration.nix @@ -5,7 +5,7 @@ ../../modules ./services/co2_exporter.nix - ./services/router.nix + ./services/router ]; sbruder = { diff --git a/machines/shinobu/services/router.nix b/machines/shinobu/services/router.nix deleted file mode 100644 index fd9ce30..0000000 --- a/machines/shinobu/services/router.nix +++ /dev/null @@ -1,439 +0,0 @@ -# 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 - - # allow traffic between lan and wan - 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 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; [ - ethtool - 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"; - }; - }; -} diff --git a/machines/shinobu/services/router/common.nix b/machines/shinobu/services/router/common.nix new file mode 100644 index 0000000..62dfc73 --- /dev/null +++ b/machines/shinobu/services/router/common.nix @@ -0,0 +1,13 @@ +{ + domain = "home.sbruder.de"; + vpnBypassFwMark = 10000; + wg-upstream = { + endpoint = rec { + address = "193.32.248.71"; + port = 51820; + full = "${address}:${toString port}"; + }; + publicKey = "eprzkkkSbXCANngQDo305DIAvkKAnZaN71IpTNaOoTk="; + addresses = [ "10.66.208.88/32" "fc00:bbbb:bbbb:bb01::3:d057/128" ]; + }; +} diff --git a/machines/shinobu/services/router/default.nix b/machines/shinobu/services/router/default.nix new file mode 100644 index 0000000..2e34f2a --- /dev/null +++ b/machines/shinobu/services/router/default.nix @@ -0,0 +1,48 @@ +# 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 + cfg = import ./common.nix; +in +{ + imports = [ + ./dnsmasq.nix + ./networkd.nix + ./nft.nix + ./wlan.nix + ]; + + boot.kernel.sysctl = { + "net.ipv4.conf.all.forwarding" = true; + "net.ipv6.conf.all.forwarding" = true; + }; + + environment.systemPackages = with pkgs; [ + ethtool + ]; +} diff --git a/machines/shinobu/services/router/dnsmasq.nix b/machines/shinobu/services/router/dnsmasq.nix new file mode 100644 index 0000000..867f946 --- /dev/null +++ b/machines/shinobu/services/router/dnsmasq.nix @@ -0,0 +1,65 @@ +{ config, ... }: +let + cfg = import ./common.nix; +in +{ + 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 (cfg) domain; + + # Allow resolving the router + interface-name = [ + "${config.networking.hostName}.${cfg.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.${cfg.domain},10.80.1.19"; + server = [ + "127.0.0.1#5053" + ]; + }; + }; + systemd.services.dnsmasq.after = [ "systemd-networkd.service" ]; + + networking.firewall.allowedUDPPorts = [ 53 67 ]; + networking.firewall.allowedTCPPorts = [ 53 ]; + + services.prometheus.exporters.dnsmasq = { + enable = true; + listenAddress = config.sbruder.wireguard.home.address; + leasesPath = "/var/lib/dnsmasq/dnsmasq.leases"; + }; + + 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"; + }; + }; +} diff --git a/machines/shinobu/services/router/networkd.nix b/machines/shinobu/services/router/networkd.nix new file mode 100644 index 0000000..7a0c51b --- /dev/null +++ b/machines/shinobu/services/router/networkd.nix @@ -0,0 +1,147 @@ +{ config, lib, ... }: +let + cfg = import ./common.nix; +in +{ + sops.secrets.wg-upstream-private-key = { + owner = config.users.users.systemd-network.name; + sopsFile = ../../secrets.yaml; + }; + + networking.useDHCP = false; + + 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 = with cfg.wg-upstream; { + Endpoint = endpoint.full; + PublicKey = publicKey; + 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 = [ cfg.domain ]; + address = [ "10.80.1.1/24" "fd00:80:1::1/64" ]; + }; + wg-upstream = { + name = "wg-upstream"; + address = cfg.wg-upstream.addresses; + 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 = cfg.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; +} diff --git a/machines/shinobu/services/router/nft.nix b/machines/shinobu/services/router/nft.nix new file mode 100644 index 0000000..efe0e9a --- /dev/null +++ b/machines/shinobu/services/router/nft.nix @@ -0,0 +1,32 @@ +{ config, lib, ... }: +let + cfg = import ./common.nix; + + flattenAttrValues = attrs: lib.flatten (map (v: if lib.isAttrs v then flattenAttrValues v else v) (lib.attrValues attrs)); + + mkKeyValue = lib.generators.mkKeyValueDefault + rec { + # specifies the generated string for a subset of nix values + mkValueString = v: + if lib.isString v then ''"${v}"'' + else if lib.isList v then ''{ ${lib.concatMapStringsSep ", " mkValueString v} }'' + else lib.generators.mkValueStringDefault { } v; + } " = "; + + defines = lib.concatStringsSep + "\n" + (flattenAttrValues + (lib.mapAttrsRecursive + (path: value: "define " + (mkKeyValue ''CFG_${lib.concatStringsSep "_" path}'' value)) + cfg)); +in +{ + networking.nftables = { + enable = true; + ruleset = '' + ${defines} + + include "${./rules.nft}" + ''; + }; +} diff --git a/machines/shinobu/services/router/rules.nft b/machines/shinobu/services/router/rules.nft new file mode 100644 index 0000000..d518c28 --- /dev/null +++ b/machines/shinobu/services/router/rules.nft @@ -0,0 +1,127 @@ +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 = $CFG_wg-upstream_endpoint_address +define PLASTIC_ROUTER_V4 = 192.168.0.1 +define VPN_BYPASS_MARK = $CFG_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 + + # allow traffic between lan and wan + 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 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 + } +} diff --git a/machines/shinobu/services/router/wlan.nix b/machines/shinobu/services/router/wlan.nix new file mode 100644 index 0000000..fca5126 --- /dev/null +++ b/machines/shinobu/services/router/wlan.nix @@ -0,0 +1,65 @@ +{ config, pkgs, ... }: +{ + sops.secrets.hostapd-config = { + sopsFile = ../../secrets.yaml; + }; + + # 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"; + }; + }; + + environment.systemPackages = with pkgs; [ + iw + wirelesstools + ]; + + + # Wireless + boot.kernelModules = [ "nl80211" ]; +}