Unverified Commit fcf38c9b authored by Michele Guerini Rocco's avatar Michele Guerini Rocco Committed by GitHub
Browse files

Merge pull request #252597 from rnhmjoj/pr-jool

Fixup of PR #240982
parents a9fea437 b058de4a
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@

- [stalwart-mail](https://stalw.art), an all-in-one email server (SMTP, IMAP, JMAP). Available as [services.stalwart-mail](#opt-services.stalwart-mail.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).
- [Jool](https://nicmx.github.io/Jool/en/index.html), a kernelspace NAT64 and SIIT implementation, providing translation between IPv4 and IPv6. 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.

+186 −127
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ let
    TemporaryFileSystem = [ "/" ];
    BindReadOnlyPaths = [
      builtins.storeDir
      "/run/current-system/kernel-modules"
      "/run/booted-system/kernel-modules"
    ];

    # Give capabilities to load the module and configure it
@@ -31,26 +31,96 @@ let

  configFormat = pkgs.formats.json {};

  mkDefaultAttrs = lib.mapAttrs (n: v: lib.mkDefault v);
  # Generate the config file of instance `name`
  nat64Conf = name:
    configFormat.generate "jool-nat64-${name}.conf"
      (cfg.nat64.${name} // { instance = name; });
  siitConf = name:
    configFormat.generate "jool-siit-${name}.conf"
      (cfg.siit.${name} // { instance = name; });

  # NAT64 config type
  nat64Options = lib.types.submodule {
    # The format is plain JSON
    freeformType = configFormat.type;
    # Some options with a default value
    options.framework = lib.mkOption {
      type = lib.types.enum [ "netfilter" "iptables" ];
      default = "netfilter";
      description = lib.mdDoc ''
        The framework to use for attaching Jool's translation to the exist
        kernel packet processing rules. See the
        [documentation](https://nicmx.github.io/Jool/en/intro-jool.html#design)
        for the differences between the two options.
      '';
    };
    options.global.pool6 = lib.mkOption {
      type = lib.types.strMatching "[[:xdigit:]:]+/[[:digit:]]+"
        // { description = "Network prefix in CIDR notation"; };
      default = "64:ff9b::/96";
      description = lib.mdDoc ''
        The prefix used for embedding IPv4 into IPv6 addresses.
        Defaults to the well-known NAT64 prefix, defined by
        [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052).
      '';
    };
  };

  # SIIT config type
  siitOptions = lib.types.submodule {
    # The format is, again, plain JSON
    freeformType = configFormat.type;
    # Some options with a default value
    options = { inherit (nat64Options.getSubOptions []) framework; };
  };

  makeNat64Unit = name: opts: {
    "jool-nat64-${name}" = {
      description = "Jool, NAT64 setup of instance ${name}";
      documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ];
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        Type = "oneshot";
        RemainAfterExit = true;
        ExecStartPre = "${pkgs.kmod}/bin/modprobe jool";
        ExecStart    = "${jool-cli}/bin/jool file handle ${nat64Conf name}";
        ExecStop     = "${jool-cli}/bin/jool -f ${nat64Conf name} instance remove";
      } // hardening;
    };
  };

  defaultNat64 = {
    instance = "default";
    framework = "netfilter";
    global.pool6 = "64:ff9b::/96";
  makeSiitUnit = name: opts: {
    "jool-siit-${name}" = {
      description = "Jool, SIIT setup of instance ${name}";
      documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ];
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        Type = "oneshot";
        RemainAfterExit = true;
        ExecStartPre = "${pkgs.kmod}/bin/modprobe jool_siit";
        ExecStart    = "${jool-cli}/bin/jool_siit file handle ${siitConf name}";
        ExecStop     = "${jool-cli}/bin/jool_siit -f ${siitConf name} instance remove";
      } // hardening;
    };
  defaultSiit = {
    instance = "default";
    framework = "netfilter";
  };

  nat64Conf = configFormat.generate "jool-nat64.conf" cfg.nat64.config;
  siitConf  = configFormat.generate "jool-siit.conf" cfg.siit.config;
  checkNat64 = name: _: ''
    printf 'Validating Jool configuration for NAT64 instance "${name}"... '
    jool file check ${nat64Conf name}
    printf 'Ok.\n'; touch "$out"
  '';

  checkSiit = name: _: ''
    printf 'Validating Jool configuration for SIIT instance "${name}"... '
    jool_siit file check ${siitConf name}
    printf 'Ok.\n'; touch "$out"
  '';

in

{
  ###### interface

  options = {
    networking.jool.enable = lib.mkOption {
      type = lib.types.bool;
@@ -64,15 +134,18 @@ in
        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.

        Enabling this option will install the Jool kernel module and the
        command line tools for controlling it.
      '';
    };

    networking.jool.nat64.enable = lib.mkEnableOption (lib.mdDoc "a NAT64 instance of Jool.");
    networking.jool.nat64.config = lib.mkOption {
      type = configFormat.type;
      default = defaultNat64;
    networking.jool.nat64 = lib.mkOption {
      type = lib.types.attrsOf nat64Options;
      default = { };
      example = lib.literalExpression ''
        {
          default = {
            # custom NAT64 prefix
            global.pool6 = "2001:db8:64::/96";

@@ -96,7 +169,7 @@ in
            ];

            pool4 = [
            # Ports for dynamic translation
              # Port ranges 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"; }
@@ -105,116 +178,102 @@ in
              { 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.
        Definitions of NAT64 instances of Jool.
        See the
        [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for
        the available options. Also check out the
        [tutorial](https://nicmx.github.io/Jool/en/run-nat64.html) for an
        introduction to NAT64 and how to troubleshoot the setup.

        The attribute name defines the name of the instance, with the main one
        being `default`: this can be accessed from the command line without
        specifying the name with `-i`.

        ::: {.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.
        Instances created imperatively from the command line will not interfere
        with the NixOS instances, 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`.
        Changes to an instance performed via `jool -i <name>` are applied
        correctly but will be lost after restarting the respective
        `jool-nat64-<name>.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;
    networking.jool.siit = lib.mkOption {
      type = lib.types.attrsOf siitOptions;
      default = { };
      example = lib.literalExpression ''
        {
          default = {
            # Maps any IPv4 address x.y.z.t to 2001:db8::x.y.z.t and v.v.
          pool6 = "2001:db8::/96";
            global.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" }
              { "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" }
          ]
              { "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.
        Definitions of SIIT instances of Jool.
        See the
        [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for
        the available options. Also check out the
        [tutorial](https://nicmx.github.io/Jool/en/run-vanilla.html) for an
        introduction to SIIT and how to troubleshoot the setup.

        The attribute name defines the name of the instance, with the main one
        being `default`: this can be accessed from the command line without
        specifying the name with `-i`.

        ::: {.note}
        Existing or more instances created manually will not interfere with the
        NixOS instance, provided the respective `EAMT` address mappings are not
        overlapping.
        Instances created imperatively from the command line will not interfere
        with the NixOS instances, provided the respective EAMT addresses and
        port ranges 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`.
        Changes to an instance performed via `jool -i <name>` are applied
        correctly but will be lost after restarting the respective
        `jool-siit-<name>.service`.
        :::
      '';
    };

  };

  ###### implementation

  config = lib.mkIf cfg.enable {
    environment.systemPackages = [ jool-cli ];
    # Install kernel module and cli tools
    boot.extraModulePackages = [ jool ];
    environment.systemPackages = [ jool-cli ];

    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;
    };
    # Install services for each instance
    systemd.services = lib.mkMerge
      (lib.mapAttrsToList makeNat64Unit cfg.nat64 ++
       lib.mapAttrsToList makeSiitUnit cfg.siit);

    system.checks = lib.singleton (pkgs.runCommand "jool-validated" {
      nativeBuildInputs = [ pkgs.buildPackages.jool-cli ];
    # Check the configuration of each instance
    system.checks = lib.optional (cfg.nat64 != {} || cfg.siit != {})
      (pkgs.runCommand "jool-validated"
        {
          nativeBuildInputs = with 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;

        }
        (lib.concatStrings
          (lib.mapAttrsToList checkNat64 cfg.nat64 ++
           lib.mapAttrsToList checkSiit cfg.siit)));
  };

  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+1 −1
Original line number Diff line number Diff line
@@ -395,7 +395,7 @@ in {
  jibri = handleTest ./jibri.nix {};
  jirafeau = handleTest ./jirafeau.nix {};
  jitsi-meet = handleTest ./jitsi-meet.nix {};
  jool = handleTest ./jool.nix {};
  jool = import ./jool.nix { inherit pkgs runTest; };
  k3s = handleTest ./k3s {};
  kafka = handleTest ./kafka.nix {};
  kanidm = handleTest ./kanidm.nix {};
+38 −68
Original line number Diff line number Diff line
{ system ? builtins.currentSystem,
  config ? {},
  pkgs ? import ../.. { inherit system config; }
}:

with import ../lib/testing-python.nix { inherit system pkgs; };
{ pkgs, runTest }:

let
  inherit (pkgs) lib;
@@ -23,7 +18,6 @@ let
      description = "Mock webserver";
      wants = [ "network-online.target" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig.Restart = "always";
      script = ''
        while true; do
        {
@@ -40,7 +34,7 @@ let
in

{
  siit = makeTest {
  siit = runTest {
    # 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.
@@ -49,8 +43,7 @@ in
    meta.maintainers = with lib.maintainers; [ rnhmjoj ];

    # Border relay
    nodes.relay = { ... }: {
      imports = [ ../modules/profiles/minimal.nix ];
    nodes.relay = {
      virtualisation.vlans = [ 1 2 ];

      # Enable packet routing
@@ -65,20 +58,13 @@ in
        eth2.ipv4.addresses = [ { address = "192.0.2.1";  prefixLength = 24; } ];
      };

      networking.jool = {
        enable = true;
        siit.enable = true;
        siit.config.global.pool6 = "fd::/96";
      };
      networking.jool.enable = true;
      networking.jool.siit.default.global.pool6 = "fd::/96";
    };

    # IPv6 only node
    nodes.alice = { ... }: {
      imports = [
        ../modules/profiles/minimal.nix
        ipv6Only
        (webserver 6 "Hello, Bob!")
      ];
    nodes.alice = {
      imports = [ ipv6Only (webserver 6 "Hello, Bob!") ];

      virtualisation.vlans = [ 1 ];
      networking.interfaces.eth1.ipv6 = {
@@ -89,12 +75,8 @@ in
    };

    # IPv4 only node
    nodes.bob = { ... }: {
      imports = [
        ../modules/profiles/minimal.nix
        ipv4Only
        (webserver 4 "Hello, Alice!")
      ];
    nodes.bob = {
      imports = [ ipv4Only (webserver 4 "Hello, Alice!") ];

      virtualisation.vlans = [ 2 ];
      networking.interfaces.eth1.ipv4 = {
@@ -107,17 +89,17 @@ in
    testScript = ''
      start_all()

      relay.wait_for_unit("jool-siit.service")
      relay.wait_for_unit("jool-siit-default.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")
        relay.systemctl("stop jool-siit-default.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")
        relay.systemctl("start jool-siit-default.service")
        alice.wait_until_succeeds("ping -c1 fd::192.0.2.16")
        bob.wait_until_succeeds("ping -c1 198.51.100.8")

@@ -132,7 +114,7 @@ in
    '';
  };

  nat64 = makeTest {
  nat64 = runTest {
    # 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
@@ -142,8 +124,7 @@ in
    meta.maintainers = with lib.maintainers; [ rnhmjoj ];

    # Router
    nodes.router = { ... }: {
      imports = [ ../modules/profiles/minimal.nix ];
    nodes.router = {
      virtualisation.vlans = [ 1 2 ];

      # Enable packet routing
@@ -158,10 +139,8 @@ in
        eth2.ipv4.addresses = [ { address = "203.0.113.1"; prefixLength = 24; } ];
      };

      networking.jool = {
        enable = true;
        nat64.enable = true;
        nat64.config = {
      networking.jool.enable = true;
      networking.jool.nat64.default = {
        bib = [
          { # forward HTTP 203.0.113.1 (router) → 2001:db8::9 (homeserver)
            "protocol"     = "TCP";
@@ -179,11 +158,10 @@ in
        ];
      };
    };
    };

    # LAN client (IPv6 only)
    nodes.client = { ... }: {
      imports = [ ../modules/profiles/minimal.nix ipv6Only ];
    nodes.client = {
      imports = [ ipv6Only ];
      virtualisation.vlans = [ 1 ];

      networking.interfaces.eth1.ipv6 = {
@@ -194,12 +172,8 @@ in
    };

    # LAN server (IPv6 only)
    nodes.homeserver = { ... }: {
      imports = [
        ../modules/profiles/minimal.nix
        ipv6Only
        (webserver 6 "Hello from IPv6!")
      ];
    nodes.homeserver = {
      imports = [ ipv6Only (webserver 6 "Hello from IPv6!") ];

      virtualisation.vlans = [ 1 ];
      networking.interfaces.eth1.ipv6 = {
@@ -210,12 +184,8 @@ in
    };

    # WAN server (IPv4 only)
    nodes.server = { ... }: {
      imports = [
        ../modules/profiles/minimal.nix
        ipv4Only
        (webserver 4 "Hello from IPv4!")
      ];
    nodes.server = {
      imports = [ ipv4Only (webserver 4 "Hello from IPv4!") ];

      virtualisation.vlans = [ 2 ];
      networking.interfaces.eth1.ipv4.addresses =
@@ -229,7 +199,7 @@ in
        node.wait_for_unit("network-addresses-eth1.service")

      with subtest("Client can ping the WAN server"):
        router.wait_for_unit("jool-nat64.service")
        router.wait_for_unit("jool-nat64-default.service")
        client.succeed("ping -c1 64:ff9b::203.0.113.16")

      with subtest("Client can connect to the WAN webserver"):