diff --git a/configurations/nixos/x86_64-linux/alnitak.nix b/configurations/nixos/x86_64-linux/alnitak.nix index 1ef57080d..ef2e5f780 100644 --- a/configurations/nixos/x86_64-linux/alnitak.nix +++ b/configurations/nixos/x86_64-linux/alnitak.nix @@ -43,27 +43,27 @@ args.auth-key = "file:/var/run/agenix/ts-google-9k"; }; - services.jae.router = { - enable = true; - useNextDns = true; - nextDnsEnvFile = "/var/run/agenix/nextdns"; - restrictedMacs = [ - "5c:e0:c5:8a:24:6a" - "b4:18:d1:ab:4e:5a" - ]; - upstreamDnsServers = [ - "2a07:a8c1::" - "45.90.30.0" - "2a07:a8c0::" - "45.90.28.0" - ]; - externalInterface = "enp1s0"; - internalInterface = "enp2s0"; - internalInterfaceIP = "192.168.20.1"; - dnsMasqSettings.no-resolv = true; - dnsMasqSettings.bogus-priv = true; - dnsMasqSettings.strict-order = true; - }; + # services.jae.router = { + # enable = true; + # useNextDns = true; + # nextDnsEnvFile = "/var/run/agenix/nextdns"; + # restrictedMacs = [ + # "5c:e0:c5:8a:24:6a" + # "b4:18:d1:ab:4e:5a" + # ]; + # upstreamDnsServers = [ + # "2a07:a8c1::" + # "45.90.30.0" + # "2a07:a8c0::" + # "45.90.28.0" + # ]; + # externalInterface = "enp1s0"; + # internalInterface = "enp2s0"; + # internalInterfaceIP = "192.168.20.1"; + # dnsMasqSettings.no-resolv = true; + # dnsMasqSettings.bogus-priv = true; + # dnsMasqSettings.strict-order = true; + # }; age.secrets = { ts-google-9k = { diff --git a/configurations/nixos/x86_64-linux/cygnus.nix b/configurations/nixos/x86_64-linux/cygnus.nix index 2b8b0e629..aa43d5796 100644 --- a/configurations/nixos/x86_64-linux/cygnus.nix +++ b/configurations/nixos/x86_64-linux/cygnus.nix @@ -67,8 +67,6 @@ networkConfig.IgnoreCarrierLoss = "3s"; networkConfig.IPv6PrivacyExtensions = "yes"; networkConfig.IPv6AcceptRA = "yes"; - ## don't use this by default (rely on tailscale dns only) - networkConfig.DNSDefaultRoute = false; }; }; }; diff --git a/configurations/nixos/x86_64-linux/sagittarius.nix b/configurations/nixos/x86_64-linux/sagittarius.nix index c6ecf536c..fc5590466 100644 --- a/configurations/nixos/x86_64-linux/sagittarius.nix +++ b/configurations/nixos/x86_64-linux/sagittarius.nix @@ -13,6 +13,7 @@ devices = ["/dev/mapper/encrypted_root"]; }; + ephemeralRoot = true; imports = [ ../../../profiles/admin-user/home-manager.nix ../../../profiles/admin-user/user.nix @@ -82,31 +83,21 @@ services.jae.router = { enable = true; - useNextDns = false; - nextDnsEnvFile = "/var/run/agenix/nextdns"; - restrictedMacs = [ - "5c:e0:c5:8a:24:6a" - "b4:18:d1:ab:4e:5a" - ]; - upstreamDnsServers = [ - "2a07:a8c1::" - "45.90.30.0" - "2a07:a8c0::" - "45.90.28.0" - ]; externalInterface = "enp1s0f0"; internalInterface = "enp2s0"; - internalInterfaceIP = "192.168.20.1"; - dnsMasqSettings.no-resolv = true; - dnsMasqSettings.bogus-priv = true; - dnsMasqSettings.strict-order = true; - }; - - services.prometheus.exporters = { - dnsmasq = { - enable = true; - dnsmasqListenAddress = "localhost:5342"; - }; + enableNat64 = true; # Enable NAT64 for IPv4 access from IPv6-only clients + + # Optional: Add DNS filtering with Blocky + # blockySettings = { + # blocking = { + # denylists = { + # ads = ["https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"]; + # }; + # clientGroupsBlock = { + # default = ["ads"]; + # }; + # }; + # }; }; services.vmagent = { @@ -121,10 +112,10 @@ in { scrape_configs = [ { - job_name = "dnsmasq"; + job_name = "blocky"; scrape_interval = "10s"; static_configs = [ - {targets = ["127.0.0.1:9153"];} + {targets = ["127.0.0.1:4000"];} ]; inherit relabel_configs; } @@ -145,9 +136,6 @@ file = ../../../secrets/ts-google-9k.age; owner = "1337"; }; - nextdns = { - file = ../../../secrets/nextdns.age; - }; }; users.users.${adminUser.name}.shell = lib.mkForce pkgs.bashInteractive; diff --git a/modules/router.nix b/modules/router.nix index 0c31bfaf5..3f2877860 100644 --- a/modules/router.nix +++ b/modules/router.nix @@ -1,148 +1,87 @@ { config, lib, + pkgs, ... }: let - inherit - (lib) - flatten - mapAttrsToList - mkEnableOption - mkIf - mkOption - splitString - ; - inherit - (builtins) - attrNames - head - tail - ; + inherit (lib) mkEnableOption mkIf mkOption; cfg = config.services.jae.router; - - ipBase = ip: let - s = splitString "." ip; - a = head s; - b = head (tail s); - c = head (tail (tail s)); - in "${a}.${b}.${c}"; - - internalInterfaces = { - ${cfg.internalInterface} = rec { - base = ipBase cfg.internalInterfaceIP; - address = "${base}.1"; - network = "${base}.0"; - prefixLength = 24; - netmask = "255.255.255.0"; - }; - }; - - internalInterfaceNames = attrNames internalInterfaces; in { options.services.jae.router = with lib.types; { - enable = mkEnableOption "Whether to enable the router"; - #disableDns = mkEnableOption "Whether to disable dns server"; - useNextDns = mkEnableOption "Whether to use nextdns DoH for name resolution"; - nextDnsEnvFile = mkOption { - type = nullOr str; - example = "/path/to/envfile"; - default = null; - }; - upstreamDnsServers = mkOption { - type = listOf str; - description = "List of upstream dns server addresses."; - }; - restrictedMacs = mkOption { - type = listOf str; - description = "List of mac addresses."; - default = []; - }; - dnsMasqSettings = mkOption { - type = attrsOf anything; - description = "Extra dnsmasq settings"; - }; + enable = mkEnableOption "Whether to enable the IPv6 NAT64 router"; + externalInterface = mkOption { type = str; example = "eth0"; - description = "The external interface."; + description = "The external (WAN) interface."; }; + internalInterface = mkOption { type = str; example = "eth1"; - description = "The internal interface."; + description = "The internal (LAN) interface."; }; - internalInterfaceIP = mkOption { - type = str; - example = "192.168.1.1"; - default = "192.168.1.1"; - description = "The internal interface ip."; + + blockySettings = mkOption { + type = attrs; + default = {}; + description = "Additional Blocky DNS configuration. See https://0xerr0r.github.io/blocky/latest/configuration/"; + example = lib.literalExpression '' + { + blocking = { + denylists = { + ads = ["https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"]; + }; + clientGroupsBlock = { + default = ["ads"]; + }; + }; + } + ''; + }; + + enableNat64 = mkOption { + type = lib.types.bool; + default = false; + description = "Enable NAT64 (Jool) in a separate namespace for IPv4 access from IPv6-only clients."; }; }; config = mkIf cfg.enable { networking.useDHCP = false; - networking.firewall.trustedInterfaces = internalInterfaceNames; + networking.firewall.trustedInterfaces = [cfg.internalInterface]; + ## Network configuration - IPv6 with DHCPv6 Prefix Delegation systemd.network = { enable = true; - networks = - { - "10-wan" = { - matchConfig.Name = cfg.externalInterface; - networkConfig.DHCP = "yes"; - networkConfig.IPv6AcceptRA = "yes"; - networkConfig.DNS = "127.0.0.1"; - networkConfig.DHCPPrefixDelegation = "yes"; - dhcpV6Config = { - WithoutRA = "no"; - }; + networks = { + # WAN - get IPv4 via DHCP, IPv6 via SLAAC + DHCPv6-PD + "10-wan" = { + matchConfig.Name = cfg.externalInterface; + networkConfig.DHCP = "yes"; + networkConfig.IPv6AcceptRA = "yes"; + networkConfig.DHCPPrefixDelegation = "yes"; + dhcpV6Config = { + WithoutRA = "no"; }; - } - // (lib.mapAttrs' (name: net: { - name = "11-${name}"; - value = { - matchConfig.Name = name; - networkConfig.DHCPPrefixDelegation = "yes"; - networkConfig.IPv6SendRA = "yes"; - addresses = [ - { - Address = "${net.address}/${toString net.prefixLength}"; - } - ]; - }; - }) - internalInterfaces); - }; + }; - networking.nat = { - enable = true; - inherit (cfg) externalInterface; - internalInterfaces = internalInterfaceNames; + # LAN - delegate /64 from WAN's delegated prefix + "11-lan" = { + matchConfig.Name = cfg.internalInterface; + networkConfig.DHCPPrefixDelegation = "yes"; + networkConfig.IPv6SendRA = "yes"; + # Router gets ::1 from delegated prefix automatically + }; + }; }; + ## Persistence environment.persistence."/keep".directories = ["/var/lib/dnsmasq"]; - systemd.timers.kill-nextdns = { - description = "Kill nextdns 5 minutes after boot. What a hack."; - wantedBy = ["timers.target"]; - timerConfig.OnBootSec = "5m"; - }; - systemd.services.kill-nextdns = { - description = "Kill nextdns 5 minutes after boot. What a hack."; - after = ["network-online.target"]; - wants = ["network-online.target"]; - wantedBy = ["multi-user.target"]; - serviceConfig = { - Type = "oneshot"; - ExecStartPre = "/run/current-system/sw/bin/pkill -9 nextdns"; - ExecStart = "/run/current-system/sw/bin/systemctl restart nextdns"; - }; - }; - - ## enable jool nat64 - networking.jool.enable = true; - ## enable ipv6 on local network + ## Router Advertisement (corerad) + ## Announces delegated prefix to LAN clients services.corerad = { enable = true; settings = { @@ -152,75 +91,285 @@ in { }; interfaces = [ { - name = "pref64"; + name = cfg.internalInterface; advertise = true; prefix = [ { - prefix = "64:ff9b::/96"; + prefix = "::/64"; # Uses delegated prefix } ]; - } - { - name = cfg.internalInterface; - advertise = true; - prefix = [ + pref64 = [ { - prefix = "::/64"; + prefix = "64:ff9b::/96"; } ]; rdnss = [ { - servers = ["::"]; + servers = ["::"]; # This router (link-local) lifetime = "auto"; } ]; + # Set "Managed" flag - DHCPv6 handles address assignment + # Devices can still use SLAAC if they prefer (dual mode) + managed = true; } ]; }; }; - services.dnsmasq.enable = true; - services.dnsmasq.resolveLocalQueries = true; - services.dnsmasq.settings = - { - dhcp-range = mapAttrsToList (tag: net: "${tag},${net.base}.10,${net.base}.128,255.255.255.0,24h") internalInterfaces; - dhcp-option = (mapAttrsToList (tag: net: "${tag},option:router,${net.address}") internalInterfaces) ++ ["option:dns-server,${cfg.internalInterfaceIP}"]; - interface = internalInterfaceNames; - } - // { - server = mkIf (!cfg.useNextDns) cfg.upstreamDnsServers; - # server = mkMerge [ - # (mkIf (!cfg.useNextDns) cfg.upstreamDnsServers) - # (mkIf cfg.useNextDns ["127.0.0.1#5555"]) - # ]; + ## DNS: Blocky (filtering) -> CoreDNS (DNS64) -> Upstream DoT + ## Blocky on port 53, CoreDNS on port 5300 + services.blocky = { + enable = true; + settings = lib.mkMerge [ + { + # Port configuration + ports = { + dns = 53; + http = 4000; # API/metrics + }; + + # Upstream: forward to CoreDNS for DNS64 + upstreams = { + groups = { + default = ["127.0.0.1:5300"]; + }; + }; + + # Bootstrap DNS (for initial queries before Blocky is ready) + bootstrapDns = { + upstream = "https://one.one.one.one/dns-query"; + ips = ["1.1.1.1" "1.0.0.1" "2606:4700:4700::1111" "2606:4700:4700::1001"]; + }; + + # Caching + caching = { + minTime = "5m"; + maxTime = "30m"; + prefetching = true; + }; + + # Logging + log = { + level = "info"; + format = "text"; + }; + } + cfg.blockySettings + ]; + }; + + ## DNS64: CoreDNS on port 5300 + ## Translates A records to AAAA using 64:ff9b::/96 prefix + services.coredns = { + enable = true; + config = '' + .:5300 { + bind 127.0.0.1 ::1 + dns64 { + prefix 64:ff9b::/96 + ## because blocky will use ipv4 - it's fine, we're ipv6 only + allow_ipv4 + } + forward . tls://2606:4700:4700::1111 tls://2606:4700:4700::1001 { + + tls_servername 1dot1dot1dot1.cloudflare-dns.com + } + cache 300 + log + } + ''; + }; + + ## DHCPv6: Stateful address assignment for devices without SLAAC + ## dnsmasq provides DHCPv6 only (DNS disabled, handled by Blocky) + services.dnsmasq = { + enable = true; + resolveLocalQueries = false; # Blocky handles DNS + settings = { + # Disable DNS server (port 0 = DNS disabled, DHCPv6 only) + port = 0; + + # Stateful DHCPv6 - assign addresses from ::1000 to ::1fff range + # SLAAC devices can still use ::2000+ or EUI-64 addresses + # "constructor:" automatically uses the delegated prefix + dhcp-range = ["tag:${cfg.internalInterface},::1000,::1fff,constructor:${cfg.internalInterface},64,12h"]; + + # Provide DNS server via DHCPv6 option (:: = this router's link-local) + dhcp-option = ["option6:dns-server,[::]"]; + + # Interface configuration + interface = [cfg.internalInterface]; + except-interface = [cfg.externalInterface]; + + # DHCPv6 settings dhcp-authoritative = true; dhcp-leasefile = "/var/lib/dnsmasq/dnsmasq.leases"; - add-mac = "text"; - add-subnet = "32,128"; - port = - if cfg.useNextDns - then 5342 - else 53; - } - // cfg.dnsMasqSettings; + # Logging for DHCPv6 + log-dhcp = true; + }; + }; + + ## Disable systemd-resolved (conflicts with our DNS setup) services.resolved.enable = false; - services.nextdns.enable = cfg.useNextDns; - services.nextdns.arguments = (flatten (map (mac: ["-profile" "${mac}=\${KIDSDNS_ID}"]) cfg.restrictedMacs)) ++ ["-profile" "${cfg.internalInterfaceIP}/24=\${NEXTDNS_ID}" "-cache-size" "10MB" "-discovery-dns" "127.0.0.1:5342" "-report-client-info" "-listen" "${cfg.internalInterfaceIP}:53" "-listen" "127.0.0.1:53"]; - systemd.services.nextdns = mkIf cfg.useNextDns { - serviceConfig.EnvironmentFile = cfg.nextDnsEnvFile; - after = ["systemd-networkd-wait-online.service"]; + + ## Kernel parameters for IPv6 routing + boot.kernel.sysctl = { + "net.ipv4.conf.all.forwarding" = true; + "net.ipv6.conf.all.forwarding" = true; + + "net.ipv6.conf.all.accept_ra" = 1; + "net.ipv6.conf.all.autoconf" = 1; + "net.ipv6.conf.all.use_tempaddr" = 0; + + # Allow IPv6 autoconfiguration on WAN + "net.ipv6.conf.${cfg.externalInterface}.accept_ra" = 2; + "net.ipv6.conf.${cfg.externalInterface}.autoconf" = 1; + }; + + ## NAT64 (Jool) configuration - runs in separate namespace (to allow the router itself access to ipv4 services) + ## Allows IPv6-only clients to access IPv4 services + networking.jool.enable = mkIf cfg.enableNat64 true; + + ## Jool NAT64 config - uses veth IP in pool4 + ## Main namespace will MASQUERADE to public IP via conntrack + environment.etc."jool-nat64-default.conf" = mkIf cfg.enableNat64 { + text = builtins.toJSON { + instance = "default"; + framework = "netfilter"; + global = { + pool6 = "64:ff9b::/96"; + manually-enabled = true; + }; + pool4 = [ + { + protocol = "TCP"; + prefix = "192.168.64.2/32"; + "port range" = "10000-65535"; + } + { + protocol = "UDP"; + prefix = "192.168.64.2/32"; + "port range" = "10000-65535"; + } + { + protocol = "ICMP"; + prefix = "192.168.64.2/32"; + "port range" = "10000-65535"; + } + ]; + }; + }; + + ## MASQUERADE traffic from NAT64 namespace to appear from public IP + networking.firewall.extraCommands = mkIf cfg.enableNat64 '' + # Allow forwarding from NAT64 namespace + iptables -A FORWARD -i veth-nat64 -o ${cfg.externalInterface} -j ACCEPT + iptables -A FORWARD -i ${cfg.externalInterface} -o veth-nat64 -m state --state RELATED,ESTABLISHED -j ACCEPT + + # MASQUERADE traffic from namespace (192.168.64.0/30 private range) to public IP + iptables -t nat -A POSTROUTING -s 192.168.64.0/30 -o ${cfg.externalInterface} -j MASQUERADE + ''; + + networking.firewall.extraStopCommands = mkIf cfg.enableNat64 '' + # Cleanup rules + iptables -D FORWARD -i veth-nat64 -o ${cfg.externalInterface} -j ACCEPT 2>/dev/null || true + iptables -D FORWARD -i ${cfg.externalInterface} -o veth-nat64 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true + iptables -t nat -D POSTROUTING -s 192.168.64.0/30 -o ${cfg.externalInterface} -j MASQUERADE 2>/dev/null || true + ''; + + ## Service to setup the NAT64 network namespace + systemd.services.nat64-namespace-setup = mkIf cfg.enableNat64 { + description = "Setup NAT64 network namespace"; + wantedBy = ["multi-user.target"]; + before = ["jool-nat64-default.service"]; + after = ["network-online.target" "systemd-networkd.service"]; + wants = ["network-online.target"]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + + ExecStart = pkgs.writeShellScript "setup-nat64-namespace" '' + set -euo pipefail + + # Create namespace if it doesn't exist + ${pkgs.iproute2}/bin/ip netns add nat64 2>/dev/null || true + + # Create veth pair if it doesn't exist + if ! ${pkgs.iproute2}/bin/ip link show veth-nat64 >/dev/null 2>&1; then + ${pkgs.iproute2}/bin/ip link add veth-nat64 type veth peer name veth-nat64-ns + ${pkgs.iproute2}/bin/ip link set veth-nat64-ns netns nat64 + fi + + # Configure main namespace side (both IPv6 and IPv4) + # IPv6: ULA (Unique Local Address) - fd00::/8 prefix + # IPv4: RFC 1918 private address (192.168.0.0/16) - home network range + ${pkgs.iproute2}/bin/ip addr flush dev veth-nat64 || true + ${pkgs.iproute2}/bin/ip addr add fd00:64::1/64 dev veth-nat64 + ${pkgs.iproute2}/bin/ip addr add 192.168.64.1/30 dev veth-nat64 + ${pkgs.iproute2}/bin/ip link set veth-nat64 up + + # Configure namespace side (both IPv6 and IPv4) + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.iproute2}/bin/ip addr flush dev veth-nat64-ns || true + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.iproute2}/bin/ip addr add fd00:64::2/64 dev veth-nat64-ns + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.iproute2}/bin/ip addr add 192.168.64.2/30 dev veth-nat64-ns + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.iproute2}/bin/ip link set veth-nat64-ns up + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.iproute2}/bin/ip link set lo up + + # Add route in main namespace to send NAT64 traffic to the namespace + ${pkgs.iproute2}/bin/ip route replace 64:ff9b::/96 via fd00:64::2 dev veth-nat64 + + # Add default IPv4 route in namespace to send translated packets back to main + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.iproute2}/bin/ip route replace default via 192.168.64.1 dev veth-nat64-ns + + # Add default IPv6 route in namespace to send response packets back to main + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.iproute2}/bin/ip -6 route replace default via fd00:64::1 dev veth-nat64-ns + + # Enable IPv4 and IPv6 forwarding in the namespace + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.procps}/bin/sysctl -q -w net.ipv4.ip_forward=1 + ${pkgs.iproute2}/bin/ip netns exec nat64 ${pkgs.procps}/bin/sysctl -q -w net.ipv6.conf.all.forwarding=1 + + echo "NAT64 namespace setup complete" + ''; + + ExecStop = pkgs.writeShellScript "cleanup-nat64-namespace" '' + # Remove route + ${pkgs.iproute2}/bin/ip route del 64:ff9b::/96 via fd00:64::2 dev veth-nat64 2>/dev/null || true + + # Delete veth pair (automatically removes from namespace too) + ${pkgs.iproute2}/bin/ip link del veth-nat64 2>/dev/null || true + + # Delete namespace + ${pkgs.iproute2}/bin/ip netns del nat64 2>/dev/null || true + + echo "NAT64 namespace cleanup complete" + ''; + }; }; - boot.kernel.sysctl."net.ipv4.conf.all.forwarding" = true; - boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true; + ## Jool NAT64 service - runs in the namespace + systemd.services.jool-nat64-default = mkIf cfg.enableNat64 { + after = ["nat64-namespace-setup.service"]; + requires = ["nat64-namespace-setup.service"]; + wants = ["network-online.target"]; - boot.kernel.sysctl."net.ipv6.conf.all.accept_ra" = 1; - boot.kernel.sysctl."net.ipv6.conf.all.autoconf" = 1; - boot.kernel.sysctl."net.ipv6.conf.all.use_tempaddr" = 0; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; - # allow ipv6 autoconfiguration and temporary address use on wan - boot.kernel.sysctl."net.ipv6.conf.${cfg.externalInterface}.accept_ra" = 2; - boot.kernel.sysctl."net.ipv6.conf.${cfg.externalInterface}.autoconf" = 1; + # Run in the NAT64 namespace - systemd handles the namespace isolation + NetworkNamespacePath = "/var/run/netns/nat64"; + + ExecStartPre = [ + # Module needs to be loaded in main namespace first (+ prefix runs in main namespace) + "+${pkgs.kmod}/bin/modprobe jool" + ]; + + ExecStart = lib.mkForce "${pkgs.jool-cli}/bin/jool file handle /etc/jool-nat64-default.conf"; + ExecStop = lib.mkForce "${pkgs.jool-cli}/bin/jool instance remove default"; + }; + }; }; }