1# This module enables Network Address Translation (NAT). 2# XXX: todo: support multiple upstream links 3# see http://yesican.chsoft.biz/lartc/MultihomedLinuxNetworking.html 4 5{ config, lib, pkgs, ... }: 6 7with lib; 8 9let 10 11 cfg = config.networking.nat; 12 13 dest = if cfg.externalIP == null then "-j MASQUERADE" else "-j SNAT --to-source ${cfg.externalIP}"; 14 15 flushNat = '' 16 iptables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true 17 iptables -w -t nat -F nixos-nat-pre 2>/dev/null || true 18 iptables -w -t nat -X nixos-nat-pre 2>/dev/null || true 19 iptables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true 20 iptables -w -t nat -F nixos-nat-post 2>/dev/null || true 21 iptables -w -t nat -X nixos-nat-post 2>/dev/null || true 22 23 ${cfg.extraStopCommands} 24 ''; 25 26 setupNat = '' 27 # Create subchain where we store rules 28 iptables -w -t nat -N nixos-nat-pre 29 iptables -w -t nat -N nixos-nat-post 30 31 # We can't match on incoming interface in POSTROUTING, so 32 # mark packets coming from the external interfaces. 33 ${concatMapStrings (iface: '' 34 iptables -w -t nat -A nixos-nat-pre \ 35 -i '${iface}' -j MARK --set-mark 1 36 '') cfg.internalInterfaces} 37 38 # NAT the marked packets. 39 ${optionalString (cfg.internalInterfaces != []) '' 40 iptables -w -t nat -A nixos-nat-post -m mark --mark 1 \ 41 -o ${cfg.externalInterface} ${dest} 42 ''} 43 44 # NAT packets coming from the internal IPs. 45 ${concatMapStrings (range: '' 46 iptables -w -t nat -A nixos-nat-post \ 47 -s '${range}' -o ${cfg.externalInterface} ${dest} 48 '') cfg.internalIPs} 49 50 # NAT from external ports to internal ports. 51 ${concatMapStrings (fwd: '' 52 iptables -w -t nat -A nixos-nat-pre \ 53 -i ${cfg.externalInterface} -p ${fwd.proto} \ 54 --dport ${builtins.toString fwd.sourcePort} \ 55 -j DNAT --to-destination ${fwd.destination} 56 57 ${concatMapStrings (loopbackip: 58 let 59 m = builtins.match "([0-9.]+):([0-9-]+)" fwd.destination; 60 destinationIP = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0; 61 destinationPorts = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 1; 62 in '' 63 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself 64 iptables -w -t nat -A OUTPUT \ 65 -d ${loopbackip} -p ${fwd.proto} \ 66 --dport ${builtins.toString fwd.sourcePort} \ 67 -j DNAT --to-destination ${fwd.destination} 68 69 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT 70 iptables -w -t nat -A nixos-nat-pre \ 71 -d ${loopbackip} -p ${fwd.proto} \ 72 --dport ${builtins.toString fwd.sourcePort} \ 73 -j DNAT --to-destination ${fwd.destination} 74 75 iptables -w -t nat -A nixos-nat-post \ 76 -d ${destinationIP} -p ${fwd.proto} \ 77 --dport ${destinationPorts} \ 78 -j SNAT --to-source ${loopbackip} 79 '') fwd.loopbackIPs} 80 '') cfg.forwardPorts} 81 82 ${optionalString (cfg.dmzHost != null) '' 83 iptables -w -t nat -A nixos-nat-pre \ 84 -i ${cfg.externalInterface} -j DNAT \ 85 --to-destination ${cfg.dmzHost} 86 ''} 87 88 ${cfg.extraCommands} 89 90 # Append our chains to the nat tables 91 iptables -w -t nat -A PREROUTING -j nixos-nat-pre 92 iptables -w -t nat -A POSTROUTING -j nixos-nat-post 93 ''; 94 95in 96 97{ 98 99 ###### interface 100 101 options = { 102 103 networking.nat.enable = mkOption { 104 type = types.bool; 105 default = false; 106 description = 107 '' 108 Whether to enable Network Address Translation (NAT). 109 ''; 110 }; 111 112 networking.nat.internalInterfaces = mkOption { 113 type = types.listOf types.str; 114 default = []; 115 example = [ "eth0" ]; 116 description = 117 '' 118 The interfaces for which to perform NAT. Packets coming from 119 these interface and destined for the external interface will 120 be rewritten. 121 ''; 122 }; 123 124 networking.nat.internalIPs = mkOption { 125 type = types.listOf types.str; 126 default = []; 127 example = [ "192.168.1.0/24" ]; 128 description = 129 '' 130 The IP address ranges for which to perform NAT. Packets 131 coming from these addresses (on any interface) and destined 132 for the external interface will be rewritten. 133 ''; 134 }; 135 136 networking.nat.externalInterface = mkOption { 137 type = types.str; 138 example = "eth1"; 139 description = 140 '' 141 The name of the external network interface. 142 ''; 143 }; 144 145 networking.nat.externalIP = mkOption { 146 type = types.nullOr types.str; 147 default = null; 148 example = "203.0.113.123"; 149 description = 150 '' 151 The public IP address to which packets from the local 152 network are to be rewritten. If this is left empty, the 153 IP address associated with the external interface will be 154 used. 155 ''; 156 }; 157 158 networking.nat.forwardPorts = mkOption { 159 type = with types; listOf (submodule { 160 options = { 161 sourcePort = mkOption { 162 type = types.either types.int (types.strMatching "[[:digit:]]+:[[:digit:]]+"); 163 example = 8080; 164 description = "Source port of the external interface; to specify a port range, use a string with a colon (e.g. \"60000:61000\")"; 165 }; 166 167 destination = mkOption { 168 type = types.str; 169 example = "10.0.0.1:80"; 170 description = "Forward connection to destination ip:port; to specify a port range, use ip:start-end"; 171 }; 172 173 proto = mkOption { 174 type = types.str; 175 default = "tcp"; 176 example = "udp"; 177 description = "Protocol of forwarded connection"; 178 }; 179 180 loopbackIPs = mkOption { 181 type = types.listOf types.str; 182 default = []; 183 example = literalExample ''[ "55.1.2.3" ]''; 184 description = "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT"; 185 }; 186 }; 187 }); 188 default = []; 189 example = [ { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; } ]; 190 description = 191 '' 192 List of forwarded ports from the external interface to 193 internal destinations by using DNAT. 194 ''; 195 }; 196 197 networking.nat.dmzHost = mkOption { 198 type = types.nullOr types.str; 199 default = null; 200 example = "10.0.0.1"; 201 description = 202 '' 203 The local IP address to which all traffic that does not match any 204 forwarding rule is forwarded. 205 ''; 206 }; 207 208 networking.nat.extraCommands = mkOption { 209 type = types.lines; 210 default = ""; 211 example = "iptables -A INPUT -p icmp -j ACCEPT"; 212 description = 213 '' 214 Additional shell commands executed as part of the nat 215 initialisation script. 216 ''; 217 }; 218 219 networking.nat.extraStopCommands = mkOption { 220 type = types.lines; 221 default = ""; 222 example = "iptables -D INPUT -p icmp -j ACCEPT || true"; 223 description = 224 '' 225 Additional shell commands executed as part of the nat 226 teardown script. 227 ''; 228 }; 229 230 }; 231 232 233 ###### implementation 234 235 config = mkMerge [ 236 { networking.firewall.extraCommands = mkBefore flushNat; } 237 (mkIf config.networking.nat.enable { 238 239 environment.systemPackages = [ pkgs.iptables ]; 240 241 boot = { 242 kernelModules = [ "nf_nat_ftp" ]; 243 kernel.sysctl = { 244 "net.ipv4.conf.all.forwarding" = mkOverride 99 true; 245 "net.ipv4.conf.default.forwarding" = mkOverride 99 true; 246 }; 247 }; 248 249 networking.firewall = mkIf config.networking.firewall.enable { 250 extraCommands = setupNat; 251 extraStopCommands = flushNat; 252 }; 253 254 systemd.services = mkIf (!config.networking.firewall.enable) { nat = { 255 description = "Network Address Translation"; 256 wantedBy = [ "network.target" ]; 257 after = [ "network-pre.target" "systemd-modules-load.service" ]; 258 path = [ pkgs.iptables ]; 259 unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 260 261 serviceConfig = { 262 Type = "oneshot"; 263 RemainAfterExit = true; 264 }; 265 266 script = flushNat + setupNat; 267 268 postStop = flushNat; 269 }; }; 270 }) 271 ]; 272}