383 lines
12 KiB
Nix
383 lines
12 KiB
Nix
# 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";
|
||
|
||
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;
|
||
};
|
||
|
||
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 = {
|
||
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
|
||
{
|
||
routingPolicyRuleConfig = {
|
||
To = "10.80.0.0/24";
|
||
Priority = 9;
|
||
};
|
||
}
|
||
{
|
||
routingPolicyRuleConfig = {
|
||
To = "10.80.1.0/24";
|
||
Priority = 9;
|
||
};
|
||
}
|
||
{
|
||
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 = "::";
|
||
};
|
||
}
|
||
];
|
||
};
|
||
};
|
||
};
|
||
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";
|
||
};
|
||
};
|
||
}
|