dotfiles/system/hosts/router/default.nix

759 lines
33 KiB
Nix

{ config
, pkgs
, notnft
, lib
, router-lib
, ... }:
let
rootUuid = "44444444-4444-4444-8888-888888888888";
rootPart = "/dev/disk/by-uuid/${rootUuid}";
cfg = config.router-settings;
hapdConfig = {
inherit (cfg) country_code wpa_passphrase;
he_su_beamformer = true;
he_su_beamformee = true;
he_mu_beamformer = true;
he_spr_sr_control = 3;
he_default_pe_duration = 4;
he_rts_threshold = 1023;
he_mu_edca_qos_info_param_count = 0;
he_mu_edca_qos_info_q_ack = 0;
he_mu_edca_qos_info_queue_request = 0;
he_mu_edca_qos_info_txop_request = 0;
he_mu_edca_ac_be_aifsn = 8;
he_mu_edca_ac_be_aci = 0;
he_mu_edca_ac_be_ecwmin = 9;
he_mu_edca_ac_be_ecwmax = 10;
he_mu_edca_ac_be_timer = 255;
he_mu_edca_ac_bk_aifsn = 15;
he_mu_edca_ac_bk_aci = 1;
he_mu_edca_ac_bk_ecwmin = 9;
he_mu_edca_ac_bk_ecwmax = 10;
he_mu_edca_ac_bk_timer = 255;
he_mu_edca_ac_vi_ecwmin = 5;
he_mu_edca_ac_vi_ecwmax = 7;
he_mu_edca_ac_vi_aifsn = 5;
he_mu_edca_ac_vi_aci = 2;
he_mu_edca_ac_vi_timer = 255;
he_mu_edca_ac_vo_aifsn = 5;
he_mu_edca_ac_vo_aci = 3;
he_mu_edca_ac_vo_ecwmin = 5;
he_mu_edca_ac_vo_ecwmax = 7;
he_mu_edca_ac_vo_timer = 255;
preamble = true;
country3 = "0x49"; # indoor
};
# routing tables
wan_table = 1;
# vpn table, assign an id but don't actually add a rule for it, so it is the default
vpn_table = 2;
# iot table without a route into the internet
iot_table = 3;
dnatRuleMode = rule:
if rule.mode != "" then rule.mode
else if rule.target4.address or null == gatewayAddr4 || rule.target6.address or null == gatewayAddr6 then "rule"
else "mark";
# nftables rules generator
# selfIp4/selfIp6 = block packets from these addresses
# extraInetEntries = stuff to add to inet table
# extraNetdevEntries = stuff to add to netdev table
# wans = external interfaces (internet)
# lans = internal interfaces (lan)
# netdevIngressWanRules = additional rules for ingress (netdev)
# inetInboundWanRules = additional rules for input from wan (inet), i.e. stuff meant directly for the router and not for any other device
# inetForwardRules = additional forward rules besides allow lan->wan forwarding
# inetSnatRules = snat rules (changing source address, usually just called nat)
# inetDnatRules = dnat rules (changing destination address, i.e. port forwarding)
# logPrefix = log prefix for drops
mkRules = {
selfIp4,
selfIp6,
extraInetEntries ? {},
extraNetdevEntries ? {},
wans,
lans,
netdevIngressWanRules ? [],
inetInboundWanRules ? [],
inetForwardRules ? [],
inetSnatRules ? [],
inetDnatRules ? [],
logPrefix ? "",
}: with notnft.dsl; with payload; ruleset {
filter = add table.netdev ({
ingress_common = add chain
[(is.eq (bit.and tcp.flags (f: bit.or f.fin f.syn)) (f: bit.or f.fin f.syn)) (log "${logPrefix}fin+syn drop ") drop]
[(is.eq (bit.and tcp.flags (f: bit.or f.syn f.rst)) (f: bit.or f.syn f.rst)) (log "${logPrefix}syn+rst drop ") drop]
[(is.eq (bit.and tcp.flags (f: with f; bit.or fin syn rst psh ack urg)) 0) (log "${logPrefix}null drop ") drop]
[(is tcp.flags (f: f.syn)) (is.eq tcpOpt.maxseg.size (range 0 500)) (log "${logPrefix}maxseg drop ") drop]
# reject requests with own saddr
# log if they are meant for us...
[(is.eq ip.saddr selfIp4) (is.eq (fib (f: with f; [ daddr iif ]) (f: f.type)) (f: f.local)) (log "${logPrefix}self4 ") drop]
[(is.eq ip6.saddr selfIp6) (is.eq (fib (f: with f; [ daddr iif ]) (f: f.type)) (f: f.local)) (log "${logPrefix}self6 ") drop]
# ...but drop silently if they're multicast/broadcast
[(is.eq ip.saddr selfIp4) drop]
[(is.eq ip6.saddr selfIp6) drop]
[return];
ingress_lan_common = add chain
# there are some issues with this, disable it for lan
# [(is.eq (fib (f: with f; [ saddr mark iif ]) (f: f.oif)) missing) (log "${logPrefix}oif missing ") drop]
[(jump "ingress_common")];
ingress_wan_common = add chain
netdevIngressWanRules
[(jump "ingress_common")]
# [(is.ne (fib (f: with f; [ daddr iif ]) (f: f.type)) (f: set [ f.local f.broadcast f.multicast ])) (log "${logPrefix}non-{local,broadcast,multicast} ") drop]
[(is.eq ip.protocol (f: f.icmp)) (limit { rate = 100; per = f: f.second; }) accept]
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (limit { rate = 100; per = f: f.second; }) accept]
[(is.eq ip.protocol (f: f.icmp)) (log "${logPrefix}icmp flood ") drop]
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (log "${logPrefix}icmp6 flood ") drop];
}
// extraNetdevEntries
// builtins.listToAttrs (map (name: {
name = "ingress_${name}";
value = add chain { type = f: f.filter; hook = f: f.ingress; dev = name; prio = -500; policy = f: f.accept; }
[(jump "ingress_lan_common")];
}) lans)
// builtins.listToAttrs (map (name: {
name = "ingress_${name}";
value = add chain { type = f: f.filter; hook = f: f.ingress; dev = name; prio = -500; policy = f: f.accept; }
[(jump "ingress_wan_common")];
}) wans));
global = add table { family = f: f.inet; } ({
inbound_wan_common = add chain
[(vmap ct.state { established = accept; related = accept; invalid = drop; })]
[(is ct.status (f: f.dnat)) accept]
[(is.eq (bit.and tcp.flags (f: f.syn)) 0) (is.eq ct.state (f: f.new)) (log "${logPrefix}new non-syn ") drop]
# icmp: only accept ping requests
[(is.eq ip.protocol (f: f.icmp)) (is.eq icmp.type (f: with f; set [ echo-request ])) accept]
# icmpv6: accept no-route info from link-local addresses
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq ip6.saddr (cidr "fe80::/10")) (is.eq icmpv6.code (f: f.no-route))
(is.eq icmpv6.type (f: with f; set [ mld-listener-query mld-listener-report mld-listener-done mld2-listener-report ]))
accept]
# icmpv6: accept commonly useful stuff
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.type (f: with f; set [ destination-unreachable time-exceeded echo-request echo-reply ])) accept]
# icmpv6: more common stuff
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.code (f: f.no-route))
(is.eq icmpv6.type (f: with f; set [ packet-too-big parameter-problem ])) accept]
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.code (f: f.admin-prohibited))
(is.eq icmpv6.type (f: with f; set [ parameter-problem ])) accept]
inetInboundWanRules;
# trust the lan
inbound_lan_common = add chain
[accept];
inbound = add chain { type = f: f.filter; hook = f: f.input; prio = f: f.filter; policy = f: f.drop; }
[(vmap meta.iifname ({
lo = accept;
}
// lib.genAttrs lans (_: jump "inbound_lan_common")
// lib.genAttrs wans (_: jump "inbound_wan_common")
))];
#[(log "${logPrefix}inbound drop ")];
forward = add chain { type = f: f.filter; hook = f: f.forward; prio = f: f.filter; policy = f: f.drop; }
[(vmap ct.state { established = accept; related = accept; invalid = drop; })]
[(is ct.status (f: f.dnat)) accept]
# accept lan->wan fw
[(is.eq meta.iifname (set lans)) (is.eq meta.oifname (set wans)) accept]
# accept lan->lan fw
[(is.eq meta.iifname (set lans)) (is.eq meta.oifname (set lans)) accept]
# accept wan->lan icmpv6 forward
[(is.eq meta.iifname (set wans)) (is.eq icmpv6.type (f: with f; set [ destination-unreachable time-exceeded echo-request echo-reply ])) accept]
[(is.eq meta.iifname (set wans)) (is.eq icmpv6.code (f: f.no-route)) (is.eq icmpv6.type (f: with f; set [ packet-too-big parameter-problem ])) accept]
[(is.eq meta.iifname (set wans)) (is.eq icmpv6.code (f: f.admin-prohibited)) (is.eq icmpv6.type (f: f.parameter-problem)) accept]
inetForwardRules
[(log "${logPrefix}forward drop ")];
postrouting = add chain { type = f: f.nat; hook = f: f.postrouting; prio = f: f.srcnat; policy = f: f.accept; }
# masquerade ipv6 because my isp doesn't provide it and my vpn gives a single ipv6
[(is.eq meta.protocol (f: set [ f.ip f.ip6 ])) (is.eq meta.iifname (set lans)) (is.eq meta.oifname (set wans)) masquerade]
inetSnatRules;
prerouting = add chain { type = f: f.nat; hook = f: f.prerouting; prio = f: f.dstnat; policy = f: f.accept; }
inetDnatRules;
} // extraInetEntries);
};
mkFlushRules = {}: with notnft.dsl; ruleset [ (flush ruleset) ];
unbound-python = pkgs.python3.withPackages (ps: with ps; [ pydbus dnspython requests pytricia nftables ]);
# parse a.b.c.d/x into { address, prefixLength }
netParsedCidr4 = router-lib.parseCidr cfg.network;
netParsedCidr6 = router-lib.parseCidr cfg.network6;
netnsParsedCidr4 = router-lib.parseCidr cfg.netnsNet;
netnsParsedCidr6 = router-lib.parseCidr cfg.netnsNet6;
# generate network cidr from device address
# (normalizeCidr applies network mask to the address)
netCidr4 = router-lib.serializeCidr (router-lib.normalizeCidr netParsedCidr4);
netCidr6 = router-lib.serializeCidr (router-lib.normalizeCidr netParsedCidr6);
netnsCidr4 = router-lib.serializeCidr (router-lib.normalizeCidr netnsParsedCidr4);
netnsCidr6 = router-lib.serializeCidr (router-lib.normalizeCidr netnsParsedCidr6);
gatewayAddr4 = netParsedCidr4.address;
gatewayAddr6 = netParsedCidr6.address;
mainNetnsAddr4 = netnsParsedCidr4.address;
mainNetnsAddr6 = netnsParsedCidr6.address;
wanNetnsAddr4 = cfg.wanNetnsAddr;
wanNetnsAddr6 = cfg.wanNetnsAddr6;
parsedGatewayAddr4 = router-lib.parseIp4 gatewayAddr4;
parsedGatewayAddr6 = router-lib.parseIp6 gatewayAddr6;
addToIp' = ip: n: lib.init ip ++ [ (lib.last ip + n) ];
addToIp = ip: n: router-lib.serializeIp (addToIp' ip n);
# server
serverAddress4 = addToIp parsedGatewayAddr4 1;
serverAddress6 = addToIp parsedGatewayAddr6 1;
# robot vacuum
vacuumAddress4 = addToIp parsedGatewayAddr4 2;
vacuumAddress6 = addToIp parsedGatewayAddr6 2;
# TODO: take from server config?
hosted-domains =
map
(prefix: if prefix == null then cfg.domainName else "${prefix}.${cfg.domainName}")
[
null "dns" "mumble" "mail" "music" "www" "matrix"
"search" "git" "cloud" "ns1" "ns2"
];
in {
router-settings.domainName = "pavluk.org";
router-settings.dhcpReservations = [
{ ipAddress = serverAddress4;
macAddress = cfg.serverMac; }
{ ipAddress = vacuumAddress4;
macAddress = cfg.vacuumMac; }
];
router-settings.dhcp6Reservations = [
{ ipAddress = serverAddress6;
macAddress = cfg.serverMac; }
{ ipAddress = vacuumAddress6;
macAddress = cfg.vacuumMac; }
];
router-settings.dnatRules = [
{
# TODO: take firewall settings from server config
port = notnft.dsl.set [
# http
80 443 8008 8448
# mail
25 587 465 143 993 110 995 4190
];
tcp = true; udp = false;
target4.address = serverAddress4;
target6.address = serverAddress6;
}
{
# mumble
port = 64738; tcp = true; udp = true;
target4.address = serverAddress4;
target6.address = serverAddress6;
}
{
# expose the default namespace's ssh via port 23
port = 23; tcp = true; udp = false;
target4.address = gatewayAddr4;
target4.port = 22;
target6.address = gatewayAddr6;
target6.port = 22;
}
];
imports = [ ./options.nix ];
system.stateVersion = "22.11";
fileSystems = {
# mount root on tmpfs
"/" = { device = "none"; fsType = "tmpfs"; neededForBoot = true;
options = [ "defaults" "size=2G" "mode=755" ]; };
"/persist" =
{ device = rootPart; fsType = "btrfs"; neededForBoot = true;
options = [ "compress=zstd:15" "subvol=@" ]; };
"/boot" =
{ device = rootPart; fsType = "btrfs"; neededForBoot = true;
options = [ "subvol=@boot" ]; };
"/nix" =
{ device = rootPart; fsType = "btrfs"; neededForBoot = true;
options = [ "compress=zstd:15" "subvol=@nix" ]; };
};
impermanence = {
enable = true;
path = /persist;
directories = [
{ directory = /home/${config.common.mainUsername}; user = config.common.mainUsername; group = "users"; mode = "0700"; }
{ directory = /root; mode = "0700"; }
{ directory = /var/db/dhcpcd; mode = "0755"; }
{ directory = /var/lib/private/kea; mode = "0750"; }
# for wireguard key
{ directory = /secrets; mode = "0000"; }
];
};
boot.kernel.sysctl = {
"net.ipv4.conf.all.src_valid_mark" = true;
"net.ipv4.conf.default.src_valid_mark" = true;
"net.ipv4.conf.all.forwarding" = true;
"net.ipv6.conf.all.forwarding" = true;
};
services.openssh.enable = true;
/*services.fail2ban = {
enable = true;
};*/
router.enable = true;
router.interfaces.wlan0 = {
bridge = "br0";
hostapd.enable = true;
hostapd.settings = {
inherit (cfg) ssid;
hw_mode = "g";
channel = 1;
chanlist = [ 1 ];
supported_rates = [ 60 90 120 180 240 360 480 540 ];
basic_rates = [ 60 120 240 ];
ht_capab = "[LDPC][SHORT-GI-20][SHORT-GI-40][TX-STBC][RX-STBC1][MAX-AMSDU-7935]";
} // hapdConfig;
};
router.interfaces.wlan1 = {
bridge = "br0";
hostapd.enable = true;
hostapd.settings = {
ssid = "${cfg.ssid}_5G";
ieee80211h = true;
hw_mode = "a";
channel = 36;
chanlist = [ 36 ];
tx_queue_data2_burst = 2;
ht_capab = "[HT40+][LDPC][SHORT-GI-20][SHORT-GI-40][TX-STBC][RX-STBC1][MAX-AMSDU-7935]";
vht_oper_chwidth = 1; # 80mhz ch width
vht_oper_centr_freq_seg0_idx = 42;
vht_capab = "[RXLDPC][SHORT-GI-80][SHORT-GI-160][TX-STBC-2BY1][SU-BEAMFORMER][SU-BEAMFORMEE][MU-BEAMFORMER][MU-BEAMFORMEE][RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN][RX-STBC-1][SOUNDING-DIMENSION-4][BF-ANTENNA-4][VHT160][MAX-MPDU-11454][MAX-A-MPDU-LEN-EXP7]";
} // hapdConfig;
};
router.interfaces.lan0 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
router.interfaces.lan1 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
router.interfaces.lan2 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
router.interfaces.lan3 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
router.interfaces.lan4 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
/*router.interfaces.lan5 = {
bridge = "br0";
systemdLinkMatchConfig.OriginalName = "eth1";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};*/
router.interfaces.wan = {
dependentServices = [
{ service = "wireguard-wg0"; inNetns = false; }
];
systemdLinkLinkConfig.MACAddressPolicy = "none";
systemdLinkLinkConfig.MACAddress = cfg.routerMac;
dhcpcd.enable = true;
networkNamespace = "wan";
};
networking.firewall.enable = false;
router.interfaces.br0 = {
dependentServices = [ { service = "unbound"; bindType = "wants"; } ];
ipv4.addresses = [ {
address = gatewayAddr4;
inherit (netParsedCidr4) prefixLength;
dns = [ gatewayAddr4 ];
keaSettings.reservations = map (res: {
hw-address = res.macAddress;
ip-address = res.ipAddress;
}) cfg.dhcpReservations;
} ];
ipv6.addresses = [ {
address = gatewayAddr6;
inherit (netParsedCidr6) prefixLength;
dns = [ gatewayAddr6 ];
gateways = [ gatewayAddr6 ];
radvdSettings.AdvAutonomous = true;
coreradSettings.autonomous = true;
# don't autoallocate addresses
keaSettings.pools = [ ];
# just assign the reservations
keaSettings.reservations = map (res: {
hw-address = res.macAddress;
ip-addresses = [ res.ipAddress ];
}) cfg.dhcp6Reservations;
} ];
ipv4.routes = [
{ extraArgs = [ netCidr4 "dev" "br0" "proto" "kernel" "scope" "link" "src" gatewayAddr4 "table" wan_table ]; }
# allow iot to contact ips inside the network
{ extraArgs = [ netCidr4 "dev" "br0" "proto" "kernel" "scope" "link" "src" gatewayAddr4 "table" iot_table ]; }
];
ipv6.routes = [
{ extraArgs = [ netCidr6 "dev" "br0" "proto" "kernel" "metric" "256" "pref" "medium" "table" wan_table ]; }
# allow iot to contact ips inside the network
{ extraArgs = [ netCidr6 "dev" "br0" "proto" "kernel" "metric" "256" "pref" "medium" "table" iot_table ]; }
];
ipv4.kea.enable = true;
ipv6.radvd.enable = true;
ipv6.kea.enable = true;
};
router.networkNamespaces.default = {
# set routing table depending on packet mark
rules = [
{ ipv6 = false; extraArgs = [ "fwmark" wan_table "table" wan_table ]; }
{ ipv6 = true; extraArgs = [ "fwmark" wan_table "table" wan_table ]; }
# don't add vpn_table as it should be the default
{ ipv6 = false; extraArgs = [ "fwmark" iot_table "table" iot_table ]; }
{ ipv6 = true; extraArgs = [ "fwmark" iot_table "table" iot_table ]; }
] ++ builtins.concatLists (map (rule: let
table = if rule.inVpn then 0 else wan_table;
forEachPort = func: port:
if builtins.isInt port then [ (func port) ]
else if port?set then builtins.concatLists (map (forEachPort func) port.set)
else if port?range.min then let inherit (port.range) min max; in [ (func "${toString min}-${toString max}") ]
else if port?range then let max = builtins.elemAt port.range 1; min = builtins.head port.range; in [ (func "${toString min}-${toString max}" ) ]
else throw "Unsupported expr: ${builtins.toJSON port}";
gen = len: proto: tgt:
forEachPort
(port: [ "from" "${tgt.address}/${toString len}" "ipproto" proto "sport" port "table" table ])
(if tgt.port == null then rule.port else tgt.port);
in lib.optionals (rule.tcp && rule.target4 != null) (map (x: { ipv6 = false; extraArgs = x; }) (gen 32 "tcp" rule.target4))
++ lib.optionals (rule.udp && rule.target4 != null) (map (x: { ipv6 = false; extraArgs = x; }) (gen 32 "udp" rule.target4))
++ lib.optionals (rule.tcp && rule.target6 != null) (map (x: { ipv6 = true; extraArgs = x; }) (gen 128 "tcp" rule.target6))
++ lib.optionals (rule.udp && rule.target6 != null) (map (x: { ipv6 = true; extraArgs = x; }) (gen 128 "udp" rule.target6))
) (builtins.filter (x: (x.tcp || x.udp) && dnatRuleMode x == "rule") cfg.dnatRules));
nftables.stopJsonRules = mkFlushRules {};
nftables.jsonRules = mkRules {
selfIp4 = gatewayAddr4;
selfIp6 = gatewayAddr6;
lans = [ "br0" ];
wans = [ "wg0" "veth-wan-a" ];
netdevIngressWanRules = with notnft.dsl; with payload; [
# check oif only from vpn
# dont check it from veth-wan-a because of dnat fuckery and because we already check packets coming from wan there
[(is.eq meta.iifname "wg0") (is.eq (fib (f: with f; [ saddr mark iif ]) (f: f.oif)) missing) (log "lan oif missing ") drop]
];
inetDnatRules =
builtins.concatLists (map
(rule: let
inherit (notnft.inetProtos) tcp udp;
protocols = if rule.tcp && rule.udp then notnft.dsl.set [ tcp udp ] else if rule.tcp then tcp else udp;
rule4 = rule.target4; rule6 = rule.target6;
in with notnft.dsl; with payload;
lib.optionals (rule4 != null) [
[ (is.eq meta.iifname "wg0") (is.eq ip.protocol protocols) (is.eq th.dport rule.port)
(if rule4.port == null then dnat.ip rule4.address else dnat.ip rule4.address rule4.port) ]
] ++ lib.optionals (rule6 != null) [
[ (is.eq meta.iifname "wg0") (is.eq ip6.protocol protocols) (is.eq th.dport rule.port)
(if rule6.port == null then dnat.ip6 rule6.address else dnat.ip6 rule6.address rule6.port) ]
])
(builtins.filter (x: x.inVpn && (x.tcp || x.udp)) cfg.dnatRules));
inetForwardRules = with notnft.dsl; with payload; [
# allow access to lan from the wan namespace
[(is.eq meta.iifname "veth-wan-a") (is.eq meta.oifname "br0") accept]
# allow dnat ("ct status dnat" doesn't work)
];
logPrefix = "lan ";
inetInboundWanRules = with notnft.dsl; with payload; [
[(is.eq th.dport 22) accept]
[(is.eq ip.saddr (cidr netnsCidr4)) accept]
[(is.eq ip6.saddr (cidr netnsCidr6)) accept]
];
extraInetEntries = with notnft.dsl; with payload; {
block4 = add set { type = f: f.ipv4_addr; flags = f: with f; [ interval ]; } [
(cidr "194.190.137.0" 24)
(cidr "194.190.157.0" 24)
(cidr "194.190.21.0" 24)
(cidr "194.226.130.0" 23)
];
block6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };
# those tables get populated by unbound
force_unvpn4 = add set { type = f: f.ipv4_addr; flags = f: with f; [ interval ]; };
force_vpn4 = add set { type = f: f.ipv4_addr; flags = f: with f; [ interval ]; };
force_unvpn6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };
force_vpn6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };
prerouting = add chain { type = f: f.filter; hook = f: f.prerouting; prio = f: f.filter; policy = f: f.accept; } ([
[(mangle meta.mark ct.mark)]
[(is.ne meta.mark 0) accept]
[(is.eq meta.iifname "br0") (mangle meta.mark vpn_table)]
[(is.eq ip.daddr "@force_unvpn4") (mangle meta.mark wan_table)]
[(is.eq ip6.daddr "@force_unvpn6") (mangle meta.mark wan_table)]
[(is.eq ip.daddr "@force_vpn4") (mangle meta.mark vpn_table)]
[(is.eq ip6.daddr "@force_vpn6") (mangle meta.mark vpn_table)]
] ++ # 1. dnat non-vpn: change rttable to wan
builtins.concatLists (map
(rule: let
inherit (notnft.inetProtos) tcp udp;
protocols = if rule.tcp && rule.udp then notnft.dsl.set [ tcp udp ] else if rule.tcp then tcp else udp;
rule4 = rule.target4; rule6 = rule.target6;
in with notnft.dsl; with payload;
lib.optionals (rule4 != null && rule4.address != gatewayAddr4) [
[ (is.eq meta.iifname "br0") (is.eq ip.protocol protocols) (is.eq ip.saddr rule4.address)
(is.eq th.sport (if rule4.port != null then rule4.port else rule.port)) (mangle meta.mark wan_table) ]
] ++ lib.optionals (rule6 != null && rule6.address != gatewayAddr6) [
[ (is.eq meta.iifname "br0") (is.eq ip6.nexthdr protocols) (is.eq ip6.saddr rule6.address)
(is.eq th.sport (if rule6.port != null then rule6.port else rule.port)) (mangle meta.mark wan_table) ]
])
(builtins.filter (x: !x.inVpn && (x.tcp || x.udp) && dnatRuleMode x == "mark") cfg.dnatRules))
++ # 2. dnat vpn: change rttable to vpn
builtins.concatLists (map
(rule: let
inherit (notnft.inetProtos) tcp udp;
protocols = if rule.tcp && rule.udp then notnft.dsl.set [ tcp udp ] else if rule.tcp then tcp else udp;
rule4 = rule.target4; rule6 = rule.target6;
in with notnft.dsl; with payload;
lib.optionals (rule4 != null && rule4.address != gatewayAddr4) [
[ (is ct.status (f: f.dnat)) (is.eq meta.iifname "br0") (is.eq ip.protocol protocols) (is.eq ip.saddr rule4.address)
(is.eq th.sport (if rule4.port != null then rule4.port else rule.port)) (mangle meta.mark vpn_table) ]
] ++ lib.optionals (rule6 != null && rule6.address != gatewayAddr6) [
[ (is ct.status (f: f.dnat)) (is.eq meta.iifname "br0") (is.eq ip6.protocol protocols) (is.eq ip6.saddr rule6.address)
(is.eq th.sport (if rule6.port != null then rule6.port else rule.port)) (mangle meta.mark vpn_table) ]
])
(builtins.filter (x: x.inVpn && (x.tcp || x.udp) && dnatRuleMode x == "mark") cfg.dnatRules))
++ [
[(is.eq ip.daddr "@block4") drop]
[(is.eq ip6.daddr "@block6") drop]
[(is.eq ether.saddr cfg.vacuumMac) (mangle meta.mark iot_table)]
[(mangle ct.mark meta.mark)]
]);
};
};
};
router.veths.veth-wan-a.peerName = "veth-wan-b";
router.interfaces.veth-wan-a = {
ipv4.addresses = [ netnsParsedCidr4 ];
ipv6.addresses = [ netnsParsedCidr6 ];
ipv4.routes = [
# default config duplicated for wan_table
{ extraArgs = [ netnsCidr4 "dev" "veth-wan-a" "proto" "kernel" "scope" "link" "src" mainNetnsAddr4 "table" wan_table ]; }
# default all traffic to wan in wan_table
{ extraArgs = [ "default" "via" wanNetnsAddr4 "table" wan_table ]; }
];
ipv6.routes = [
# default config duplicated for wan_table
{ extraArgs = [ netnsCidr6 "dev" "veth-wan-a" "proto" "kernel" "metric" "256" "pref" "medium" "table" wan_table ]; }
# default all traffic to wan in wan_table
{ extraArgs = [ "default" "via" wanNetnsAddr6 "table" wan_table ]; }
];
};
router.interfaces.veth-wan-b = {
networkNamespace = "wan";
ipv4.addresses = [ {
address = wanNetnsAddr4;
inherit (netnsParsedCidr4) prefixLength;
} ];
ipv6.addresses = [ {
address = wanNetnsAddr6;
inherit (netnsParsedCidr6) prefixLength;
} ];
ipv4.routes = [
{ extraArgs = [ netCidr4 "via" mainNetnsAddr4 ]; }
];
ipv6.routes = [
{ extraArgs = [ netCidr6 "via" mainNetnsAddr6 ]; }
];
};
router.networkNamespaces.wan = {
nftables.stopJsonRules = mkFlushRules {};
nftables.jsonRules = mkRules {
selfIp4 = wanNetnsAddr4;
selfIp6 = wanNetnsAddr6;
lans = [ "veth-wan-b" ];
wans = [ "wan" ];
netdevIngressWanRules = with notnft.dsl; with payload; [
[(is.eq (fib (f: with f; [ saddr mark iif ]) (f: f.oif)) missing) (log "wan oif missing ") drop]
];
inetDnatRules =
builtins.concatLists (map
(rule: let
inherit (notnft.inetProtos) tcp udp;
protocols = if rule.tcp && rule.udp then notnft.dsl.set [ tcp udp ] else if rule.tcp then tcp else udp;
rule4 = rule.target4; rule6 = rule.target6;
in with notnft.dsl; with payload;
lib.optionals (rule4 != null) [
[ (is.eq meta.iifname "wan") (is.eq ip.protocol protocols) (is.eq th.dport rule.port)
(if rule4.port == null then dnat.ip rule4.address else dnat.ip rule4.address rule4.port) ]
] ++ lib.optionals (rule6 != null) [
[ (is.eq meta.iifname "wan") (is.eq ip6.nexthdr protocols) (is.eq th.dport rule.port)
(if rule6.port == null then dnat.ip6 rule6.address else dnat.ip6 rule6.address rule6.port) ]
])
(builtins.filter (x: !x.inVpn && (x.tcp || x.udp)) cfg.dnatRules));
inetSnatRules =
# historically, i needed this, now i switched to ip rules
# if i ever need this again, i have it right here
builtins.concatLists (map
(rule: let
inherit (notnft.inetProtos) tcp udp;
protocols = if rule.tcp && rule.udp then notnft.dsl.set [ tcp udp ] else if rule.tcp then tcp else udp;
rule4 = rule.target4; rule6 = rule.target6;
in with notnft.dsl; with payload;
lib.optionals (rule4 != null && rule4.address == gatewayAddr4) [
[ (is.eq meta.iifname "wan") (is.eq meta.oifname "veth-wan-b") (is.eq ip.protocol protocols)
(is.eq th.dport (if rule4.port != null then rule4.port else rule.port)) (is.eq ip.daddr rule4.address) masquerade ]
] ++ lib.optionals (rule6 != null && rule6.address == gatewayAddr6) [
[ (is.eq meta.iifname "wan") (is.eq meta.oifname "veth-wan-b") (is.eq ip6.nexthdr protocols)
(is.eq th.dport (if rule6.port != null then rule6.port else rule.port)) (is.eq ip6.daddr rule6.address) masquerade ]
])
(builtins.filter (x: !x.inVpn && (x.tcp || x.udp) && dnatRuleMode x == "snat") cfg.dnatRules));
logPrefix = "wan ";
inetInboundWanRules = with notnft.dsl; with payload; [
# DHCP
[(is.eq meta.nfproto (x: x.ipv4)) (is.eq udp.dport 68) accept]
[(is.eq meta.nfproto (x: x.ipv6)) (is.eq udp.dport 546) accept]
# igmp, used for setting up multicast groups
[(is.eq ip.protocol (f: f.igmp)) accept]
# accept router solicitation stuff
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.type (f: with f; set [ nd-router-solicit nd-router-advert ])) accept]
# accept neighbor solicitation stuff
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.code (f: f.no-route))
(is.eq icmpv6.type (f: with f; set [ nd-neighbor-solicit nd-neighbor-advert ]))
accept]
# SSH
[(is.eq th.dport 22) accept]
];
};
};
networking.wireguard.interfaces.wg0 = cfg.wireguard // {
socketNamespace = "wan";
interfaceNamespace = "init";
};
networking.resolvconf.extraConfig = ''
name_servers="${mainNetnsAddr4} ${mainNetnsAddr6}"
'';
users.users.${config.common.mainUsername}.extraGroups = [ config.services.unbound.group ];
services.unbound = {
enable = true;
package = pkgs.unbound-with-systemd.override {
stdenv = pkgs.ccacheStdenv;
withPythonModule = true;
python = unbound-python;
};
localControlSocketPath = "/run/unbound/unbound.ctl";
# we override resolvconf above manually
resolveLocalQueries = false;
settings = {
server = {
interface = [ mainNetnsAddr4 mainNetnsAddr6 gatewayAddr4 gatewayAddr6 ];
access-control = [ "${netnsCidr4} allow" "${netnsCidr6} allow" "${netCidr4} allow" "${netCidr6} allow" ];
aggressive-nsec = true;
do-ip6 = true;
module-config = ''"validator python iterator"'';
local-zone = [
''"local." static''
''"${cfg.domainName}." typetransparent''
];
local-data = builtins.concatLists (map (domain:
[
''"${domain}. A ${serverAddress4}"''
''"${domain}. AAAA ${serverAddress6}"''
]) hosted-domains);
};
python.python-script = toString ./avahi-resolver-v2.py;
remote-control.control-enable = true;
};
};
networking.hosts."${serverAddress4}" = hosted-domains;
networking.hosts."${serverAddress6}" = hosted-domains;
systemd.services.unbound = {
environment.PYTHONPATH = "${unbound-python}/${unbound-python.sitePackages}";
environment.MDNS_ACCEPT_NAMES = "^.*\\.local\\.$";
# load vpn_domains.json and vpn_ips.json, as well as unvpn_domains.json and unvpn_ips.json
# resolve domains and append it to ips and add it to the nftables sets
environment.NFT_QUERIES = "vpn:force_vpn4,force_vpn6;unvpn:force_unvpn4,force_unvpn6";
# it needs to run after nftables has been set up because it sets up the sets
after = [ "nftables-default.service" ];
wants = [ "nftables-default.service" ];
# allow it to call nft
serviceConfig.AmbientCapabilities = [ "CAP_NET_ADMIN" ];
};
systemd.services.update-rkn-blacklist = {
# fetch vpn_ips.json and vpn_domains.json for unbound
script = ''
BLACKLIST=$(${pkgs.coreutils}/bin/mktemp) || exit 1
${pkgs.curl}/bin/curl "https://reestr.rublacklist.net/api/v2/ips/json/" -o "$BLACKLIST" || exit 1
chown unbound:unbound "$BLACKLIST" && mv "$BLACKLIST" /var/lib/unbound/vpn_ips.json
${pkgs.curl}/bin/curl "https://reestr.rublacklist.net/api/v2/domains/json/" -o "$BLACKLIST" || exit 1
chown unbound:unbound "$BLACKLIST" && mv "$BLACKLIST" /var/lib/unbound/vpn_domains.json
'';
serviceConfig = {
Type = "oneshot";
};
};
systemd.timers.update-rkn-blacklist = {
wantedBy = [ "timers.target" ];
partOf = [ "update-rkn-blacklist.service" ];
# slightly unusual time to reduce server load
timerConfig.OnCalendar = [ "*-*-* 00:00:00" ]; # every day
timerConfig.RandomizedDelaySec = 43200; # execute at random time in the first 12 hours
};
# run an extra sshd so we can connect even if forwarding/routing between namespaces breaks
# i don't want to connect by uart each time something goes wrong
systemd.services.sshd-wan = {
description = "SSH Daemon (WAN)";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "netns-wan.service" ];
bindsTo = [ "netns-wan.service" ];
stopIfChanged = false;
path = with pkgs; [ gawk config.programs.ssh.package ];
environment.LD_LIBRARY_PATH = config.system.nssModules.path;
restartTriggers = [ config.environment.etc."ssh/sshd_config".source ];
preStart = config.systemd.services.sshd.preStart;
serviceConfig = {
ExecStart = "${config.programs.ssh.package}/bin/sshd -D -f /etc/ssh/sshd_config";
KillMode = "process";
Restart = "always";
Type = "simple";
NetworkNamespacePath = "/var/run/netns/wan";
};
};
services.printing = {
enable = true;
allowFrom = [ "localhost" netCidr4 netCidr6 ];
browsing = true;
clientConf = ''
ServerName router.local
'';
defaultShared = true;
drivers = [ pkgs.hplip ];
startWhenNeeded = false;
};
services.avahi = {
enable = true;
hostName = "router";
allowInterfaces = [ "br0" ];
publish = {
enable = true;
addresses = true;
domain = true;
userServices = true;
};
};
# it takes a stupidly long time when done via qemu
# (also it's supposed to be disabled by default but it was enabled for me, why?)
documentation.man.generateCaches = false;
}