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