1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.networking.firewall;
10
11 ifaceSet = lib.concatStringsSep ", " (map (x: ''"${x}"'') cfg.trustedInterfaces);
12
13 portsToNftSet =
14 ports: portRanges:
15 lib.concatStringsSep ", " (
16 map (x: toString x) ports ++ map (x: "${toString x.from}-${toString x.to}") portRanges
17 );
18
19in
20
21{
22
23 options = {
24
25 networking.firewall = {
26 extraInputRules = lib.mkOption {
27 type = lib.types.lines;
28 default = "";
29 example = "ip6 saddr { fc00::/7, fe80::/10 } tcp dport 24800 accept";
30 description = ''
31 Additional nftables rules to be appended to the input-allow
32 chain.
33
34 This option only works with the nftables based firewall.
35 '';
36 };
37
38 extraForwardRules = lib.mkOption {
39 type = lib.types.lines;
40 default = "";
41 example = "iifname wg0 accept";
42 description = ''
43 Additional nftables rules to be appended to the forward-allow
44 chain.
45
46 This option only works with the nftables based firewall.
47 '';
48 };
49
50 extraReversePathFilterRules = lib.mkOption {
51 type = lib.types.lines;
52 default = "";
53 example = "fib daddr . mark . iif type local accept";
54 description = ''
55 Additional nftables rules to be appended to the rpfilter-allow
56 chain.
57
58 This option only works with the nftables based firewall.
59 '';
60 };
61 };
62
63 };
64
65 config = lib.mkIf (cfg.enable && config.networking.nftables.enable) {
66
67 assertions = [
68 {
69 assertion = cfg.extraCommands == "";
70 message = "extraCommands is incompatible with the nftables based firewall: ${cfg.extraCommands}";
71 }
72 {
73 assertion = cfg.extraStopCommands == "";
74 message = "extraStopCommands is incompatible with the nftables based firewall: ${cfg.extraStopCommands}";
75 }
76 {
77 assertion = cfg.pingLimit == null || !(lib.hasPrefix "--" cfg.pingLimit);
78 message = "nftables syntax like \"2/second\" should be used in networking.firewall.pingLimit";
79 }
80 {
81 assertion = config.networking.nftables.rulesetFile == null;
82 message = "networking.nftables.rulesetFile conflicts with the firewall";
83 }
84 ];
85
86 networking.nftables.tables."nixos-fw".family = "inet";
87 networking.nftables.tables."nixos-fw".content = ''
88 set temp-ports {
89 comment "Temporarily opened ports"
90 type inet_proto . inet_service
91 flags interval
92 auto-merge
93 }
94
95 ${lib.optionalString (cfg.checkReversePath != false) ''
96 chain rpfilter {
97 type filter hook prerouting priority mangle + 10; policy drop;
98
99 meta nfproto ipv4 udp sport . udp dport { 67 . 68, 68 . 67 } accept comment "DHCPv4 client/server"
100 fib saddr . mark ${lib.optionalString (cfg.checkReversePath != "loose") ". iif"} oif exists accept
101
102 jump rpfilter-allow
103
104 ${lib.optionalString cfg.logReversePathDrops ''
105 log level info prefix "rpfilter drop: "
106 ''}
107
108 }
109 ''}
110
111 chain rpfilter-allow {
112 ${cfg.extraReversePathFilterRules}
113 }
114
115 chain input {
116 type filter hook input priority filter; policy drop;
117
118 ${lib.optionalString (
119 ifaceSet != ""
120 ) ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''}
121
122 # Some ICMPv6 types like NDP is untracked
123 ct state vmap {
124 invalid : drop,
125 established : accept,
126 related : accept,
127 new : jump input-allow,
128 untracked: jump input-allow,
129 }
130
131 ${lib.optionalString cfg.logRefusedConnections ''
132 tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: "
133 ''}
134 ${lib.optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
135 pkttype broadcast log level info prefix "refused broadcast: "
136 pkttype multicast log level info prefix "refused multicast: "
137 ''}
138 ${lib.optionalString cfg.logRefusedPackets ''
139 pkttype host log level info prefix "refused packet: "
140 ''}
141
142 ${lib.optionalString cfg.rejectPackets ''
143 meta l4proto tcp reject with tcp reset
144 reject
145 ''}
146
147 }
148
149 chain input-allow {
150
151 ${lib.concatStrings (
152 lib.mapAttrsToList (
153 iface: cfg:
154 let
155 ifaceExpr = lib.optionalString (iface != "default") "iifname ${iface}";
156 tcpSet = portsToNftSet cfg.allowedTCPPorts cfg.allowedTCPPortRanges;
157 udpSet = portsToNftSet cfg.allowedUDPPorts cfg.allowedUDPPortRanges;
158 in
159 ''
160 ${lib.optionalString (tcpSet != "") "${ifaceExpr} tcp dport { ${tcpSet} } accept"}
161 ${lib.optionalString (udpSet != "") "${ifaceExpr} udp dport { ${udpSet} } accept"}
162 ''
163 ) cfg.allInterfaces
164 )}
165
166 meta l4proto . th dport @temp-ports accept
167
168 ${lib.optionalString cfg.allowPing ''
169 icmp type echo-request ${
170 lib.optionalString (cfg.pingLimit != null) "limit rate ${cfg.pingLimit}"
171 } accept comment "allow ping"
172 ''}
173
174 icmpv6 type != { nd-redirect, 139 } accept comment "Accept all ICMPv6 messages except redirects and node information queries (type 139). See RFC 4890, section 4.4."
175 ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client"
176
177 ${cfg.extraInputRules}
178
179 }
180
181 ${lib.optionalString cfg.filterForward ''
182 chain forward {
183 type filter hook forward priority filter; policy drop;
184
185 ct state vmap {
186 invalid : drop,
187 established : accept,
188 related : accept,
189 new : jump forward-allow,
190 untracked : jump forward-allow,
191 }
192
193 }
194
195 chain forward-allow {
196
197 icmpv6 type != { router-renumbering, 139 } accept comment "Accept all ICMPv6 messages except renumbering and node information queries (type 139). See RFC 4890, section 4.3."
198
199 ct status dnat accept comment "allow port forward"
200
201 ${cfg.extraForwardRules}
202
203 }
204 ''}
205 '';
206
207 };
208
209}