shinobu/router: Split configuration
This commit is contained in:
parent
c48d626c68
commit
133bb85357
|
@ -5,7 +5,7 @@
|
||||||
../../modules
|
../../modules
|
||||||
|
|
||||||
./services/co2_exporter.nix
|
./services/co2_exporter.nix
|
||||||
./services/router.nix
|
./services/router
|
||||||
];
|
];
|
||||||
|
|
||||||
sbruder = {
|
sbruder = {
|
||||||
|
|
|
@ -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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
13
machines/shinobu/services/router/common.nix
Normal file
13
machines/shinobu/services/router/common.nix
Normal file
|
@ -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" ];
|
||||||
|
};
|
||||||
|
}
|
48
machines/shinobu/services/router/default.nix
Normal file
48
machines/shinobu/services/router/default.nix
Normal file
|
@ -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
|
||||||
|
];
|
||||||
|
}
|
65
machines/shinobu/services/router/dnsmasq.nix
Normal file
65
machines/shinobu/services/router/dnsmasq.nix
Normal file
|
@ -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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
147
machines/shinobu/services/router/networkd.nix
Normal file
147
machines/shinobu/services/router/networkd.nix
Normal file
|
@ -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;
|
||||||
|
}
|
32
machines/shinobu/services/router/nft.nix
Normal file
32
machines/shinobu/services/router/nft.nix
Normal file
|
@ -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}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
127
machines/shinobu/services/router/rules.nft
Normal file
127
machines/shinobu/services/router/rules.nft
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
65
machines/shinobu/services/router/wlan.nix
Normal file
65
machines/shinobu/services/router/wlan.nix
Normal file
|
@ -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" ];
|
||||||
|
}
|
Loading…
Reference in a new issue