From 4611e127725e9e62edee67c7f99602a64d25a31e Mon Sep 17 00:00:00 2001 From: Simon Bruder Date: Wed, 18 Oct 2023 20:12:41 +0200 Subject: [PATCH] shinobu/router: Add network segmentation --- machines/shinobu/services/router/common.nix | 53 ++++++- machines/shinobu/services/router/default.nix | 139 ++++++++++++------- machines/shinobu/services/router/dnsmasq.nix | 46 +++--- machines/shinobu/services/router/nft.nix | 5 +- machines/shinobu/services/router/rules.nft | 15 +- 5 files changed, 179 insertions(+), 79 deletions(-) diff --git a/machines/shinobu/services/router/common.nix b/machines/shinobu/services/router/common.nix index 9cae934..0c28330 100644 --- a/machines/shinobu/services/router/common.nix +++ b/machines/shinobu/services/router/common.nix @@ -1,6 +1,55 @@ -{ ... }: +{ lib, ... }: +let + mkSubnet = v4: v6: + let + splitCidr = lib.splitString "/"; + fst = lib.flip lib.elemAt 0; + snd = lib.flip lib.elemAt 1; + + v4Split = splitCidr v4; + v6Split = splitCidr v6; + in + { + v4 = rec { + cidr = v4; + net = fst v4Split; + suffix = snd v4Split; + withoutLastComponent = lib.substring 0 ((lib.stringLength net) - 1) net; + gateway = "${withoutLastComponent}1"; + gatewayCidr = "${gateway}/${suffix}"; + }; + v6 = rec { + cidr = v6; + net = fst v6Split; + suffix = snd v6Split; + gateway = "${net}1"; + gatewayCidr = "${gateway}/${suffix}"; + }; + }; +in { - domain = "home.sbruder.de"; + vlan = { + lan = { + id = 10; + subnet = mkSubnet "10.80.1.0/24" "fd00:80:1::/64"; + domain = "home.sbruder.de"; + }; + management = { + id = 20; + subnet = mkSubnet "10.80.2.0/24" "fd00:80:2::/64"; + domain = "management.sbruder.de"; + }; + guest = { + id = 30; + subnet = mkSubnet "10.80.3.0/24" "fd00:80:3::/64"; + domain = "guest.sbruder.de"; + }; + iot = { + id = 40; + subnet = mkSubnet "10.80.4.0/24" "fd00:80:4::/64"; + domain = "iot.sbruder.de"; + }; + }; tc = { interface = "enp1s0"; # 4160 kbit is slightly smaller than the average upload diff --git a/machines/shinobu/services/router/default.nix b/machines/shinobu/services/router/default.nix index 6dcc197..c1355f5 100644 --- a/machines/shinobu/services/router/default.nix +++ b/machines/shinobu/services/router/default.nix @@ -1,26 +1,22 @@ # Home network configuration -# (2.5GbE clients) -# | | -# +----------+ +----------+ -# | | | | | | (1GbE clients) -# | | | | | +|-|-|-|-|+ -# +---+----+ +-+-+-+-+-+ |5 4 3 2 1| -# |upstream| | 1 2 3 4 | |TL-SG105 | -# +--------+ | shinobu | +---------+ +# +# +----------+ +------------+ +# | | | | (clients)# (guests) +# | | | +--|--|--|--|-#+-|--|--|-|-unused| +# +---+----+ +-+-+-+-+-+ | 01 02 …… 12 # 13 …… 24 | 25 26 | +# |upstream| | 1 2 3 4 | | aruba Instant On 1830 | (SFP) | +# +--------+ | 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. +# connected to a aruba (HPE) Instant ON 1830 24-port 1GbE 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) three layers of NAT: # device → NAT on shinobu → NAT on plastic router → PŸUR CGNAT # -# 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. +# Because of issues with the NICs operating at 2.5GbE, +# all clients are connected to the switch, +# even if they have a 2.5GbE NIC. # # 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. @@ -52,48 +48,85 @@ in enable = true; # not all interfaces need to be up wait-online.extraArgs = [ "--any" ]; - netdevs = { - br-lan = { - netdevConfig = { - Name = "br-lan"; - Kind = "bridge"; + netdevs = lib.mkMerge [ + (lib.mapAttrs + (name: config: { + netdevConfig = { + Kind = "vlan"; + Name = name; + }; + vlanConfig = { + Id = config.id; + }; + }) + cfg.vlan) + (lib.mapAttrs' + (name: config: lib.nameValuePair "br-${name}" { + netdevConfig = { + Name = "br-${name}"; + Kind = "bridge"; + }; + }) + cfg.vlan) + ]; + networks = lib.mkMerge [ + (lib.mapAttrs + (name: config: { + inherit name; + matchConfig = { + Type = "vlan"; + }; + bridge = [ "br-${name}" ]; + }) + cfg.vlan) + (lib.mapAttrs' + (name: config: lib.nameValuePair "br-${name}" { + name = "br-${name}"; + domains = [ config.domain ]; + address = lib.mapAttrsToList (family: familyConfig: familyConfig.gatewayCidr) config.subnet; + networkConfig = { + IPv6AcceptRA = false; + }; + }) + cfg.vlan) + { + wan = { + name = "enp1s0"; + DHCP = "ipv4"; + networkConfig = { + IPv6AcceptRA = "yes"; + }; + dhcpV4Config = { + UseDNS = "no"; + }; + ipv6AcceptRAConfig = { + # Only use RA + DHCPv6Client = false; + UseDNS = "no"; + }; }; - }; - }; - networks = { - wan = { - name = "enp1s0"; - DHCP = "ipv4"; - networkConfig = { - IPv6AcceptRA = "yes"; + physical-lan = { + name = "enp2s0"; + vlan = lib.attrNames cfg.vlan; + # no autoconfiguration needed, only tagged VLAN + networkConfig = { + LinkLocalAddressing = "no"; + LLDP = "no"; + EmitLLDP = "no"; + IPv6AcceptRA = "no"; + IPv6SendRA = "no"; + }; }; - dhcpV4Config = { - UseDNS = "no"; + lan2 = { + name = "enp3s0"; + bridge = [ "br-lan" ]; }; - ipv6AcceptRAConfig = { - # Only use RA - DHCPv6Client = false; - UseDNS = "no"; + lan3 = { + name = "enp4s0"; + bridge = [ "br-lan" ]; }; - }; - 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" ]; - }; - }; + } + ]; }; services.resolved.enable = false; } diff --git a/machines/shinobu/services/router/dnsmasq.nix b/machines/shinobu/services/router/dnsmasq.nix index 84b2ee1..b462b13 100644 --- a/machines/shinobu/services/router/dnsmasq.nix +++ b/machines/shinobu/services/router/dnsmasq.nix @@ -1,4 +1,4 @@ -{ config, pkgs, ... }: +{ config, lib, pkgs, ... }: let cfg = pkgs.callPackage ./common.nix { }; in @@ -9,35 +9,39 @@ in 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 + interface = lib.mapAttrsToList (name: config: "br-${name}") cfg.vlan; # only respond to queries from own interfaces no-hosts = true; # do not resolve hosts from /etc/hosts no-resolv = true; # only use explicitly configured resolvers + dhcp-fqdn = true; # only insert qualified names of DHCP clients into DNS cache-size = 10000; - inherit (cfg) domain; + domain = [ + "invalid.sbruder.de" # used when no rule below matches + ] ++ (lib.flatten (lib.mapAttrsToList + (name: { domain, subnet, ... }: [ + "${domain},br-${name}" # only this is not enough + "${domain},${subnet.v4.cidr}" + "${domain},${subnet.v6.cidr}" + ]) + cfg.vlan)); # Allow resolving the router - interface-name = [ - "${config.networking.hostName}.${cfg.domain},br-lan" - "${config.networking.hostName},br-lan" - ]; + interface-name = lib.mapAttrsToList (name: { domain, ... }: "${config.networking.hostName}.${domain},br-${name}") cfg.vlan; - # 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" - ]; + dhcp-range = lib.flatten (lib.mapAttrsToList + (name: { subnet, ... }: [ + "tag:br-${name},${subnet.v4.withoutLastComponent}2,${subnet.v4.withoutLastComponent}254,12h" # DHCPv4 + "tag:br-${name},${subnet.v6.net},ra-stateless,ra-names" # SLAAC (for addresses) / DHCPv6 (for DNS) + ]) + cfg.vlan); + dhcp-option = lib.flatten (lib.mapAttrsToList + (name: { subnet, ... }: [ + "tag:br-${name},option:router,${subnet.v4.gateway}" + "tag:br-${name},option6:dns-server,${subnet.v6.gateway}" + ]) + cfg.vlan); - # 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" ]; diff --git a/machines/shinobu/services/router/nft.nix b/machines/shinobu/services/router/nft.nix index af5c516..0ef384c 100644 --- a/machines/shinobu/services/router/nft.nix +++ b/machines/shinobu/services/router/nft.nix @@ -10,7 +10,10 @@ let else lib.generators.mkValueStringDefault { } v; } " = "; - passthru = { }; + passthru = { + VLANS = lib.attrNames cfg.vlan; + VLAN_BRIDGES = map (name: "br-${name}") (lib.attrNames cfg.vlan); + }; defines = lib.concatStringsSep "\n" diff --git a/machines/shinobu/services/router/rules.nft b/machines/shinobu/services/router/rules.nft index 64cfcf1..3272aeb 100644 --- a/machines/shinobu/services/router/rules.nft +++ b/machines/shinobu/services/router/rules.nft @@ -1,4 +1,4 @@ -define NAT_LAN_IFACES = { "br-lan" } +define NAT_LAN_IFACES = { "br-lan", "br-guest" } define PHYSICAL_WAN = "enp1s0" define NAT_WAN_IFACES = { $PHYSICAL_WAN } @@ -6,9 +6,20 @@ table inet filter { chain forward { type filter hook forward priority filter; policy drop - # allow traffic between lan and wan + # plastic router, might be vulnerable (FIXME v6 is still reachable) + iifname "br-guest" ip daddr "192.168.0.1" drop + + # allow traffic between selected VLANs 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 + + # traffic from lan to all other vlans is allowed + iifname "br-lan" oifname $VLAN_BRIDGES counter accept; + iifname $VLAN_BRIDGES oifname "br-lan" ct state established,related counter accept + + iifname "br-iot" ip daddr 167.235.30.249 tcp dport 1883 counter accept # FIXME migrate service to shinobu + iifname "br-iot" udp dport 123 counter accept # FIXME too generic + iifname $NAT_WAN_IFACES oifname "br-iot" ct state established,related counter accept } }