1{ config, pkgs, lib, ... }:
2
3let
4 cfg = config.networking.jool;
5
6 jool = config.boot.kernelPackages.jool;
7 jool-cli = pkgs.jool-cli;
8
9 hardening = {
10 # Run as unprivileged user
11 User = "jool";
12 Group = "jool";
13 DynamicUser = true;
14
15 # Restrict filesystem to only read the jool module
16 TemporaryFileSystem = [ "/" ];
17 BindReadOnlyPaths = [
18 builtins.storeDir
19 "/run/booted-system/kernel-modules"
20 ];
21
22 # Give capabilities to load the module and configure it
23 AmbientCapabilities = [ "CAP_SYS_MODULE" "CAP_NET_ADMIN" ];
24 RestrictAddressFamilies = [ "AF_NETLINK" ];
25
26 # Other restrictions
27 RestrictNamespaces = [ "net" ];
28 SystemCallFilter = [ "@system-service" "@module" ];
29 CapabilityBoundingSet = [ "CAP_SYS_MODULE" "CAP_NET_ADMIN" ];
30 };
31
32 configFormat = pkgs.formats.json {};
33
34 # Generate the config file of instance `name`
35 nat64Conf = name:
36 configFormat.generate "jool-nat64-${name}.conf"
37 (cfg.nat64.${name} // { instance = name; });
38 siitConf = name:
39 configFormat.generate "jool-siit-${name}.conf"
40 (cfg.siit.${name} // { instance = name; });
41
42 # NAT64 config type
43 nat64Options = lib.types.submodule {
44 # The format is plain JSON
45 freeformType = configFormat.type;
46 # Some options with a default value
47 options.framework = lib.mkOption {
48 type = lib.types.enum [ "netfilter" "iptables" ];
49 default = "netfilter";
50 description = ''
51 The framework to use for attaching Jool's translation to the exist
52 kernel packet processing rules. See the
53 [documentation](https://nicmx.github.io/Jool/en/intro-jool.html#design)
54 for the differences between the two options.
55 '';
56 };
57 options.global.pool6 = lib.mkOption {
58 type = lib.types.strMatching "[[:xdigit:]:]+/[[:digit:]]+"
59 // { description = "Network prefix in CIDR notation"; };
60 default = "64:ff9b::/96";
61 description = ''
62 The prefix used for embedding IPv4 into IPv6 addresses.
63 Defaults to the well-known NAT64 prefix, defined by
64 [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052).
65 '';
66 };
67 };
68
69 # SIIT config type
70 siitOptions = lib.types.submodule {
71 # The format is, again, plain JSON
72 freeformType = configFormat.type;
73 # Some options with a default value
74 options = { inherit (nat64Options.getSubOptions []) framework; };
75 };
76
77 makeNat64Unit = name: opts: {
78 "jool-nat64-${name}" = {
79 description = "Jool, NAT64 setup of instance ${name}";
80 documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ];
81 after = [ "network.target" ];
82 wantedBy = [ "multi-user.target" ];
83 serviceConfig = {
84 Type = "oneshot";
85 RemainAfterExit = true;
86 ExecStartPre = "${pkgs.kmod}/bin/modprobe jool";
87 ExecStart = "${jool-cli}/bin/jool file handle ${nat64Conf name}";
88 ExecStop = "${jool-cli}/bin/jool -f ${nat64Conf name} instance remove";
89 } // hardening;
90 };
91 };
92
93 makeSiitUnit = name: opts: {
94 "jool-siit-${name}" = {
95 description = "Jool, SIIT setup of instance ${name}";
96 documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ];
97 after = [ "network.target" ];
98 wantedBy = [ "multi-user.target" ];
99 serviceConfig = {
100 Type = "oneshot";
101 RemainAfterExit = true;
102 ExecStartPre = "${pkgs.kmod}/bin/modprobe jool_siit";
103 ExecStart = "${jool-cli}/bin/jool_siit file handle ${siitConf name}";
104 ExecStop = "${jool-cli}/bin/jool_siit -f ${siitConf name} instance remove";
105 } // hardening;
106 };
107 };
108
109 checkNat64 = name: _: ''
110 printf 'Validating Jool configuration for NAT64 instance "${name}"... '
111 jool file check ${nat64Conf name}
112 printf 'Ok.\n'; touch "$out"
113 '';
114
115 checkSiit = name: _: ''
116 printf 'Validating Jool configuration for SIIT instance "${name}"... '
117 jool_siit file check ${siitConf name}
118 printf 'Ok.\n'; touch "$out"
119 '';
120
121in
122
123{
124 options = {
125 networking.jool.enable = lib.mkOption {
126 type = lib.types.bool;
127 default = false;
128 relatedPackages = [ "linuxPackages.jool" "jool-cli" ];
129 description = ''
130 Whether to enable Jool, an Open Source implementation of IPv4/IPv6
131 translation on Linux.
132
133 Jool can perform stateless IP/ICMP translation (SIIT) or stateful
134 NAT64, analogous to the IPv4 NAPT. Refer to the upstream
135 [documentation](https://nicmx.github.io/Jool/en/intro-xlat.html) for
136 the supported modes of translation and how to configure them.
137
138 Enabling this option will install the Jool kernel module and the
139 command line tools for controlling it.
140 '';
141 };
142
143 networking.jool.nat64 = lib.mkOption {
144 type = lib.types.attrsOf nat64Options;
145 default = { };
146 example = lib.literalExpression ''
147 {
148 default = {
149 # custom NAT64 prefix
150 global.pool6 = "2001:db8:64::/96";
151
152 # Port forwarding
153 bib = [
154 { # SSH 192.0.2.16 → 2001:db8:a::1
155 "protocol" = "TCP";
156 "ipv4 address" = "192.0.2.16#22";
157 "ipv6 address" = "2001:db8:a::1#22";
158 }
159 { # DNS (TCP) 192.0.2.16 → 2001:db8:a::2
160 "protocol" = "TCP";
161 "ipv4 address" = "192.0.2.16#53";
162 "ipv6 address" = "2001:db8:a::2#53";
163 }
164 { # DNS (UDP) 192.0.2.16 → 2001:db8:a::2
165 "protocol" = "UDP";
166 "ipv4 address" = "192.0.2.16#53";
167 "ipv6 address" = "2001:db8:a::2#53";
168 }
169 ];
170
171 pool4 = [
172 # Port ranges for dynamic translation
173 { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; }
174 { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; }
175 { protocol = "ICMP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; }
176
177 # Ports for static BIB entries
178 { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "22"; }
179 { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "53"; }
180 ];
181 };
182 }
183 '';
184 description = ''
185 Definitions of NAT64 instances of Jool.
186 See the
187 [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for
188 the available options. Also check out the
189 [tutorial](https://nicmx.github.io/Jool/en/run-nat64.html) for an
190 introduction to NAT64 and how to troubleshoot the setup.
191
192 The attribute name defines the name of the instance, with the main one
193 being `default`: this can be accessed from the command line without
194 specifying the name with `-i`.
195
196 ::: {.note}
197 Instances created imperatively from the command line will not interfere
198 with the NixOS instances, provided the respective `pool4` addresses and
199 port ranges are not overlapping.
200 :::
201
202 ::: {.warning}
203 Changes to an instance performed via `jool -i <name>` are applied
204 correctly but will be lost after restarting the respective
205 `jool-nat64-<name>.service`.
206 :::
207 '';
208 };
209
210 networking.jool.siit = lib.mkOption {
211 type = lib.types.attrsOf siitOptions;
212 default = { };
213 example = lib.literalExpression ''
214 {
215 default = {
216 # Maps any IPv4 address x.y.z.t to 2001:db8::x.y.z.t and v.v.
217 global.pool6 = "2001:db8::/96";
218
219 # Explicit address mappings
220 eamt = [
221 # 2001:db8:1:: ←→ 192.0.2.0
222 { "ipv6 prefix" = "2001:db8:1::/128"; "ipv4 prefix" = "192.0.2.0"; }
223 # 2001:db8:1::x ←→ 198.51.100.x
224 { "ipv6 prefix" = "2001:db8:2::/120"; "ipv4 prefix" = "198.51.100.0/24"; }
225 ];
226 };
227 }
228 '';
229 description = ''
230 Definitions of SIIT instances of Jool.
231 See the
232 [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for
233 the available options. Also check out the
234 [tutorial](https://nicmx.github.io/Jool/en/run-vanilla.html) for an
235 introduction to SIIT and how to troubleshoot the setup.
236
237 The attribute name defines the name of the instance, with the main one
238 being `default`: this can be accessed from the command line without
239 specifying the name with `-i`.
240
241 ::: {.note}
242 Instances created imperatively from the command line will not interfere
243 with the NixOS instances, provided the respective EAMT addresses and
244 port ranges are not overlapping.
245 :::
246
247 ::: {.warning}
248 Changes to an instance performed via `jool -i <name>` are applied
249 correctly but will be lost after restarting the respective
250 `jool-siit-<name>.service`.
251 :::
252 '';
253 };
254
255 };
256
257 config = lib.mkIf cfg.enable {
258 # Install kernel module and cli tools
259 boot.extraModulePackages = [ jool ];
260 environment.systemPackages = [ jool-cli ];
261
262 # Install services for each instance
263 systemd.services = lib.mkMerge
264 (lib.mapAttrsToList makeNat64Unit cfg.nat64 ++
265 lib.mapAttrsToList makeSiitUnit cfg.siit);
266
267 # Check the configuration of each instance
268 system.checks = lib.optional (cfg.nat64 != {} || cfg.siit != {})
269 (pkgs.runCommand "jool-validated"
270 {
271 nativeBuildInputs = with pkgs.buildPackages; [ jool-cli ];
272 preferLocalBuild = true;
273 }
274 (lib.concatStrings
275 (lib.mapAttrsToList checkNat64 cfg.nat64 ++
276 lib.mapAttrsToList checkSiit cfg.siit)));
277 };
278
279 meta.maintainers = with lib.maintainers; [ rnhmjoj ];
280
281}