nixos-config/machines/shinobu/services/router.nix
Simon Bruder aa85febe12
shinobu/router: Fix IPv6 networking
Previously, I did not have IPv6 upstream, so even a wrong configuration
worked. Now it uses a different routing table for IPv4 and IPv6, so it
also works on dual-stack upstreams.

However, how it worked without IPv6 forwarding enabled, is still a
mystery to me.
2023-09-12 15:00:51 +02:00

396 lines
13 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 Intels wisdom, its not possible to use 5GHz in AP mode.
{ config, lib, pkgs, ... }:
let
domain = "home.sbruder.de";
noVpnFwMark = 10000;
in
{
sops.secrets.wg-upstream-private-key = {
owner = config.users.users.systemd-network.name;
sopsFile = ../secrets.yaml;
};
sops.secrets.wg-upstream-psk = {
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
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
# plastic router
iifname $NAT_LAN_IFACES oifname $PHYSICAL_WAN ip daddr $PLASTIC_ROUTER_V4 counter accept
iifname $PHYSICAL_WAN oifname $NAT_LAN_IFACES ip saddr $PLASTIC_ROUTER_V4 ct state established,related counter accept
iifname $NAT_LAN_IFACES oifname $PHYSICAL_WAN ip daddr $VUEKO_V4 udp dport $VUEKO_PORT counter accept
iifname $PHYSICAL_WAN oifname $NAT_LAN_IFACES ip saddr $VUEKO_V4 udp sport $VUEKO_PORT ct state established,related counter accept
iifname $NAT_LAN_IFACES oifname $PHYSICAL_WAN ip6 daddr $VUEKO_V6 udp dport $VUEKO_PORT counter accept
iifname $PHYSICAL_WAN oifname $NAT_LAN_IFACES ip6 saddr $VUEKO_V6 udp sport $VUEKO_PORT ct state established,related 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
}
}
table inet mangle {
chain output {
type route hook output priority mangle
# Add fwmark noVpnMark to packets to vueko, so it will get routed correctly
ip daddr $VUEKO_V4 udp dport $VUEKO_PORT mark set ${toString noVpnFwMark} counter
ip6 daddr $VUEKO_V6 udp dport $VUEKO_PORT mark set ${toString noVpnFwMark} 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
}
chain output {
type filter hook output priority -50; policy accept
# accept connections to plastic router
oifname $PHYSICAL_WAN ip daddr $PLASTIC_ROUTER_V4 accept
# accept icmpv6
oifname $PHYSICAL_WAN icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
# accept connections to selected endpoints
oifname $PHYSICAL_WAN ip daddr $WG_UPSTREAM_ENDPOINT counter accept
oifname $PHYSICAL_WAN ip daddr $VUEKO_V4 counter accept
oifname $PHYSICAL_WAN ip6 daddr $VUEKO_V6 counter accept
# drop all other packets
oifname $PHYSICAL_WAN counter drop
}
}
'';
};
};
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 = "185.189.112.26:1637";
PublicKey = "PyLCXAQT8KkM4T+dUsOQfn+Ub3pGxfGlxkIApuig+hk=";
PresharedKeyFile = config.sops.secrets.wg-upstream-psk.path;
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.128.218.130/32" "fd7d:76ee:e68f:a993:316e:a8d5:2b60:8e69/128" ];
dns = [ "10.128.0.1" "fd7d:76ee:e68f:a993::1" ];
routingPolicyRules = [
{
routingPolicyRuleConfig = {
Family = "both"; # default is only ipv4
FirewallMark = 51820;
InvertRule = "yes";
Table = 51820;
Priority = 10;
#SuppressPrefixLength = 0; # cant be used here (forwarding does not work with it)
};
}
# FIXME: those two shouldnt be necessary
# It should automatically detect those routes existing and prioritise them
# LAN (v4)
{
routingPolicyRuleConfig = {
To = "10.80.0.0/24";
Priority = 9;
};
}
# LAN (v6)
{
routingPolicyRuleConfig = {
To = "fd00:80:1::/64";
Priority = 9;
};
}
# wg-home
{
routingPolicyRuleConfig = {
To = "10.80.1.0/24";
Priority = 9;
};
}
# vueko (v4) for wg-home
{
routingPolicyRuleConfig = {
To = "168.119.176.53";
FirewallMark = noVpnFwMark;
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 = config.systemd.network.networks.wg-upstream.dns ++ [
#"9.9.9.9" # dns.quad9.net
#"2620:fe::fe"
];
};
};
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";
};
};
}