shinobu/router: Add network segmentation

This commit is contained in:
Simon Bruder 2023-10-18 20:12:41 +02:00
parent 1740570d00
commit 4611e12772
Signed by: simon
GPG key ID: 8D3C82F9F309F8EC
5 changed files with 179 additions and 79 deletions

View file

@ -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
{ {
vlan = {
lan = {
id = 10;
subnet = mkSubnet "10.80.1.0/24" "fd00:80:1::/64";
domain = "home.sbruder.de"; 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 = { tc = {
interface = "enp1s0"; interface = "enp1s0";
# 4160kbit is slightly smaller than the average upload # 4160kbit is slightly smaller than the average upload

View file

@ -1,26 +1,22 @@
# Home network configuration # Home network configuration
# (2.5GbE clients) #
# | | # +----------+ +------------+
# +----------+ +----------+ # | | | | (clients)# (guests)
# | | | | | | (1GbE clients) # | | | +--|--|--|--|-#+-|--|--|-|-unused|
# | | | | | +|-|-|-|-|+ # +---+----+ +-+-+-+-+-+ | 01 02 …… 12 # 13 …… 24 | 25 26 |
# +---+----+ +-+-+-+-+-+ |5 4 3 2 1| # |upstream| | 1 2 3 4 | | aruba Instant On 1830 | (SFP) |
# |upstream| | 1 2 3 4 | |TL-SG105 | # +--------+ | shinobu | +------------------------+-------+
# +--------+ | shinobu | +---------+
# +---------+ # +---------+
# #
# It consists of shinobu as a router (this configuration), # 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). # 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: # 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 # device → NAT on shinobu → NAT on plastic router → PŸUR CGNAT
# #
# Because the switch only supports GbE, # Because of issues with the NICs operating at 2.5GbE,
# the two clients I currently have with support for 2.5GbE are connected # all clients are connected to the switch,
# directly to the two remaining network interfaces on shinobu. # even if they have a 2.5GbE NIC.
# 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. # 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. # Once nixpkgs PR 222536 is merged, I will migrate to using the NixOS module.
@ -52,15 +48,48 @@ in
enable = true; enable = true;
# not all interfaces need to be up # not all interfaces need to be up
wait-online.extraArgs = [ "--any" ]; wait-online.extraArgs = [ "--any" ];
netdevs = { netdevs = lib.mkMerge [
br-lan = { (lib.mapAttrs
(name: config: {
netdevConfig = { netdevConfig = {
Name = "br-lan"; Kind = "vlan";
Name = name;
};
vlanConfig = {
Id = config.id;
};
})
cfg.vlan)
(lib.mapAttrs'
(name: config: lib.nameValuePair "br-${name}" {
netdevConfig = {
Name = "br-${name}";
Kind = "bridge"; 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;
}; };
networks = { })
cfg.vlan)
{
wan = { wan = {
name = "enp1s0"; name = "enp1s0";
DHCP = "ipv4"; DHCP = "ipv4";
@ -76,9 +105,17 @@ in
UseDNS = "no"; UseDNS = "no";
}; };
}; };
lan1 = { physical-lan = {
name = "enp2s0"; name = "enp2s0";
bridge = [ "br-lan" ]; vlan = lib.attrNames cfg.vlan;
# no autoconfiguration needed, only tagged VLAN
networkConfig = {
LinkLocalAddressing = "no";
LLDP = "no";
EmitLLDP = "no";
IPv6AcceptRA = "no";
IPv6SendRA = "no";
};
}; };
lan2 = { lan2 = {
name = "enp3s0"; name = "enp3s0";
@ -88,12 +125,8 @@ in
name = "enp4s0"; name = "enp4s0";
bridge = [ "br-lan" ]; 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; services.resolved.enable = false;
} }

View file

@ -1,4 +1,4 @@
{ config, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = pkgs.callPackage ./common.nix { }; cfg = pkgs.callPackage ./common.nix { };
in in
@ -9,35 +9,39 @@ in
settings = { settings = {
bogus-priv = true; # do not forward revese lookups of internal addresses bogus-priv = true; # do not forward revese lookups of internal addresses
domain-needed = true; # do not forward names without domain 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-hosts = true; # do not resolve hosts from /etc/hosts
no-resolv = true; # only use explicitly configured resolvers no-resolv = true; # only use explicitly configured resolvers
dhcp-fqdn = true; # only insert qualified names of DHCP clients into DNS
cache-size = 10000; 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 # Allow resolving the router
interface-name = [ interface-name = lib.mapAttrsToList (name: { domain, ... }: "${config.networking.hostName}.${domain},br-${name}") cfg.vlan;
"${config.networking.hostName}.${cfg.domain},br-lan"
"${config.networking.hostName},br-lan"
];
# DHCPv4 dhcp-range = lib.flatten (lib.mapAttrsToList
dhcp-range = [ (name: { subnet, ... }: [
"10.80.1.20,10.80.1.150,12h" # DHCPv4 "tag:br-${name},${subnet.v4.withoutLastComponent}2,${subnet.v4.withoutLastComponent}254,12h" # DHCPv4
"fd00:80:1::,ra-stateless,ra-names" # SLAAC (for addresses) / DHCPv6 (for DNS) "tag:br-${name},${subnet.v6.net},ra-stateless,ra-names" # SLAAC (for addresses) / DHCPv6 (for DNS)
]; ])
dhcp-option = [ cfg.vlan);
"option:router,10.80.1.1" dhcp-option = lib.flatten (lib.mapAttrsToList
"option6:dns-server,fd00:80:1::1" (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 = [ server = [
"127.0.0.1#5053" "127.0.0.1#5053"
]; ];

View file

@ -10,7 +10,10 @@ let
else lib.generators.mkValueStringDefault { } v; 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 defines = lib.concatStringsSep
"\n" "\n"

View file

@ -1,4 +1,4 @@
define NAT_LAN_IFACES = { "br-lan" } define NAT_LAN_IFACES = { "br-lan", "br-guest" }
define PHYSICAL_WAN = "enp1s0" define PHYSICAL_WAN = "enp1s0"
define NAT_WAN_IFACES = { $PHYSICAL_WAN } define NAT_WAN_IFACES = { $PHYSICAL_WAN }
@ -6,9 +6,20 @@ table inet filter {
chain forward { chain forward {
type filter hook forward priority filter; policy drop 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_LAN_IFACES oifname $NAT_WAN_IFACES counter accept
iifname $NAT_WAN_IFACES oifname $NAT_LAN_IFACES ct state established,related 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
} }
} }