diff options
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2311.section.md | 2 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/services/networking/jool.nix | 222 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/jool.nix | 250 |
5 files changed, 476 insertions, 0 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index 31e90c30cf173..8880c71fa9a63 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -30,6 +30,8 @@ - [sitespeed-io](https://sitespeed.io), a tool that can generate metrics (timings, diagnostics) for websites. Available as [services.sitespeed-io](#opt-services.sitespeed-io.enable). +- [Jool](https://nicmx.github.io/Jool/en/index.html), an Open Source implementation of IPv4/IPv6 translation on Linux. Available as [networking.jool.enable](#opt-networking.jool.enable). + - [Apache Guacamole](https://guacamole.apache.org/), a cross-platform, clientless remote desktop gateway. Available as [services.guacamole-server](#opt-services.guacamole-server.enable) and [services.guacamole-client](#opt-services.guacamole-client.enable) services. - [pgBouncer](https://www.pgbouncer.org), a PostgreSQL connection pooler. Available as [services.pgbouncer](#opt-services.pgbouncer.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index c7d76fe2eee93..97b8f61e1e70d 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -929,6 +929,7 @@ ./services/networking/jibri/default.nix ./services/networking/jicofo.nix ./services/networking/jitsi-videobridge.nix + ./services/networking/jool.nix ./services/networking/kea.nix ./services/networking/keepalived/default.nix ./services/networking/keybase.nix diff --git a/nixos/modules/services/networking/jool.nix b/nixos/modules/services/networking/jool.nix new file mode 100644 index 0000000000000..3aafbe40967ce --- /dev/null +++ b/nixos/modules/services/networking/jool.nix @@ -0,0 +1,222 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.networking.jool; + + jool = config.boot.kernelPackages.jool; + jool-cli = pkgs.jool-cli; + + hardening = { + # Run as unprivileged user + User = "jool"; + Group = "jool"; + DynamicUser = true; + + # Restrict filesystem to only read the jool module + TemporaryFileSystem = [ "/" ]; + BindReadOnlyPaths = [ + builtins.storeDir + "/run/current-system/kernel-modules" + ]; + + # Give capabilities to load the module and configure it + AmbientCapabilities = [ "CAP_SYS_MODULE" "CAP_NET_ADMIN" ]; + RestrictAddressFamilies = [ "AF_NETLINK" ]; + + # Other restrictions + RestrictNamespaces = [ "net" ]; + SystemCallFilter = [ "@system-service" "@module" ]; + CapabilityBoundingSet = [ "CAP_SYS_MODULE" "CAP_NET_ADMIN" ]; + }; + + configFormat = pkgs.formats.json {}; + + mkDefaultAttrs = lib.mapAttrs (n: v: lib.mkDefault v); + + defaultNat64 = { + instance = "default"; + framework = "netfilter"; + global.pool6 = "64:ff9b::/96"; + }; + defaultSiit = { + instance = "default"; + framework = "netfilter"; + }; + + nat64Conf = configFormat.generate "jool-nat64.conf" cfg.nat64.config; + siitConf = configFormat.generate "jool-siit.conf" cfg.siit.config; + +in + +{ + ###### interface + + options = { + networking.jool.enable = lib.mkOption { + type = lib.types.bool; + default = false; + relatedPackages = [ "linuxPackages.jool" "jool-cli" ]; + description = lib.mdDoc '' + Whether to enable Jool, an Open Source implementation of IPv4/IPv6 + translation on Linux. + + Jool can perform stateless IP/ICMP translation (SIIT) or stateful + NAT64, analogous to the IPv4 NAPT. Refer to the upstream + [documentation](https://nicmx.github.io/Jool/en/intro-xlat.html) for + the supported modes of translation and how to configure them. + ''; + }; + + networking.jool.nat64.enable = lib.mkEnableOption (lib.mdDoc "a NAT64 instance of Jool."); + networking.jool.nat64.config = lib.mkOption { + type = configFormat.type; + default = defaultNat64; + example = lib.literalExpression '' + { + # custom NAT64 prefix + global.pool6 = "2001:db8:64::/96"; + + # Port forwarding + bib = [ + { # SSH 192.0.2.16 → 2001:db8:a::1 + "protocol" = "TCP"; + "ipv4 address" = "192.0.2.16#22"; + "ipv6 address" = "2001:db8:a::1#22"; + } + { # DNS (TCP) 192.0.2.16 → 2001:db8:a::2 + "protocol" = "TCP"; + "ipv4 address" = "192.0.2.16#53"; + "ipv6 address" = "2001:db8:a::2#53"; + } + { # DNS (UDP) 192.0.2.16 → 2001:db8:a::2 + "protocol" = "UDP"; + "ipv4 address" = "192.0.2.16#53"; + "ipv6 address" = "2001:db8:a::2#53"; + } + ]; + + pool4 = [ + # Ports for dynamic translation + { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } + { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } + { protocol = "ICMP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } + + # Ports for static BIB entries + { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "22"; } + { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "53"; } + ]; + } + ''; + description = lib.mdDoc '' + The configuration of a stateful NAT64 instance of Jool managed through + NixOS. See https://nicmx.github.io/Jool/en/config-atomic.html for the + available options. + + ::: {.note} + Existing or more instances created manually will not interfere with the + NixOS instance, provided the respective `pool4` addresses and port + ranges are not overlapping. + ::: + + ::: {.warning} + Changes to the NixOS instance performed via `jool instance nixos-nat64` + are applied correctly but will be lost after restarting + `jool-nat64.service`. + ::: + ''; + }; + + networking.jool.siit.enable = lib.mkEnableOption (lib.mdDoc "a SIIT instance of Jool."); + networking.jool.siit.config = lib.mkOption { + type = configFormat.type; + default = defaultSiit; + example = lib.literalExpression '' + { + # Maps any IPv4 address x.y.z.t to 2001:db8::x.y.z.t and v.v. + pool6 = "2001:db8::/96"; + + # Explicit address mappings + eamt = [ + # 2001:db8:1:: ←→ 192.0.2.0 + { "ipv6 prefix": "2001:db8:1::/128", "ipv4 prefix": "192.0.2.0" } + # 2001:db8:1::x ←→ 198.51.100.x + { "ipv6 prefix": "2001:db8:2::/120", "ipv4 prefix": "198.51.100.0/24" } + ] + } + ''; + description = lib.mdDoc '' + The configuration of a SIIT instance of Jool managed through + NixOS. See https://nicmx.github.io/Jool/en/config-atomic.html for the + available options. + + ::: {.note} + Existing or more instances created manually will not interfere with the + NixOS instance, provided the respective `EAMT` address mappings are not + overlapping. + ::: + + ::: {.warning} + Changes to the NixOS instance performed via `jool instance nixos-siit` + are applied correctly but will be lost after restarting + `jool-siit.service`. + ::: + ''; + }; + + }; + + ###### implementation + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ jool-cli ]; + boot.extraModulePackages = [ jool ]; + + systemd.services.jool-nat64 = lib.mkIf cfg.nat64.enable { + description = "Jool, NAT64 setup"; + documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + reloadIfChanged = true; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${pkgs.kmod}/bin/modprobe jool"; + ExecStart = "${jool-cli}/bin/jool file handle ${nat64Conf}"; + ExecStop = "${jool-cli}/bin/jool -f ${nat64Conf} instance remove"; + } // hardening; + }; + + systemd.services.jool-siit = lib.mkIf cfg.siit.enable { + description = "Jool, SIIT setup"; + documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + reloadIfChanged = true; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${pkgs.kmod}/bin/modprobe jool_siit"; + ExecStart = "${jool-cli}/bin/jool_siit file handle ${siitConf}"; + ExecStop = "${jool-cli}/bin/jool_siit -f ${siitConf} instance remove"; + } // hardening; + }; + + system.checks = lib.singleton (pkgs.runCommand "jool-validated" { + nativeBuildInputs = [ pkgs.buildPackages.jool-cli ]; + preferLocalBuild = true; + } '' + printf 'Validating Jool configuration... ' + ${lib.optionalString cfg.siit.enable "jool_siit file check ${siitConf}"} + ${lib.optionalString cfg.nat64.enable "jool file check ${nat64Conf}"} + printf 'ok\n' + touch "$out" + ''); + + networking.jool.nat64.config = mkDefaultAttrs defaultNat64; + networking.jool.siit.config = mkDefaultAttrs defaultSiit; + + }; + + meta.maintainers = with lib.maintainers; [ rnhmjoj ]; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 4b338dac69a7d..6f17bd2cdd3b5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -391,6 +391,7 @@ in { jibri = handleTest ./jibri.nix {}; jirafeau = handleTest ./jirafeau.nix {}; jitsi-meet = handleTest ./jitsi-meet.nix {}; + jool = handleTest ./jool.nix {}; k3s = handleTest ./k3s {}; kafka = handleTest ./kafka.nix {}; kanidm = handleTest ./kanidm.nix {}; diff --git a/nixos/tests/jool.nix b/nixos/tests/jool.nix new file mode 100644 index 0000000000000..6d5ded9b18e07 --- /dev/null +++ b/nixos/tests/jool.nix @@ -0,0 +1,250 @@ +{ system ? builtins.currentSystem, + config ? {}, + pkgs ? import ../.. { inherit system config; } +}: + +with import ../lib/testing-python.nix { inherit system pkgs; }; + +let + inherit (pkgs) lib; + + ipv6Only = { + networking.useDHCP = false; + networking.interfaces.eth1.ipv4.addresses = lib.mkVMOverride [ ]; + }; + + ipv4Only = { + networking.useDHCP = false; + networking.interfaces.eth1.ipv6.addresses = lib.mkVMOverride [ ]; + }; + + webserver = ip: msg: { + systemd.services.webserver = { + description = "Mock webserver"; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.Restart = "always"; + script = '' + while true; do + { + printf 'HTTP/1.0 200 OK\n' + printf 'Content-Length: ${toString (1 + builtins.stringLength msg)}\n' + printf '\n${msg}\n\n' + } | ${pkgs.libressl.nc}/bin/nc -${toString ip}nvl 80 + done + ''; + }; + networking.firewall.allowedTCPPorts = [ 80 ]; + }; + +in + +{ + siit = makeTest { + # This test simulates the setup described in [1] with two IPv6 and + # IPv4-only devices on different subnets communicating through a border + # relay running Jool in SIIT mode. + # [1]: https://nicmx.github.io/Jool/en/run-vanilla.html + name = "jool-siit"; + meta.maintainers = with lib.maintainers; [ rnhmjoj ]; + + # Border relay + nodes.relay = { ... }: { + imports = [ ../modules/profiles/minimal.nix ]; + virtualisation.vlans = [ 1 2 ]; + + # Enable packet routing + boot.kernel.sysctl = { + "net.ipv6.conf.all.forwarding" = 1; + "net.ipv4.conf.all.forwarding" = 1; + }; + + networking.useDHCP = false; + networking.interfaces = lib.mkVMOverride { + eth1.ipv6.addresses = [ { address = "fd::198.51.100.1"; prefixLength = 120; } ]; + eth2.ipv4.addresses = [ { address = "192.0.2.1"; prefixLength = 24; } ]; + }; + + networking.jool = { + enable = true; + siit.enable = true; + siit.config.global.pool6 = "fd::/96"; + }; + }; + + # IPv6 only node + nodes.alice = { ... }: { + imports = [ + ../modules/profiles/minimal.nix + ipv6Only + (webserver 6 "Hello, Bob!") + ]; + + virtualisation.vlans = [ 1 ]; + networking.interfaces.eth1.ipv6 = { + addresses = [ { address = "fd::198.51.100.8"; prefixLength = 120; } ]; + routes = [ { address = "fd::192.0.2.0"; prefixLength = 120; + via = "fd::198.51.100.1"; } ]; + }; + }; + + # IPv4 only node + nodes.bob = { ... }: { + imports = [ + ../modules/profiles/minimal.nix + ipv4Only + (webserver 4 "Hello, Alice!") + ]; + + virtualisation.vlans = [ 2 ]; + networking.interfaces.eth1.ipv4 = { + addresses = [ { address = "192.0.2.16"; prefixLength = 24; } ]; + routes = [ { address = "198.51.100.0"; prefixLength = 24; + via = "192.0.2.1"; } ]; + }; + }; + + testScript = '' + start_all() + + relay.wait_for_unit("jool-siit.service") + alice.wait_for_unit("network-addresses-eth1.service") + bob.wait_for_unit("network-addresses-eth1.service") + + with subtest("Alice and Bob can't ping each other"): + relay.systemctl("stop jool-siit.service") + alice.fail("ping -c1 fd::192.0.2.16") + bob.fail("ping -c1 198.51.100.8") + + with subtest("Alice and Bob can ping using the relay"): + relay.systemctl("start jool-siit.service") + alice.wait_until_succeeds("ping -c1 fd::192.0.2.16") + bob.wait_until_succeeds("ping -c1 198.51.100.8") + + with subtest("Alice can connect to Bob's webserver"): + bob.wait_for_open_port(80) + alice.succeed("curl -vvv http://[fd::192.0.2.16] >&2") + alice.succeed("curl --fail -s http://[fd::192.0.2.16] | grep -q Alice") + + with subtest("Bob can connect to Alices's webserver"): + alice.wait_for_open_port(80) + bob.succeed("curl --fail -s http://198.51.100.8 | grep -q Bob") + ''; + }; + + nat64 = makeTest { + # This test simulates the setup described in [1] with two IPv6-only nodes + # (a client and a homeserver) on the LAN subnet and an IPv4 node on the WAN. + # The router runs Jool in stateful NAT64 mode, masquarading the LAN and + # forwarding ports using static BIB entries. + # [1]: https://nicmx.github.io/Jool/en/run-nat64.html + name = "jool-nat64"; + meta.maintainers = with lib.maintainers; [ rnhmjoj ]; + + # Router + nodes.router = { ... }: { + imports = [ ../modules/profiles/minimal.nix ]; + virtualisation.vlans = [ 1 2 ]; + + # Enable packet routing + boot.kernel.sysctl = { + "net.ipv6.conf.all.forwarding" = 1; + "net.ipv4.conf.all.forwarding" = 1; + }; + + networking.useDHCP = false; + networking.interfaces = lib.mkVMOverride { + eth1.ipv6.addresses = [ { address = "2001:db8::1"; prefixLength = 96; } ]; + eth2.ipv4.addresses = [ { address = "203.0.113.1"; prefixLength = 24; } ]; + }; + + networking.jool = { + enable = true; + nat64.enable = true; + nat64.config = { + bib = [ + { # forward HTTP 203.0.113.1 (router) → 2001:db8::9 (homeserver) + "protocol" = "TCP"; + "ipv4 address" = "203.0.113.1#80"; + "ipv6 address" = "2001:db8::9#80"; + } + ]; + pool4 = [ + # Ports for dynamic translation + { protocol = "TCP"; prefix = "203.0.113.1/32"; "port range" = "40001-65535"; } + { protocol = "UDP"; prefix = "203.0.113.1/32"; "port range" = "40001-65535"; } + { protocol = "ICMP"; prefix = "203.0.113.1/32"; "port range" = "40001-65535"; } + # Ports for static BIB entries + { protocol = "TCP"; prefix = "203.0.113.1/32"; "port range" = "80"; } + ]; + }; + }; + }; + + # LAN client (IPv6 only) + nodes.client = { ... }: { + imports = [ ../modules/profiles/minimal.nix ipv6Only ]; + virtualisation.vlans = [ 1 ]; + + networking.interfaces.eth1.ipv6 = { + addresses = [ { address = "2001:db8::8"; prefixLength = 96; } ]; + routes = [ { address = "64:ff9b::"; prefixLength = 96; + via = "2001:db8::1"; } ]; + }; + }; + + # LAN server (IPv6 only) + nodes.homeserver = { ... }: { + imports = [ + ../modules/profiles/minimal.nix + ipv6Only + (webserver 6 "Hello from IPv6!") + ]; + + virtualisation.vlans = [ 1 ]; + networking.interfaces.eth1.ipv6 = { + addresses = [ { address = "2001:db8::9"; prefixLength = 96; } ]; + routes = [ { address = "64:ff9b::"; prefixLength = 96; + via = "2001:db8::1"; } ]; + }; + }; + + # WAN server (IPv4 only) + nodes.server = { ... }: { + imports = [ + ../modules/profiles/minimal.nix + ipv4Only + (webserver 4 "Hello from IPv4!") + ]; + + virtualisation.vlans = [ 2 ]; + networking.interfaces.eth1.ipv4.addresses = + [ { address = "203.0.113.16"; prefixLength = 24; } ]; + }; + + testScript = '' + start_all() + + for node in [client, homeserver, server]: + node.wait_for_unit("network-addresses-eth1.service") + + with subtest("Client can ping the WAN server"): + router.wait_for_unit("jool-nat64.service") + client.succeed("ping -c1 64:ff9b::203.0.113.16") + + with subtest("Client can connect to the WAN webserver"): + server.wait_for_open_port(80) + client.succeed("curl --fail -s http://[64:ff9b::203.0.113.16] | grep -q IPv4!") + + with subtest("Router BIB entries are correctly populated"): + router.succeed("jool bib display | grep -q 'Dynamic TCP.*2001:db8::8'") + router.succeed("jool bib display | grep -q 'Static TCP.*2001:db8::9'") + + with subtest("WAN server can reach the LAN server"): + homeserver.wait_for_open_port(80) + server.succeed("curl --fail -s http://203.0.113.1 | grep -q IPv6!") + ''; + + }; + +} |