about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--nixos/doc/manual/release-notes/rl-2311.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/networking/jool.nix222
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/jool.nix250
-rw-r--r--pkgs/os-specific/linux/jool/cli.nix10
-rw-r--r--pkgs/os-specific/linux/jool/default.nix4
-rw-r--r--pkgs/os-specific/linux/jool/validate-config.patch193
8 files changed, 681 insertions, 2 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!")
+    '';
+
+  };
+
+}
diff --git a/pkgs/os-specific/linux/jool/cli.nix b/pkgs/os-specific/linux/jool/cli.nix
index e6a41ef31ecf8..ee5ee1128a867 100644
--- a/pkgs/os-specific/linux/jool/cli.nix
+++ b/pkgs/os-specific/linux/jool/cli.nix
@@ -1,4 +1,6 @@
-{ lib, stdenv, fetchFromGitHub, fetchpatch, autoreconfHook, pkg-config, libnl, iptables }:
+{ lib, stdenv, fetchFromGitHub, nixosTests
+, autoreconfHook, pkg-config, libnl, iptables
+}:
 
 let
   sourceAttrs = (import ./source.nix) { inherit fetchFromGitHub; };
@@ -10,6 +12,10 @@ stdenv.mkDerivation {
 
   src = sourceAttrs.src;
 
+  patches = [
+    ./validate-config.patch
+  ];
+
   outputs = [
     "out"
     "man"
@@ -24,6 +30,8 @@ stdenv.mkDerivation {
     sed -e 's%^XTABLES_SO_DIR = .*%XTABLES_SO_DIR = '"$out"'/lib/xtables%g' -i src/usr/iptables/Makefile
   '';
 
+  passthru.tests = { inherit (nixosTests) jool; };
+
   meta = with lib; {
     homepage = "https://www.jool.mx/";
     description = "Fairly compliant SIIT and Stateful NAT64 for Linux - CLI tools";
diff --git a/pkgs/os-specific/linux/jool/default.nix b/pkgs/os-specific/linux/jool/default.nix
index 2ee5f0d6e0784..91276cbc11b17 100644
--- a/pkgs/os-specific/linux/jool/default.nix
+++ b/pkgs/os-specific/linux/jool/default.nix
@@ -1,4 +1,4 @@
-{ lib, stdenv, fetchFromGitHub, fetchpatch, kernel }:
+{ lib, stdenv, fetchFromGitHub, kernel, nixosTests }:
 
 let
   sourceAttrs = (import ./source.nix) { inherit fetchFromGitHub; };
@@ -23,6 +23,8 @@ stdenv.mkDerivation {
 
   installTargets = "modules_install";
 
+  passthru.tests = { inherit (nixosTests) jool; };
+
   meta = with lib; {
     homepage = "https://www.jool.mx/";
     description = "Fairly compliant SIIT and Stateful NAT64 for Linux - kernel modules";
diff --git a/pkgs/os-specific/linux/jool/validate-config.patch b/pkgs/os-specific/linux/jool/validate-config.patch
new file mode 100644
index 0000000000000..8841b6fb14f34
--- /dev/null
+++ b/pkgs/os-specific/linux/jool/validate-config.patch
@@ -0,0 +1,193 @@
+From df0a1cf61188b5b7bb98675d746cb63d9300f148 Mon Sep 17 00:00:00 2001
+From: rnhmjoj <rnhmjoj@inventati.org>
+Date: Sat, 1 Jul 2023 18:47:05 +0200
+Subject: [PATCH] Add mode to validate the atomic configuration
+
+---
+ src/usr/argp/main.c       |  6 ++++++
+ src/usr/argp/wargp/file.c | 26 +++++++++++++++++++++++++-
+ src/usr/argp/wargp/file.h |  1 +
+ src/usr/nl/file.c         | 32 ++++++++++++++++++++++----------
+ src/usr/nl/file.h         |  3 ++-
+ 5 files changed, 56 insertions(+), 12 deletions(-)
+
+diff --git a/src/usr/argp/main.c b/src/usr/argp/main.c
+index 744a6df0..d04917da 100644
+--- a/src/usr/argp/main.c
++++ b/src/usr/argp/main.c
+@@ -238,6 +238,12 @@ static struct cmd_option file_ops[] = {
+ 			.handler = handle_file_update,
+ 			.handle_autocomplete = autocomplete_file_update,
+ 		},
++		{
++			.label = "check",
++			.xt = XT_ANY,
++			.handler = handle_file_check,
++			.handle_autocomplete = autocomplete_file_update,
++		},
+ 		{ 0 },
+ };
+ 
+diff --git a/src/usr/argp/wargp/file.c b/src/usr/argp/wargp/file.c
+index 0951b544..27ee3e64 100644
+--- a/src/usr/argp/wargp/file.c
++++ b/src/usr/argp/wargp/file.c
+@@ -26,6 +26,30 @@ static struct wargp_option update_opts[] = {
+ 	{ 0 },
+ };
+ 
++int handle_file_check(char *iname, int argc, char **argv, void const *arg)
++{
++	struct update_args uargs = { 0 };
++	struct joolnl_socket sk = { 0 };
++	struct jool_result result;
++
++	result.error = wargp_parse(update_opts, argc, argv, &uargs);
++	if (result.error)
++		return result.error;
++
++	if (!uargs.file_name.value) {
++		struct requirement reqs[] = {
++				{ false, "a file name" },
++				{ 0 }
++		};
++		return requirement_print(reqs);
++	}
++
++	result = joolnl_file_parse(&sk, xt_get(), iname, uargs.file_name.value,
++			uargs.force.value, true);
++
++	return pr_result(&result);
++}
++
+ int handle_file_update(char *iname, int argc, char **argv, void const *arg)
+ {
+ 	struct update_args uargs = { 0 };
+@@ -49,7 +73,7 @@ int handle_file_update(char *iname, int argc, char **argv, void const *arg)
+ 		return pr_result(&result);
+ 
+ 	result = joolnl_file_parse(&sk, xt_get(), iname, uargs.file_name.value,
+-			uargs.force.value);
++			uargs.force.value, false);
+ 
+ 	joolnl_teardown(&sk);
+ 	return pr_result(&result);
+diff --git a/src/usr/argp/wargp/file.h b/src/usr/argp/wargp/file.h
+index ce5de508..8ea4a4d2 100644
+--- a/src/usr/argp/wargp/file.h
++++ b/src/usr/argp/wargp/file.h
+@@ -2,6 +2,7 @@
+ #define SRC_USR_ARGP_WARGP_FILE_H_
+ 
+ int handle_file_update(char *iname, int argc, char **argv, void const *arg);
++int handle_file_check(char *iname, int argc, char **argv, void const *arg);
+ void autocomplete_file_update(void const *args);
+ 
+ #endif /* SRC_USR_ARGP_WARGP_FILE_H_ */
+diff --git a/src/usr/nl/file.c b/src/usr/nl/file.c
+index f9413236..51a668bd 100644
+--- a/src/usr/nl/file.c
++++ b/src/usr/nl/file.c
+@@ -29,6 +29,7 @@ static struct joolnl_socket sk;
+ static char const *iname;
+ static xlator_flags flags;
+ static __u8 force;
++static bool check;
+ 
+ struct json_meta {
+ 	char const *name; /* This being NULL signals the end of the array. */
+@@ -163,9 +164,11 @@ static struct jool_result handle_array(cJSON *json, int attrtype, char *name,
+ 				goto too_small;
+ 
+ 			nla_nest_end(msg, root);
+-			result = joolnl_request(&sk, msg, NULL, NULL);
+-			if (result.error)
+-				return result;
++			if (!check) {
++				result = joolnl_request(&sk, msg, NULL, NULL);
++				if (result.error)
++					return result;
++			}
+ 
+ 			msg = NULL;
+ 			json = json->prev;
+@@ -179,6 +182,8 @@ static struct jool_result handle_array(cJSON *json, int attrtype, char *name,
+ 		return result_success();
+ 
+ 	nla_nest_end(msg, root);
++	if (check)
++		return result_success();
+ 	return joolnl_request(&sk, msg, NULL, NULL);
+ 
+ too_small:
+@@ -244,6 +249,8 @@ static struct jool_result handle_global(cJSON *json)
+ 
+ 	nla_nest_end(msg, root);
+ 	free(meta);
++	if (check)
++		return result_success();
+ 	return joolnl_request(&sk, msg, NULL, NULL);
+ 
+ revert_meta:
+@@ -654,9 +661,11 @@ static struct jool_result send_ctrl_msg(bool init)
+ 	else
+ 		NLA_PUT(msg, JNLAR_ATOMIC_END, 0, NULL);
+ 
+-	result = joolnl_request(&sk, msg, NULL, NULL);
+-	if (result.error)
+-		return result;
++	if (!check) {
++		result = joolnl_request(&sk, msg, NULL, NULL);
++		if (result.error)
++			return result;
++	}
+ 
+ 	return result_success();
+ 
+@@ -683,9 +692,11 @@ static struct jool_result do_parsing(char const *iname, char *buffer)
+ 	if (result.error)
+ 		goto fail;
+ 
+-	result = send_ctrl_msg(true);
+-	if (result.error)
+-		goto fail;
++	if (!check) {
++		result = send_ctrl_msg(true);
++		if (result.error)
++			goto fail;
++	}
+ 
+ 	switch (xlator_flags2xt(flags)) {
+ 	case XT_SIIT:
+@@ -718,12 +729,13 @@ fail:
+ }
+ 
+ struct jool_result joolnl_file_parse(struct joolnl_socket *_sk, xlator_type xt,
+-		char const *iname, char const *file_name, bool _force)
++		char const *iname, char const *file_name, bool _force, bool _check)
+ {
+ 	char *buffer;
+ 	struct jool_result result;
+ 
+ 	sk = *_sk;
++	check = _check;
+ 	flags = xt;
+ 	force = _force ? JOOLNLHDR_FLAGS_FORCE : 0;
+ 
+diff --git a/src/usr/nl/file.h b/src/usr/nl/file.h
+index 51802aaf..8b4a66dd 100644
+--- a/src/usr/nl/file.h
++++ b/src/usr/nl/file.h
+@@ -9,7 +9,8 @@ struct jool_result joolnl_file_parse(
+ 	xlator_type xt,
+ 	char const *iname,
+ 	char const *file_name,
+-	bool force
++	bool force,
++	bool check
+ );
+ 
+ struct jool_result joolnl_file_get_iname(
+-- 
+2.40.1
+