dotfiles/system/hosts/router/default.nix

844 lines
39 KiB
Nix
Raw Normal View History

2023-05-17 15:27:00 +07:00
{ config
2023-06-24 07:12:11 +07:00
, pkgs
, notnft
, lib
, router-lib
, server-config
2023-05-17 15:27:00 +07:00
, ... }:
let
2023-06-20 15:11:01 +07:00
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
};
2023-06-24 07:12:11 +07:00
# 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;
dnatRuleMode = rule:
if rule.mode != "" then rule.mode
else if rule.target4.address or null == netAddresses.lan4 || rule.target6.address or null == netAddresses.lan6 then "rule"
2023-06-24 07:12:11 +07:00
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)
# inetInboundLanRules = same for lan
2023-06-24 07:12:11 +07:00
# 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 ? [],
inetInboundLanRules ? [],
2023-06-24 07:12:11 +07:00
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 ignore if they're multicast/broadcast
2023-06-24 07:12:11 +07:00
[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]
inetInboundLanRules
2023-06-24 07:12:11 +07:00
[(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]
# separate limits for echo-request and all other icmp types
[(is.eq ip.protocol (f: f.icmp)) (is.eq icmp.type (f: f.echo-request)) (limit { rate = 50; per = f: f.second; }) accept]
[(is.eq ip.protocol (f: f.icmp)) (is.ne icmp.type (f: f.echo-request)) (limit { rate = 100; per = f: f.second; }) accept]
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.type (f: f.echo-request)) (limit { rate = 50; per = f: f.second; }) accept]
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.ne icmpv6.type (f: f.echo-request)) (limit { rate = 100; per = f: f.second; }) accept]
# always accept destination unreachable and time-exceeded
[(is.eq ip.protocol (f: f.icmp)) (is.eq icmp.type (f: with f; set [ destination-unreachable time-exceeded ])) accept]
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.type (f: with f; set [ destination-unreachable time-exceeded ])) accept]
# don't log echo-request drops
[(is.eq ip.protocol (f: f.icmp)) (is.eq icmp.type (f: f.echo-request)) drop]
[(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.type (f: f.echo-request)) drop]
2023-06-24 07:12:11 +07:00
[(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: f.echo-request)) accept]
2023-06-24 07:12:11 +07:00
# 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 ")];
2023-06-24 07:12:11 +07:00
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);
};
netAddressesWithPrefixLen = {
lan4 = cfg.network;
lan6 = cfg.network6;
netns4 = cfg.netnsNet;
netns6 = cfg.netnsNet6;
};
2023-06-24 07:12:11 +07:00
# parse a.b.c.d/x into { address, prefixLength }
netParsedCidrs = builtins.mapAttrs (_: router-lib.parseCidr) netAddressesWithPrefixLen;
2023-06-24 07:12:11 +07:00
# generate network cidr from device address
# (normalizeCidr applies network mask to the address)
netCidrs = builtins.mapAttrs (_: v: router-lib.serializeCidr (router-lib.normalizeCidr v)) netParsedCidrs;
2023-06-24 07:12:11 +07:00
2023-07-12 14:27:16 +07:00
netAddresses = builtins.mapAttrs (_: v: v.address) netParsedCidrs // {
netnsWan4 = cfg.wanNetnsAddr;
netnsWan6 = cfg.wanNetnsAddr6;
};
2023-06-24 07:12:11 +07:00
parsedGatewayAddr4 = router-lib.parseIp4 netAddresses.lan4;
parsedGatewayAddr6 = router-lib.parseIp6 netAddresses.lan6;
2023-06-24 07:12:11 +07:00
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;
hosted-domains =
builtins.concatLists
(builtins.attrValues
(builtins.mapAttrs
(k: v: [ k ] ++ v.serverAliases)
server-config.services.nginx.virtualHosts));
2023-05-17 15:27:00 +07:00
in {
imports = [ ./options.nix ./metrics.nix ];
system.stateVersion = "22.11";
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;
ignoreIP = [ netCidrs.lan4 netCidrs.lan6 ];
maxretry = 10;
};
2023-06-24 07:12:11 +07:00
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; }
];
# dnat to server, take ports from its firewall config
router-settings.dnatRules = let
bannedPorts = [
631 9100 # printing
5353 # avahi
];
inherit (server-config.networking.firewall) allowedTCPPorts allowedTCPPortRanges allowedUDPPorts allowedUDPPortRanges;
tcpAndUdp = builtins.filter (x: !builtins.elem x bannedPorts && builtins.elem x allowedTCPPorts) allowedUDPPorts;
tcpOnly = builtins.filter (x: !builtins.elem x (bannedPorts ++ allowedUDPPorts)) allowedTCPPorts;
udpOnly = builtins.filter (x: !builtins.elem x (bannedPorts ++ allowedTCPPorts)) allowedUDPPorts;
rangesTcpAndUdp = builtins.filter (x: builtins.elem x allowedTCPPortRanges) allowedUDPPortRanges;
rangesTcpOnly = builtins.filter (x: !builtins.elem x allowedUDPPortRanges) allowedTCPPortRanges;
rangesUdpOnly = builtins.filter (x: !builtins.elem x allowedTCPPortRanges) allowedUDPPortRanges;
in lib.optional (tcpAndUdp != [ ]) {
port = notnft.dsl.set tcpAndUdp; tcp = true; udp = true;
target4.address = serverAddress4; target6.address = serverAddress6;
} ++ lib.optional (tcpOnly != [ ]) {
port = notnft.dsl.set tcpOnly; tcp = true; udp = false;
target4.address = serverAddress4; target6.address = serverAddress6;
} ++ lib.optional (udpOnly != [ ]) {
port = notnft.dsl.set udpOnly; tcp = false; udp = true;
target4.address = serverAddress4; target6.address = serverAddress6;
} ++ map (range: {
port = notnft.dsl.range range.from range.to; tcp = true; udp = true;
target4.address = serverAddress4; target6.address = serverAddress6;
}) rangesTcpAndUdp ++ map (range: {
port = notnft.dsl.range range.from range.to; tcp = true; udp = false;
target4.address = serverAddress4; target6.address = serverAddress6;
}) rangesTcpOnly ++ map (range: {
port = notnft.dsl.range range.from range.to; tcp = false; udp = true;
target4.address = serverAddress4; target6.address = serverAddress6;
}) rangesUdpOnly;
2023-06-24 07:12:11 +07:00
2023-06-20 15:11:01 +07:00
router.enable = true;
# 2.4g ap
2023-06-20 15:11:01 +07:00
router.interfaces.wlan0 = {
bridge = "br0";
hostapd.enable = true;
hostapd.settings = {
inherit (cfg) ssid;
hw_mode = "g";
channel = 3;
chanlist = [ 3 ];
2023-06-20 15:11:01 +07:00
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;
};
# 5g ap
2023-06-20 15:11:01 +07:00
router.interfaces.wlan1 = {
bridge = "br0";
hostapd.enable = true;
hostapd.settings = {
2023-06-24 07:12:11 +07:00
ssid = "${cfg.ssid}_5G";
2023-06-20 15:11:01 +07:00
ieee80211h = true;
hw_mode = "a";
channel = 60;
chanlist = [ 60 ];
2023-06-20 15:11:01 +07:00
tx_queue_data2_burst = 2;
ht_capab = "[HT40+][LDPC][SHORT-GI-20][SHORT-GI-40][TX-STBC][RX-STBC1][MAX-AMSDU-7935]";
2023-06-24 07:12:11 +07:00
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]";
2023-06-20 15:11:01 +07:00
} // hapdConfig;
};
# ethernet lan0-3
2023-06-20 15:11:01 +07:00
router.interfaces.lan0 = {
2023-06-24 07:12:11 +07:00
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
router.interfaces.lan1 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
2023-06-20 15:11:01 +07:00
};
2023-06-24 07:12:11 +07:00
router.interfaces.lan2 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
router.interfaces.lan3 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
# sfp lan4
2023-06-24 07:12:11 +07:00
router.interfaces.lan4 = {
bridge = "br0";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
/*
# sfp lan5
router.interfaces.lan5 = {
2023-06-24 07:12:11 +07:00
bridge = "br0";
# i could try to figure out why this doesn't work... but i don't even have sfp to plug into this
2023-06-24 07:12:11 +07:00
systemdLinkMatchConfig.OriginalName = "eth1";
systemdLinkLinkConfig.MACAddressPolicy = "persistent";
};
*/
# ethernet wan
2023-06-24 07:12:11 +07:00
router.interfaces.wan = {
dependentServices = [
{ service = "wireguard-wg0"; inNetns = false; }
];
systemdLinkLinkConfig.MACAddressPolicy = "none";
systemdLinkLinkConfig.MACAddress = cfg.routerMac;
dhcpcd = {
enable = true;
# technically this should be assigned to br0 instead of veth-wan-b
# however, br0 is in a different namespace!
# Considering this doesn't work at all because my ISP doesn't offer IPv6,
# I'd say this is "good enough" since it might still work in the wan
# namespace, though I can't test it.
extraConfig = ''
interface wan
ipv6rs
ia_na 0
ia_pd 1 veth-wan-b/0
'';
};
2023-06-24 07:12:11 +07:00
networkNamespace = "wan";
2023-06-20 15:11:01 +07:00
};
# disable default firewall as it uses iptables
# (and we have our own firewall)
2023-06-24 07:12:11 +07:00
networking.firewall.enable = false;
# br0, which bridges all lan devices
# this is "the" lan device
2023-06-20 15:11:01 +07:00
router.interfaces.br0 = {
2023-06-24 07:12:11 +07:00
dependentServices = [ { service = "unbound"; bindType = "wants"; } ];
2023-07-12 14:27:16 +07:00
ipv4.addresses = lib.toList (netParsedCidrs.lan4 // {
dns = [ netAddresses.lan4 ];
2023-06-24 07:12:11 +07:00
keaSettings.reservations = map (res: {
hw-address = res.macAddress;
ip-address = res.ipAddress;
}) cfg.dhcpReservations;
2023-07-12 14:27:16 +07:00
});
ipv6.addresses = lib.toList (netParsedCidrs.lan6 // {
dns = [ netAddresses.lan6 ];
gateways = [ netAddresses.lan6 ];
2023-06-24 07:12:11 +07:00
radvdSettings.AdvAutonomous = true;
coreradSettings.autonomous = true;
# don't autoallocate addresses, keep autonomous ones
2023-06-24 07:12:11 +07:00
keaSettings.pools = [ ];
# just assign the reservations
keaSettings.reservations = map (res: {
hw-address = res.macAddress;
ip-addresses = [ res.ipAddress ];
}) cfg.dhcp6Reservations;
2023-07-12 14:27:16 +07:00
});
2023-06-24 07:12:11 +07:00
ipv4.routes = [
{ extraArgs = [ netCidrs.lan4 "dev" "br0" "proto" "kernel" "scope" "link" "src" netAddresses.lan4 "table" wan_table ]; }
2023-06-24 07:12:11 +07:00
];
ipv6.routes = [
{ extraArgs = [ netCidrs.lan6 "dev" "br0" "proto" "kernel" "metric" "256" "pref" "medium" "table" wan_table ]; }
2023-06-24 07:12:11 +07:00
];
2023-06-20 15:11:01 +07:00
ipv4.kea.enable = true;
ipv6.corerad.enable = true;
2023-06-24 07:12:11 +07:00
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 ]; }
# below is dnat config
] ++ 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 rules
# things to note: this has the code for switching between rtables
# otherwise, boring stuff
2023-06-24 07:12:11 +07:00
nftables.jsonRules = mkRules {
selfIp4 = netAddresses.lan4;
selfIp6 = netAddresses.lan6;
2023-06-24 07:12:11 +07:00
lans = [ "br0" ];
wans = [ "wg0" "veth-wan-a" ];
logPrefix = "lan ";
2023-06-24 07:12:11 +07:00
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)
];
inetInboundWanRules = with notnft.dsl; with payload; [
[(is.eq ip.saddr (cidr netCidrs.netns4)) accept]
[(is.eq ip6.saddr (cidr netCidrs.netns6)) accept]
2023-06-24 07:12:11 +07:00
];
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_unvpn6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };
force_vpn4 = add set { type = f: f.ipv4_addr; flags = f: with f; [ interval ]; };
2023-06-24 07:12:11 +07:00
force_vpn6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };
allow_iot4 = add set { type = f: f.ipv4_addr; flags = f: with f; [ interval ]; };
allow_iot6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };
2023-06-24 07:12:11 +07:00
# TODO: is type=route hook=output better? It might help get rid of the routing inconsistency
2023-07-21 01:54:58 +07:00
# between router-originated and forwarded traffic. The problem is type=route isn't supported
# for family=inet, so I don't care enough to test it right now.
2023-06-24 07:12:11 +07:00
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)]
# don't vpn smtp requests so spf works fine (and in case the vpn blocks requests over port 25)
[(is.eq ip.saddr serverAddress4) (is.eq ip.protocol (f: f.tcp)) (is.eq tcp.dport 25) (mangle meta.mark wan_table)]
[(is.eq ip6.saddr serverAddress6) (is.eq ip6.nexthdr (f: f.tcp)) (is.eq tcp.dport 25) (mangle meta.mark wan_table)]
# but block requests to port 25 from other hosts so they can't send mail pretending to originate from my domain
[(is.ne ip.saddr serverAddress4) (is.eq ip.protocol (f: f.tcp)) (is.eq tcp.dport 25) drop]
[(is.ne ip6.saddr serverAddress6) (is.eq ip6.nexthdr (f: f.tcp)) (is.eq tcp.dport 25) drop]
2023-06-24 07:12:11 +07:00
[(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) [
2023-06-24 07:12:11 +07:00
[ (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) [
2023-06-24 07:12:11 +07:00
[ (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) [
2023-06-24 07:12:11 +07:00
[ (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) [
2023-06-24 07:12:11 +07:00
[ (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]
# this doesn't work... it still gets routed, even though iot_table doesn't have a default route
# [(is.eq ip.saddr vacuumAddress4) (is.ne ip.daddr) (mangle meta.mark iot_table)]
# [(is.eq ether.saddr cfg.vacuumMac) (mangle meta.mark iot_table)]
# instead of debugging that, simply change the approach
[(is.eq ether.saddr cfg.vacuumMac) (is.ne ip.daddr (cidr netCidrs.lan4)) (is.ne ip.daddr "@allow_iot4") (log "iot4 ") drop]
[(is.eq ether.saddr cfg.vacuumMac) (is.ne ip6.daddr (cidr netCidrs.lan6)) (is.ne ip6.daddr "@allow_iot6") (log "iot6 ") drop]
2023-06-24 07:12:11 +07:00
[(mangle ct.mark meta.mark)]
]);
};
};
};
# veths are virtual ethernet cables
# veth-wan-a - located in the default namespace
# veth-wan-b - located in the wan namespace
# this allows routing traffic to wan namespace from default namespace via veth-wan-a
# (and vice versa)
2023-06-24 07:12:11 +07:00
router.veths.veth-wan-a.peerName = "veth-wan-b";
router.interfaces.veth-wan-a = {
ipv4.addresses = [ netParsedCidrs.netns4 ];
ipv6.addresses = [ netParsedCidrs.netns6 ];
2023-06-24 07:12:11 +07:00
ipv4.routes = [
# default config duplicated for wan_table
{ extraArgs = [ netCidrs.netns4 "dev" "veth-wan-a" "proto" "kernel" "scope" "link" "src" netAddresses.netns4 "table" wan_table ]; }
2023-06-24 07:12:11 +07:00
# default all traffic to wan in wan_table
2023-07-12 14:27:16 +07:00
{ extraArgs = [ "default" "via" netAddresses.netnsWan4 "table" wan_table ]; }
2023-06-24 07:12:11 +07:00
];
ipv6.routes = [
# default config duplicated for wan_table
{ extraArgs = [ netCidrs.netns6 "dev" "veth-wan-a" "proto" "kernel" "metric" "256" "pref" "medium" "table" wan_table ]; }
2023-06-24 07:12:11 +07:00
# default all traffic to wan in wan_table
2023-07-12 14:27:16 +07:00
{ extraArgs = [ "default" "via" netAddresses.netnsWan6 "table" wan_table ]; }
2023-06-24 07:12:11 +07:00
];
2023-06-20 15:11:01 +07:00
};
2023-06-24 07:12:11 +07:00
router.interfaces.veth-wan-b = {
networkNamespace = "wan";
ipv4.addresses = [ {
2023-07-12 14:27:16 +07:00
address = netAddresses.netnsWan4;
inherit (netParsedCidrs.netns4) prefixLength;
2023-06-24 07:12:11 +07:00
} ];
ipv6.addresses = [ {
2023-07-12 14:27:16 +07:00
address = netAddresses.netnsWan6;
inherit (netParsedCidrs.netns6) prefixLength;
2023-06-24 07:12:11 +07:00
} ];
# allow wan->default namespace communication
2023-06-24 07:12:11 +07:00
ipv4.routes = [
{ extraArgs = [ netCidrs.lan4 "via" netAddresses.netns4 ]; }
2023-06-24 07:12:11 +07:00
];
ipv6.routes = [
{ extraArgs = [ netCidrs.lan6 "via" netAddresses.netns6 ]; }
2023-06-24 07:12:11 +07:00
];
};
router.networkNamespaces.wan = {
# this is the even more boring nftables config
2023-06-24 07:12:11 +07:00
nftables.jsonRules = mkRules {
2023-07-12 14:27:16 +07:00
selfIp4 = netAddresses.netnsWan4;
selfIp6 = netAddresses.netnsWan6;
2023-06-24 07:12:11 +07:00
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) [
2023-06-24 07:12:11 +07:00
[ (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) [
2023-06-24 07:12:11 +07:00
[ (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 tcp.dport 23) accept]
2023-06-24 07:12:11 +07:00
];
};
};
# vpn socket is in wan namespace, meaning traffic gets sent through the wan namespace
# vpn interface is in default namespace, meaning it can be used in the default namespace
2023-06-24 07:12:11 +07:00
networking.wireguard.interfaces.wg0 = cfg.wireguard // {
socketNamespace = "wan";
interfaceNamespace = "init";
};
# use main netns's address instead of 127.0.0.1
# this ensures all network namespaces can access it
2023-06-24 07:12:11 +07:00
networking.resolvconf.extraConfig = ''
name_servers="${netAddresses.netns4} ${netAddresses.netns6}"
2023-06-24 07:12:11 +07:00
'';
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 = pkgs.python3;
2023-06-24 07:12:11 +07:00
};
localControlSocketPath = "/run/unbound/unbound.ctl";
# we override resolvconf above manually
resolveLocalQueries = false;
settings = {
server = rec {
interface = [ netAddresses.netns4 netAddresses.netns6 netAddresses.lan4 netAddresses.lan6 ];
access-control = [ "${netCidrs.netns4} allow" "${netCidrs.netns6} allow" "${netCidrs.lan4} allow" "${netCidrs.lan6} allow" ];
2023-06-24 07:12:11 +07:00
aggressive-nsec = true;
do-ip6 = true;
module-config = ''"validator python iterator"'';
local-zone = [
# incompatible with avahi resolver
# ''"local." static''
''"${server-config.server.domainName}." typetransparent''
2023-06-24 07:12:11 +07:00
];
local-data = builtins.concatLists (map (domain:
[
''"${domain}. A ${serverAddress4}"''
''"${domain}. AAAA ${serverAddress6}"''
]) hosted-domains);
# incompatible with avahi resolver
# ++ [
# ''"retracker.local. A ${netAddresses.lan4}"''
# ''"retracker.local. AAAA ${netAddresses.lan6}"''
# ];
# performance tuning
num-threads = 4; # cpu core count
msg-cache-slabs = 4; # nearest power of 2 to num-threads
rrset-cache-slabs = msg-cache-slabs;
infra-cache-slabs = msg-cache-slabs;
key-cache-slabs = msg-cache-slabs;
so-reuseport = true;
msg-cache-size = "50m"; # (default 4m)
rrset-cache-size = "100m"; # msg*2 (default 4m)
# timeouts
unknown-server-time-limit = 752; # default=376
2023-06-24 07:12:11 +07:00
};
# normally it would refer to the flake path, but then the service changes on every flake update
# instead, write a new file in nix store
python.python-script = builtins.toFile "avahi-resolver-v2.py" (builtins.readFile ./avahi-resolver-v2.py);
2023-06-24 07:12:11 +07:00
remote-control.control-enable = true;
};
};
environment.etc."unbound/iot_ips.json".text = builtins.toJSON [
# local multicast
"224.0.0.0/24"
];
environment.etc."unbound/iot_domains.json".text = builtins.toJSON [
# ntp time sync
"pool.ntp.org"
# valetudo update check
"api.github.com" "github.com" "*.githubusercontent.com"
];
2023-06-24 07:12:11 +07:00
networking.hosts."${serverAddress4}" = hosted-domains;
networking.hosts."${serverAddress6}" = hosted-domains;
systemd.services.unbound = lib.mkIf config.services.unbound.enable {
environment.PYTHONPATH = let
unbound-python = pkgs.python3.withPackages (ps: with ps; [ pydbus dnspython requests pytricia nftables ]);
in
"${unbound-python}/${unbound-python.sitePackages}";
environment.MDNS_ACCEPT_NAMES = "^(.*\\.)?local\\.$";
# resolve retracker.local to whatever router.local resolves to
# we can't add a local zone alongside using avahi resolver, so we have to use hacks like this
environment.DOMAIN_NAME_OVERRIDES = "retracker.local->router.local";
2023-06-24 07:12:11 +07:00
# 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;iot:allow_iot4,allow_iot6";
serviceConfig.EnvironmentFile = "/secrets/unbound_env";
# it needs to run after nftables has been set up because it sets up the sets
after = [ "nftables-default.service" "avahi-daemon.service" ];
wants = [ "nftables-default.service" "avahi-daemon.service" ];
2023-06-24 07:12:11 +07:00
# 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
${pkgs.jq}/bin/jq ".[0:0]" "$BLACKLIST" && chown unbound:unbound "$BLACKLIST" && mv "$BLACKLIST" /var/lib/unbound/vpn_ips.json
2023-06-24 07:12:11 +07:00
${pkgs.curl}/bin/curl "https://reestr.rublacklist.net/api/v2/domains/json/" -o "$BLACKLIST" || exit 1
${pkgs.jq}/bin/jq ".[0:0]" "$BLACKLIST" && chown unbound:unbound "$BLACKLIST" && mv "$BLACKLIST" /var/lib/unbound/vpn_domains.json
2023-06-24 07:12:11 +07:00
'';
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
# (use port 23 because 22 is forwarded to the server)
2023-06-24 07:12:11 +07:00
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 -p 23";
2023-06-24 07:12:11 +07:00
KillMode = "process";
Restart = "always";
Type = "simple";
NetworkNamespacePath = "/var/run/netns/wan";
};
};
services.printing = {
enable = true;
allowFrom = [ "localhost" netCidrs.lan4 netCidrs.lan6 ];
2023-06-24 07:12:11 +07:00
browsing = true;
clientConf = ''
ServerName router.local
'';
defaultShared = true;
drivers = [ pkgs.hplip ];
startWhenNeeded = false;
};
# share printers (and allow unbound to resolve .local)
2023-06-24 07:12:11 +07:00
services.avahi = {
enable = true;
hostName = "router";
allowInterfaces = [ "br0" ];
publish = {
enable = true;
addresses = true;
domain = true;
userServices = true;
};
};
services.iperf3 = {
enable = true;
bind = netAddresses.lan4;
};
services.opentracker = {
enable = true;
extraOptions = "-i ${netAddresses.lan4} -p 6969 -P 6969 -p 80";
};
2023-06-24 07:12:11 +07:00
# 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;
impermanence.directories = [
# for wireguard key
{ directory = /secrets; mode = "0000"; }
# my custom impermanence module doesnt detect it
{ directory = /var/db/dhcpcd; mode = "0755"; }
{ directory = /var/lib/private/kea; mode = "0750"; }
];
2023-05-17 15:27:00 +07:00
}