1{ config, lib, ... }:
2
3with lib;
4
5let
6 cfg = config.networking.nat;
7
8 mkDest = externalIP: if externalIP == null then "masquerade" else "snat ${externalIP}";
9 dest = mkDest cfg.externalIP;
10 destIPv6 = mkDest cfg.externalIPv6;
11
12 toNftSet = list: concatStringsSep ", " list;
13 toNftRange = ports: replaceStrings [ ":" ] [ "-" ] (toString ports);
14
15 ifaceSet = toNftSet (map (x: ''"${x}"'') cfg.internalInterfaces);
16 ipSet = toNftSet cfg.internalIPs;
17 ipv6Set = toNftSet cfg.internalIPv6s;
18 oifExpr = optionalString (cfg.externalInterface != null) ''oifname "${cfg.externalInterface}"'';
19
20 # Whether given IP (plus optional port) is an IPv6.
21 isIPv6 = ip: length (lib.splitString ":" ip) > 2;
22
23 splitIPPorts =
24 IPPorts:
25 let
26 matchIP = if isIPv6 IPPorts then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
27 m = builtins.match "${matchIP}:([0-9-]+)" IPPorts;
28 in
29 {
30 IP = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 0;
31 ports = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 1;
32 };
33
34 mkTable =
35 {
36 ipVer,
37 dest,
38 ipSet,
39 forwardPorts,
40 dmzHost,
41 externalIP,
42 }:
43 let
44 # nftables maps for port forward
45 # [daddr .] l4proto . dport : addr . port
46 fwdMap = toNftSet (
47 map (
48 fwd:
49 with (splitIPPorts fwd.destination);
50 "${
51 optionalString (externalIP != null) "${externalIP} . "
52 }${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
53 ) forwardPorts
54 );
55
56 # nftables maps for port forward loopback dnat
57 # daddr . l4proto . dport : addr . port
58 fwdLoopDnatMap = toNftSet (
59 concatMap (
60 fwd:
61 map (
62 loopbackip:
63 with (splitIPPorts fwd.destination);
64 "${loopbackip} . ${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
65 ) fwd.loopbackIPs
66 ) forwardPorts
67 );
68
69 # nftables set for port forward loopback snat
70 # daddr . l4proto . dport
71 fwdLoopSnatSet = toNftSet (
72 map (fwd: with (splitIPPorts fwd.destination); "${IP} . ${fwd.proto} . ${ports}") forwardPorts
73 );
74 in
75 ''
76 chain pre {
77 type nat hook prerouting priority dstnat;
78
79 ${optionalString (fwdMap != "") ''
80 iifname "${cfg.externalInterface}" meta l4proto { tcp, udp } dnat ${
81 optionalString (externalIP != null) "${ipVer} daddr . "
82 }meta l4proto . th dport map { ${fwdMap} } comment "port forward"
83 ''}
84
85 ${optionalString (fwdLoopDnatMap != "") ''
86 meta l4proto { tcp, udp } dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from other hosts behind NAT"
87 ''}
88
89 ${optionalString (dmzHost != null) ''
90 iifname "${cfg.externalInterface}" dnat ${dmzHost} comment "dmz"
91 ''}
92 }
93
94 chain post {
95 type nat hook postrouting priority srcnat;
96
97 ${optionalString (ifaceSet != "") ''
98 iifname { ${ifaceSet} } ${oifExpr} ${dest} comment "from internal interfaces"
99 ''}
100 ${optionalString (ipSet != "") ''
101 ${ipVer} saddr { ${ipSet} } ${oifExpr} ${dest} comment "from internal IPs"
102 ''}
103
104 ${optionalString (fwdLoopSnatSet != "") ''
105 iifname != "${cfg.externalInterface}" ${ipVer} daddr . meta l4proto . th dport { ${fwdLoopSnatSet} } masquerade comment "port forward loopback snat"
106 ''}
107 }
108
109 chain out {
110 type nat hook output priority mangle;
111
112 ${optionalString (fwdLoopDnatMap != "") ''
113 meta l4proto { tcp, udp } dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from the host itself"
114 ''}
115 }
116 '';
117
118in
119
120{
121
122 config = mkIf (config.networking.nftables.enable && cfg.enable) {
123
124 assertions = [
125 {
126 assertion = cfg.extraCommands == "";
127 message = "extraCommands is incompatible with the nftables based nat module: ${cfg.extraCommands}";
128 }
129 {
130 assertion = cfg.extraStopCommands == "";
131 message = "extraStopCommands is incompatible with the nftables based nat module: ${cfg.extraStopCommands}";
132 }
133 {
134 assertion = config.networking.nftables.rulesetFile == null;
135 message = "networking.nftables.rulesetFile conflicts with the nat module";
136 }
137 ];
138
139 networking.nftables.tables = {
140 "nixos-nat" = {
141 family = "ip";
142 content = mkTable {
143 ipVer = "ip";
144 inherit dest ipSet;
145 forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
146 inherit (cfg) dmzHost externalIP;
147 };
148 };
149 "nixos-nat6" = mkIf cfg.enableIPv6 {
150 family = "ip6";
151 name = "nixos-nat";
152 content = mkTable {
153 ipVer = "ip6";
154 dest = destIPv6;
155 ipSet = ipv6Set;
156 forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
157 dmzHost = null;
158 externalIP = cfg.externalIPv6;
159 };
160 };
161 };
162
163 networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward ''
164 ${optionalString (ifaceSet != "") ''
165 iifname { ${ifaceSet} } ${oifExpr} accept comment "from internal interfaces"
166 ''}
167 ${optionalString (ipSet != "") ''
168 ip saddr { ${ipSet} } ${oifExpr} accept comment "from internal IPs"
169 ''}
170 ${optionalString (ipv6Set != "") ''
171 ip6 saddr { ${ipv6Set} } ${oifExpr} accept comment "from internal IPv6s"
172 ''}
173 '';
174
175 };
176}