1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 cfg = config.networking.nftables;
9
10 tableSubmodule =
11 { name, ... }:
12 {
13 options = {
14 enable = lib.mkOption {
15 type = lib.types.bool;
16 default = true;
17 description = "Enable this table.";
18 };
19
20 name = lib.mkOption {
21 type = lib.types.str;
22 description = "Table name.";
23 };
24
25 content = lib.mkOption {
26 type = lib.types.lines;
27 description = "The table content.";
28 };
29
30 family = lib.mkOption {
31 description = "Table family.";
32 type = lib.types.enum [
33 "ip"
34 "ip6"
35 "inet"
36 "arp"
37 "bridge"
38 "netdev"
39 ];
40 };
41 };
42
43 config = {
44 name = lib.mkDefault name;
45 };
46 };
47in
48{
49 ###### interface
50
51 options = {
52 networking.nftables.enable = lib.mkOption {
53 type = lib.types.bool;
54 default = false;
55 description = ''
56 Whether to enable nftables and use nftables based firewall if enabled.
57 nftables is a Linux-based packet filtering framework intended to
58 replace frameworks like iptables.
59
60 Note that if you have Docker enabled you will not be able to use
61 nftables without intervention. Docker uses iptables internally to
62 setup NAT for containers. This module disables the ip_tables kernel
63 module, however Docker automatically loads the module. Please see
64 <https://github.com/NixOS/nixpkgs/issues/24318#issuecomment-289216273>
65 for more information.
66
67 There are other programs that use iptables internally too, such as
68 libvirt. For information on how the two firewalls interact, see
69 <https://wiki.nftables.org/wiki-nftables/index.php/Troubleshooting#Question_4._How_do_nftables_and_iptables_interact_when_used_on_the_same_system.3F>.
70 '';
71 };
72
73 networking.nftables.checkRuleset = lib.mkOption {
74 type = lib.types.bool;
75 default = true;
76 description = ''
77 Run `nft check` on the ruleset to spot syntax errors during build.
78 Because this is executed in a sandbox, the check might fail if it requires
79 access to any environmental factors or paths outside the Nix store.
80 To circumvent this, the ruleset file can be edited using the preCheckRuleset
81 option to work in the sandbox environment.
82 '';
83 };
84
85 networking.nftables.checkRulesetRedirects = lib.mkOption {
86 type = lib.types.addCheck (lib.types.attrsOf lib.types.path) (
87 attrs: lib.all lib.types.path.check (lib.attrNames attrs)
88 );
89 default = {
90 "/etc/hosts" = config.environment.etc.hosts.source;
91 "/etc/protocols" = config.environment.etc.protocols.source;
92 "/etc/services" = config.environment.etc.services.source;
93 };
94 defaultText = lib.literalExpression ''
95 {
96 "/etc/hosts" = config.environment.etc.hosts.source;
97 "/etc/protocols" = config.environment.etc.protocols.source;
98 "/etc/services" = config.environment.etc.services.source;
99 }
100 '';
101 description = ''
102 Set of paths that should be intercepted and rewritten while checking the ruleset
103 using `pkgs.buildPackages.libredirect`.
104 '';
105 };
106
107 networking.nftables.preCheckRuleset = lib.mkOption {
108 type = lib.types.lines;
109 default = "";
110 example = lib.literalExpression ''
111 sed 's/skgid meadow/skgid nogroup/g' -i ruleset.conf
112 '';
113 description = ''
114 This script gets run before the ruleset is checked. It can be used to
115 create additional files needed for the ruleset check to work, or modify
116 the ruleset for cases the build environment cannot cover.
117 '';
118 };
119
120 networking.nftables.flushRuleset = lib.mkEnableOption "flushing the entire ruleset on each reload";
121
122 networking.nftables.extraDeletions = lib.mkOption {
123 type = lib.types.lines;
124 default = "";
125 example = ''
126 # this makes deleting a non-existing table a no-op instead of an error
127 table inet some-table;
128
129 delete table inet some-table;
130 '';
131 description = ''
132 Extra deletion commands to be run on every firewall start, reload
133 and after stopping the firewall.
134 '';
135 };
136
137 networking.nftables.ruleset = lib.mkOption {
138 type = lib.types.lines;
139 default = "";
140 example = ''
141 # Check out https://wiki.nftables.org/ for better documentation.
142 # Table for both IPv4 and IPv6.
143 table inet filter {
144 # Block all incoming connections traffic except SSH and "ping".
145 chain input {
146 type filter hook input priority 0;
147
148 # accept any localhost traffic
149 iifname lo accept
150
151 # accept traffic originated from us
152 ct state {established, related} accept
153
154 # ICMP
155 # routers may also want: mld-listener-query, nd-router-solicit
156 ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
157 ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
158
159 # allow "ping"
160 ip6 nexthdr icmpv6 icmpv6 type echo-request accept
161 ip protocol icmp icmp type echo-request accept
162
163 # accept SSH connections (required for a server)
164 tcp dport 22 accept
165
166 # count and drop any other traffic
167 counter drop
168 }
169
170 # Allow all outgoing connections.
171 chain output {
172 type filter hook output priority 0;
173 accept
174 }
175
176 chain forward {
177 type filter hook forward priority 0;
178 accept
179 }
180 }
181 '';
182 description = ''
183 The ruleset to be used with nftables. Should be in a format that
184 can be loaded using "/bin/nft -f". The ruleset is updated atomically.
185 Note that if the tables should be cleaned first, either:
186 - networking.nftables.flushRuleset = true; needs to be set (flushes all tables)
187 - networking.nftables.extraDeletions needs to be set
188 - or networking.nftables.tables can be used, which will clean up the table automatically
189 '';
190 };
191 networking.nftables.rulesetFile = lib.mkOption {
192 type = lib.types.nullOr lib.types.path;
193 default = null;
194 description = ''
195 The ruleset file to be used with nftables. Should be in a format that
196 can be loaded using "nft -f". The ruleset is updated atomically.
197 '';
198 };
199
200 networking.nftables.flattenRulesetFile = lib.mkOption {
201 type = lib.types.bool;
202 default = false;
203 description = ''
204 Use `builtins.readFile` rather than `include` to handle {option}`networking.nftables.rulesetFile`. It is useful when you want to apply {option}`networking.nftables.preCheckRuleset` to {option}`networking.nftables.rulesetFile`.
205
206 ::: {.note}
207 It is expected that {option}`networking.nftables.rulesetFile` can be accessed from the build sandbox.
208 :::
209 '';
210 };
211
212 networking.nftables.tables = lib.mkOption {
213 type = lib.types.attrsOf (lib.types.submodule tableSubmodule);
214
215 default = { };
216
217 description = ''
218 Tables to be added to ruleset.
219 Tables will be added together with delete statements to clean up the table before every update.
220 '';
221
222 example = {
223 filter = {
224 family = "inet";
225 content = ''
226 # Check out https://wiki.nftables.org/ for better documentation.
227 # Table for both IPv4 and IPv6.
228 # Block all incoming connections traffic except SSH and "ping".
229 chain input {
230 type filter hook input priority 0;
231
232 # accept any localhost traffic
233 iifname lo accept
234
235 # accept traffic originated from us
236 ct state {established, related} accept
237
238 # ICMP
239 # routers may also want: mld-listener-query, nd-router-solicit
240 ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
241 ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
242
243 # allow "ping"
244 ip6 nexthdr icmpv6 icmpv6 type echo-request accept
245 ip protocol icmp icmp type echo-request accept
246
247 # accept SSH connections (required for a server)
248 tcp dport 22 accept
249
250 # count and drop any other traffic
251 counter drop
252 }
253
254 # Allow all outgoing connections.
255 chain output {
256 type filter hook output priority 0;
257 accept
258 }
259
260 chain forward {
261 type filter hook forward priority 0;
262 accept
263 }
264 '';
265 };
266 };
267 };
268 };
269
270 ###### implementation
271
272 config = lib.mkIf cfg.enable {
273 boot.blacklistedKernelModules = [ "ip_tables" ];
274 environment.systemPackages = [ pkgs.nftables ];
275 # versionOlder for backportability, remove afterwards
276 networking.nftables.flushRuleset = lib.mkDefault (
277 lib.versionOlder config.system.stateVersion "23.11"
278 || (cfg.rulesetFile != null || cfg.ruleset != "")
279 );
280 systemd.services.nftables = {
281 description = "nftables firewall";
282 after = [ "sysinit.target" ];
283 before = [
284 "network-pre.target"
285 "shutdown.target"
286 ];
287 conflicts = [ "shutdown.target" ];
288 wants = [
289 "network-pre.target"
290 "sysinit.target"
291 ];
292 wantedBy = [ "multi-user.target" ];
293 reloadIfChanged = true;
294 serviceConfig =
295 let
296 enabledTables = lib.filterAttrs (_: table: table.enable) cfg.tables;
297 deletionsScript = pkgs.writeScript "nftables-deletions" ''
298 #! ${pkgs.nftables}/bin/nft -f
299 ${
300 if cfg.flushRuleset then
301 "flush ruleset"
302 else
303 lib.concatStringsSep "\n" (
304 lib.mapAttrsToList (_: table: ''
305 table ${table.family} ${table.name}
306 delete table ${table.family} ${table.name}
307 '') enabledTables
308 )
309 }
310 ${cfg.extraDeletions}
311 '';
312 deletionsScriptVar = "/var/lib/nftables/deletions.nft";
313 ensureDeletions = pkgs.writeShellScript "nftables-ensure-deletions" ''
314 touch ${deletionsScriptVar}
315 chmod +x ${deletionsScriptVar}
316 '';
317 saveDeletionsScript = pkgs.writeShellScript "nftables-save-deletions" ''
318 cp ${deletionsScript} ${deletionsScriptVar}
319 '';
320 cleanupDeletionsScript = pkgs.writeShellScript "nftables-cleanup-deletions" ''
321 rm ${deletionsScriptVar}
322 '';
323 rulesScript = pkgs.writeTextFile {
324 name = "nftables-rules";
325 executable = true;
326 text = ''
327 #! ${pkgs.nftables}/bin/nft -f
328 # previous deletions, if any
329 include "${deletionsScriptVar}"
330 # current deletions
331 include "${deletionsScript}"
332 ${lib.concatStringsSep "\n" (
333 lib.mapAttrsToList (_: table: ''
334 table ${table.family} ${table.name} {
335 ${table.content}
336 }
337 '') enabledTables
338 )}
339 ${cfg.ruleset}
340 ${
341 if cfg.rulesetFile != null then
342 if cfg.flattenRulesetFile then
343 builtins.readFile cfg.rulesetFile
344 else
345 ''
346 include "${cfg.rulesetFile}"
347 ''
348 else
349 ""
350 }
351 '';
352 checkPhase = lib.optionalString cfg.checkRuleset ''
353 cp $out ruleset.conf
354 sed 's|include "${deletionsScriptVar}"||' -i ruleset.conf
355 ${cfg.preCheckRuleset}
356 export NIX_REDIRECTS=${
357 lib.escapeShellArg (
358 lib.concatStringsSep ":" (lib.mapAttrsToList (n: v: "${n}=${v}") cfg.checkRulesetRedirects)
359 )
360 }
361 LD_PRELOAD="${pkgs.buildPackages.libredirect}/lib/libredirect.so ${pkgs.buildPackages.lklWithFirewall.lib}/lib/liblkl-hijack.so" \
362 ${pkgs.buildPackages.nftables}/bin/nft --check --file ruleset.conf
363 '';
364 };
365 in
366 {
367 Type = "oneshot";
368 RemainAfterExit = true;
369 ExecStart = [
370 ensureDeletions
371 rulesScript
372 ];
373 ExecStartPost = saveDeletionsScript;
374 ExecReload = [
375 ensureDeletions
376 rulesScript
377 saveDeletionsScript
378 ];
379 ExecStop = [
380 deletionsScriptVar
381 cleanupDeletionsScript
382 ];
383 StateDirectory = "nftables";
384 };
385 unitConfig.DefaultDependencies = false;
386 };
387 };
388}