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