mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-12 12:45:27 +03:00
nixos/{firewall, nat}: add a nftables based implementation
This commit is contained in:
parent
2379de680d
commit
a43c7b2a70
15 changed files with 1158 additions and 723 deletions
|
@ -303,6 +303,13 @@
|
||||||
the Nix store.
|
the Nix store.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
The <literal>firewall</literal> and <literal>nat</literal>
|
||||||
|
module now has a nftables based implementation. Enable
|
||||||
|
<literal>networking.nftables</literal> to use it.
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
The <literal>services.fwupd</literal> module now allows
|
The <literal>services.fwupd</literal> module now allows
|
||||||
|
|
|
@ -86,6 +86,8 @@ In addition to numerous new and upgraded packages, this release has the followin
|
||||||
|
|
||||||
- Resilio sync secret keys can now be provided using a secrets file at runtime, preventing these secrets from ending up in the Nix store.
|
- Resilio sync secret keys can now be provided using a secrets file at runtime, preventing these secrets from ending up in the Nix store.
|
||||||
|
|
||||||
|
- The `firewall` and `nat` module now has a nftables based implementation. Enable `networking.nftables` to use it.
|
||||||
|
|
||||||
- The `services.fwupd` module now allows arbitrary daemon settings to be configured in a structured manner ([`services.fwupd.daemonSettings`](#opt-services.fwupd.daemonSettings)).
|
- The `services.fwupd` module now allows arbitrary daemon settings to be configured in a structured manner ([`services.fwupd.daemonSettings`](#opt-services.fwupd.daemonSettings)).
|
||||||
|
|
||||||
- The `unifi-poller` package and corresponding NixOS module have been renamed to `unpoller` to match upstream.
|
- The `unifi-poller` package and corresponding NixOS module have been renamed to `unpoller` to match upstream.
|
||||||
|
|
|
@ -821,6 +821,8 @@
|
||||||
./services/networking/firefox-syncserver.nix
|
./services/networking/firefox-syncserver.nix
|
||||||
./services/networking/fireqos.nix
|
./services/networking/fireqos.nix
|
||||||
./services/networking/firewall.nix
|
./services/networking/firewall.nix
|
||||||
|
./services/networking/firewall-iptables.nix
|
||||||
|
./services/networking/firewall-nftables.nix
|
||||||
./services/networking/flannel.nix
|
./services/networking/flannel.nix
|
||||||
./services/networking/freenet.nix
|
./services/networking/freenet.nix
|
||||||
./services/networking/freeradius.nix
|
./services/networking/freeradius.nix
|
||||||
|
@ -891,6 +893,8 @@
|
||||||
./services/networking/namecoind.nix
|
./services/networking/namecoind.nix
|
||||||
./services/networking/nar-serve.nix
|
./services/networking/nar-serve.nix
|
||||||
./services/networking/nat.nix
|
./services/networking/nat.nix
|
||||||
|
./services/networking/nat-iptables.nix
|
||||||
|
./services/networking/nat-nftables.nix
|
||||||
./services/networking/nats.nix
|
./services/networking/nats.nix
|
||||||
./services/networking/nbd.nix
|
./services/networking/nbd.nix
|
||||||
./services/networking/ncdns.nix
|
./services/networking/ncdns.nix
|
||||||
|
|
|
@ -53,13 +53,18 @@ in {
|
||||||
networking.firewall = mkIf cfg.openFirewall {
|
networking.firewall = mkIf cfg.openFirewall {
|
||||||
allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
|
allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
|
||||||
allowedUDPPorts = [ 9003 ];
|
allowedUDPPorts = [ 9003 ];
|
||||||
extraCommands = ''
|
extraCommands = optionalString (!config.networking.nftables.enable) ''
|
||||||
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
|
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
|
||||||
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
|
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
|
||||||
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
|
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
|
||||||
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
|
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
|
||||||
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
|
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
|
||||||
'';
|
'';
|
||||||
|
extraInputRules = optionalString config.networking.nftables.enable ''
|
||||||
|
ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept
|
||||||
|
ip daddr 224.0.0.0/4 accept
|
||||||
|
pkttype { multicast, broadcast } accept
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ in {
|
||||||
{ from = 30000; to = 30010; }
|
{ from = 30000; to = 30010; }
|
||||||
];
|
];
|
||||||
allowedUDPPorts = [ 9003 ];
|
allowedUDPPorts = [ 9003 ];
|
||||||
extraCommands = ''
|
extraCommands = optionalString (!config.networking.nftables.enable) ''
|
||||||
## IGMP / Broadcast ##
|
## IGMP / Broadcast ##
|
||||||
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
|
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
|
||||||
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
|
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
|
||||||
|
@ -66,6 +66,11 @@ in {
|
||||||
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
|
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
|
||||||
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
|
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
|
||||||
'';
|
'';
|
||||||
|
extraInputRules = optionalString config.networking.nftables.enable ''
|
||||||
|
ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept
|
||||||
|
ip daddr 224.0.0.0/4 accept
|
||||||
|
pkttype { multicast, broadcast } accept
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
334
nixos/modules/services/networking/firewall-iptables.nix
Normal file
334
nixos/modules/services/networking/firewall-iptables.nix
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
/* This module enables a simple firewall.
|
||||||
|
|
||||||
|
The firewall can be customised in arbitrary ways by setting
|
||||||
|
‘networking.firewall.extraCommands’. For modularity, the firewall
|
||||||
|
uses several chains:
|
||||||
|
|
||||||
|
- ‘nixos-fw’ is the main chain for input packet processing.
|
||||||
|
|
||||||
|
- ‘nixos-fw-accept’ is called for accepted packets. If you want
|
||||||
|
additional logging, or want to reject certain packets anyway, you
|
||||||
|
can insert rules at the start of this chain.
|
||||||
|
|
||||||
|
- ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for
|
||||||
|
refused packets. (The former jumps to the latter after logging
|
||||||
|
the packet.) If you want additional logging, or want to accept
|
||||||
|
certain packets anyway, you can insert rules at the start of
|
||||||
|
this chain.
|
||||||
|
|
||||||
|
- ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table,
|
||||||
|
called from the built-in ‘PREROUTING’ chain. If the kernel
|
||||||
|
supports it and `cfg.checkReversePath` is set this chain will
|
||||||
|
perform a reverse path filter test.
|
||||||
|
|
||||||
|
- ‘nixos-drop’ is used while reloading the firewall in order to drop
|
||||||
|
all traffic. Since reloading isn't implemented in an atomic way
|
||||||
|
this'll prevent any traffic from leaking through while reloading
|
||||||
|
the firewall. However, if the reloading fails, the ‘firewall-stop’
|
||||||
|
script will be called which in return will effectively disable the
|
||||||
|
complete firewall (in the default configuration).
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
cfg = config.networking.firewall;
|
||||||
|
|
||||||
|
inherit (config.boot.kernelPackages) kernel;
|
||||||
|
|
||||||
|
kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
|
||||||
|
|
||||||
|
helpers = import ./helpers.nix { inherit config lib; };
|
||||||
|
|
||||||
|
writeShScript = name: text:
|
||||||
|
let
|
||||||
|
dir = pkgs.writeScriptBin name ''
|
||||||
|
#! ${pkgs.runtimeShell} -e
|
||||||
|
${text}
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
"${dir}/bin/${name}";
|
||||||
|
|
||||||
|
startScript = writeShScript "firewall-start" ''
|
||||||
|
${helpers}
|
||||||
|
|
||||||
|
# Flush the old firewall rules. !!! Ideally, updating the
|
||||||
|
# firewall would be atomic. Apparently that's possible
|
||||||
|
# with iptables-restore.
|
||||||
|
ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
|
||||||
|
for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
|
||||||
|
ip46tables -F "$chain" 2> /dev/null || true
|
||||||
|
ip46tables -X "$chain" 2> /dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# The "nixos-fw-accept" chain just accepts packets.
|
||||||
|
ip46tables -N nixos-fw-accept
|
||||||
|
ip46tables -A nixos-fw-accept -j ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
# The "nixos-fw-refuse" chain rejects or drops packets.
|
||||||
|
ip46tables -N nixos-fw-refuse
|
||||||
|
|
||||||
|
${if cfg.rejectPackets then ''
|
||||||
|
# Send a reset for existing TCP connections that we've
|
||||||
|
# somehow forgotten about. Send ICMP "port unreachable"
|
||||||
|
# for everything else.
|
||||||
|
ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
|
||||||
|
ip46tables -A nixos-fw-refuse -j REJECT
|
||||||
|
'' else ''
|
||||||
|
ip46tables -A nixos-fw-refuse -j DROP
|
||||||
|
''}
|
||||||
|
|
||||||
|
|
||||||
|
# The "nixos-fw-log-refuse" chain performs logging, then
|
||||||
|
# jumps to the "nixos-fw-refuse" chain.
|
||||||
|
ip46tables -N nixos-fw-log-refuse
|
||||||
|
|
||||||
|
${optionalString cfg.logRefusedConnections ''
|
||||||
|
ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
|
||||||
|
''}
|
||||||
|
${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
|
||||||
|
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
|
||||||
|
-j LOG --log-level info --log-prefix "refused broadcast: "
|
||||||
|
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
|
||||||
|
-j LOG --log-level info --log-prefix "refused multicast: "
|
||||||
|
''}
|
||||||
|
ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
|
||||||
|
${optionalString cfg.logRefusedPackets ''
|
||||||
|
ip46tables -A nixos-fw-log-refuse \
|
||||||
|
-j LOG --log-level info --log-prefix "refused packet: "
|
||||||
|
''}
|
||||||
|
ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
|
||||||
|
|
||||||
|
|
||||||
|
# The "nixos-fw" chain does the actual work.
|
||||||
|
ip46tables -N nixos-fw
|
||||||
|
|
||||||
|
# Clean up rpfilter rules
|
||||||
|
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
|
||||||
|
ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
|
||||||
|
ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
|
||||||
|
|
||||||
|
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
|
||||||
|
# Perform a reverse-path test to refuse spoofers
|
||||||
|
# For now, we just drop, as the mangle table doesn't have a log-refuse yet
|
||||||
|
ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
|
||||||
|
ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN
|
||||||
|
|
||||||
|
# Allows this host to act as a DHCP4 client without first having to use APIPA
|
||||||
|
iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
|
||||||
|
|
||||||
|
# Allows this host to act as a DHCPv4 server
|
||||||
|
iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
|
||||||
|
|
||||||
|
${optionalString cfg.logReversePathDrops ''
|
||||||
|
ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
|
||||||
|
''}
|
||||||
|
ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
|
||||||
|
|
||||||
|
ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
|
||||||
|
''}
|
||||||
|
|
||||||
|
# Accept all traffic on the trusted interfaces.
|
||||||
|
${flip concatMapStrings cfg.trustedInterfaces (iface: ''
|
||||||
|
ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
|
||||||
|
'')}
|
||||||
|
|
||||||
|
# Accept packets from established or related connections.
|
||||||
|
ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
|
||||||
|
|
||||||
|
# Accept connections to the allowed TCP ports.
|
||||||
|
${concatStrings (mapAttrsToList (iface: cfg:
|
||||||
|
concatMapStrings (port:
|
||||||
|
''
|
||||||
|
ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
||||||
|
''
|
||||||
|
) cfg.allowedTCPPorts
|
||||||
|
) cfg.allInterfaces)}
|
||||||
|
|
||||||
|
# Accept connections to the allowed TCP port ranges.
|
||||||
|
${concatStrings (mapAttrsToList (iface: cfg:
|
||||||
|
concatMapStrings (rangeAttr:
|
||||||
|
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
|
||||||
|
''
|
||||||
|
ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
||||||
|
''
|
||||||
|
) cfg.allowedTCPPortRanges
|
||||||
|
) cfg.allInterfaces)}
|
||||||
|
|
||||||
|
# Accept packets on the allowed UDP ports.
|
||||||
|
${concatStrings (mapAttrsToList (iface: cfg:
|
||||||
|
concatMapStrings (port:
|
||||||
|
''
|
||||||
|
ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
||||||
|
''
|
||||||
|
) cfg.allowedUDPPorts
|
||||||
|
) cfg.allInterfaces)}
|
||||||
|
|
||||||
|
# Accept packets on the allowed UDP port ranges.
|
||||||
|
${concatStrings (mapAttrsToList (iface: cfg:
|
||||||
|
concatMapStrings (rangeAttr:
|
||||||
|
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
|
||||||
|
''
|
||||||
|
ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
||||||
|
''
|
||||||
|
) cfg.allowedUDPPortRanges
|
||||||
|
) cfg.allInterfaces)}
|
||||||
|
|
||||||
|
# Optionally respond to ICMPv4 pings.
|
||||||
|
${optionalString cfg.allowPing ''
|
||||||
|
iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
|
||||||
|
"-m limit ${cfg.pingLimit} "
|
||||||
|
}-j nixos-fw-accept
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString config.networking.enableIPv6 ''
|
||||||
|
# Accept all ICMPv6 messages except redirects and node
|
||||||
|
# information queries (type 139). See RFC 4890, section
|
||||||
|
# 4.4.
|
||||||
|
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
|
||||||
|
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
|
||||||
|
ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
|
||||||
|
|
||||||
|
# Allow this host to act as a DHCPv6 client
|
||||||
|
ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
|
||||||
|
''}
|
||||||
|
|
||||||
|
${cfg.extraCommands}
|
||||||
|
|
||||||
|
# Reject/drop everything else.
|
||||||
|
ip46tables -A nixos-fw -j nixos-fw-log-refuse
|
||||||
|
|
||||||
|
|
||||||
|
# Enable the firewall.
|
||||||
|
ip46tables -A INPUT -j nixos-fw
|
||||||
|
'';
|
||||||
|
|
||||||
|
stopScript = writeShScript "firewall-stop" ''
|
||||||
|
${helpers}
|
||||||
|
|
||||||
|
# Clean up in case reload fails
|
||||||
|
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
|
||||||
|
|
||||||
|
# Clean up after added ruleset
|
||||||
|
ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
|
||||||
|
|
||||||
|
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
|
||||||
|
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
|
||||||
|
''}
|
||||||
|
|
||||||
|
${cfg.extraStopCommands}
|
||||||
|
'';
|
||||||
|
|
||||||
|
reloadScript = writeShScript "firewall-reload" ''
|
||||||
|
${helpers}
|
||||||
|
|
||||||
|
# Create a unique drop rule
|
||||||
|
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
|
||||||
|
ip46tables -F nixos-drop 2>/dev/null || true
|
||||||
|
ip46tables -X nixos-drop 2>/dev/null || true
|
||||||
|
ip46tables -N nixos-drop
|
||||||
|
ip46tables -A nixos-drop -j DROP
|
||||||
|
|
||||||
|
# Don't allow traffic to leak out until the script has completed
|
||||||
|
ip46tables -A INPUT -j nixos-drop
|
||||||
|
|
||||||
|
${cfg.extraStopCommands}
|
||||||
|
|
||||||
|
if ${startScript}; then
|
||||||
|
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "Failed to reload firewall... Stopping"
|
||||||
|
${stopScript}
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
extraCommands = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = "iptables -A INPUT -p icmp -j ACCEPT";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Additional shell commands executed as part of the firewall
|
||||||
|
initialisation script. These are executed just before the
|
||||||
|
final "reject" firewall rule is added, so they can be used
|
||||||
|
to allow packets that would otherwise be refused.
|
||||||
|
|
||||||
|
This option only works with the iptables based firewall.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraStopCommands = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = "iptables -P INPUT ACCEPT";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Additional shell commands executed as part of the firewall
|
||||||
|
shutdown script. These are executed just after the removal
|
||||||
|
of the NixOS input rule, or if the service enters a failed
|
||||||
|
state.
|
||||||
|
|
||||||
|
This option only works with the iptables based firewall.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
# FIXME: Maybe if `enable' is false, the firewall should still be
|
||||||
|
# built but not started by default?
|
||||||
|
config = mkIf (cfg.enable && config.networking.nftables.enable == false) {
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
# This is approximately "checkReversePath -> kernelHasRPFilter",
|
||||||
|
# but the checkReversePath option can include non-boolean
|
||||||
|
# values.
|
||||||
|
{
|
||||||
|
assertion = cfg.checkReversePath == false || kernelHasRPFilter;
|
||||||
|
message = "This kernel does not support rpfilter";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.firewall.checkReversePath = mkIf (!kernelHasRPFilter) (mkDefault false);
|
||||||
|
|
||||||
|
systemd.services.firewall = {
|
||||||
|
description = "Firewall";
|
||||||
|
wantedBy = [ "sysinit.target" ];
|
||||||
|
wants = [ "network-pre.target" ];
|
||||||
|
before = [ "network-pre.target" ];
|
||||||
|
after = [ "systemd-modules-load.service" ];
|
||||||
|
|
||||||
|
path = [ cfg.package ] ++ cfg.extraPackages;
|
||||||
|
|
||||||
|
# FIXME: this module may also try to load kernel modules, but
|
||||||
|
# containers don't have CAP_SYS_MODULE. So the host system had
|
||||||
|
# better have all necessary modules already loaded.
|
||||||
|
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
|
||||||
|
unitConfig.DefaultDependencies = false;
|
||||||
|
|
||||||
|
reloadIfChanged = true;
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
ExecStart = "@${startScript} firewall-start";
|
||||||
|
ExecReload = "@${reloadScript} firewall-reload";
|
||||||
|
ExecStop = "@${stopScript} firewall-stop";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
167
nixos/modules/services/networking/firewall-nftables.nix
Normal file
167
nixos/modules/services/networking/firewall-nftables.nix
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
cfg = config.networking.firewall;
|
||||||
|
|
||||||
|
ifaceSet = concatStringsSep ", " (
|
||||||
|
map (x: ''"${x}"'') cfg.trustedInterfaces
|
||||||
|
);
|
||||||
|
|
||||||
|
portsToNftSet = ports: portRanges: concatStringsSep ", " (
|
||||||
|
map (x: toString x) ports
|
||||||
|
++ map (x: "${toString x.from}-${toString x.to}") portRanges
|
||||||
|
);
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
extraInputRules = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = "ip6 saddr { fc00::/7, fe80::/10 } tcp dport 24800 accept";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Additional nftables rules to be appended to the input-allow
|
||||||
|
chain.
|
||||||
|
|
||||||
|
This option only works with the nftables based firewall.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraForwardRules = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = "iifname wg0 accept";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Additional nftables rules to be appended to the forward-allow
|
||||||
|
chain.
|
||||||
|
|
||||||
|
This option only works with the nftables based firewall.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf (cfg.enable && config.networking.nftables.enable) {
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.extraCommands == "";
|
||||||
|
message = "extraCommands is incompatible with the nftables based firewall: ${cfg.extraCommands}";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.extraStopCommands == "";
|
||||||
|
message = "extraStopCommands is incompatible with the nftables based firewall: ${cfg.extraStopCommands}";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.pingLimit == null || !(hasPrefix "--" cfg.pingLimit);
|
||||||
|
message = "nftables syntax like \"2/second\" should be used in networking.firewall.pingLimit";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = config.networking.nftables.rulesetFile == null;
|
||||||
|
message = "networking.nftables.rulesetFile conflicts with the firewall";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.nftables.ruleset = ''
|
||||||
|
|
||||||
|
table inet nixos-fw {
|
||||||
|
|
||||||
|
${optionalString (cfg.checkReversePath != false) ''
|
||||||
|
chain rpfilter {
|
||||||
|
type filter hook prerouting priority mangle + 10; policy drop;
|
||||||
|
|
||||||
|
meta nfproto ipv4 udp sport . udp dport { 67 . 68, 68 . 67 } accept comment "DHCPv4 client/server"
|
||||||
|
fib saddr . mark ${optionalString (cfg.checkReversePath != "loose") ". iif"} oif exists accept
|
||||||
|
|
||||||
|
${optionalString cfg.logReversePathDrops ''
|
||||||
|
log level info prefix "rpfilter drop: "
|
||||||
|
''}
|
||||||
|
|
||||||
|
}
|
||||||
|
''}
|
||||||
|
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority filter; policy drop;
|
||||||
|
|
||||||
|
${optionalString (ifaceSet != "") ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''}
|
||||||
|
|
||||||
|
# Some ICMPv6 types like NDP is untracked
|
||||||
|
ct state vmap { invalid : drop, established : accept, related : accept, * : jump input-allow } comment "*: new and untracked"
|
||||||
|
|
||||||
|
${optionalString cfg.logRefusedConnections ''
|
||||||
|
tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: "
|
||||||
|
''}
|
||||||
|
${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
|
||||||
|
pkttype broadcast log level info prefix "refused broadcast: "
|
||||||
|
pkttype multicast log level info prefix "refused multicast: "
|
||||||
|
''}
|
||||||
|
${optionalString cfg.logRefusedPackets ''
|
||||||
|
pkttype host log level info prefix "refused packet: "
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString cfg.rejectPackets ''
|
||||||
|
meta l4proto tcp reject with tcp reset
|
||||||
|
reject
|
||||||
|
''}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
chain input-allow {
|
||||||
|
|
||||||
|
${concatStrings (mapAttrsToList (iface: cfg:
|
||||||
|
let
|
||||||
|
ifaceExpr = optionalString (iface != "default") "iifname ${iface}";
|
||||||
|
tcpSet = portsToNftSet cfg.allowedTCPPorts cfg.allowedTCPPortRanges;
|
||||||
|
udpSet = portsToNftSet cfg.allowedUDPPorts cfg.allowedUDPPortRanges;
|
||||||
|
in
|
||||||
|
''
|
||||||
|
${optionalString (tcpSet != "") "${ifaceExpr} tcp dport { ${tcpSet} } accept"}
|
||||||
|
${optionalString (udpSet != "") "${ifaceExpr} udp dport { ${udpSet} } accept"}
|
||||||
|
''
|
||||||
|
) cfg.allInterfaces)}
|
||||||
|
|
||||||
|
${optionalString cfg.allowPing ''
|
||||||
|
icmp type echo-request ${optionalString (cfg.pingLimit != null) "limit rate ${cfg.pingLimit}"} accept comment "allow ping"
|
||||||
|
''}
|
||||||
|
|
||||||
|
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."
|
||||||
|
ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client"
|
||||||
|
|
||||||
|
${cfg.extraInputRules}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
${optionalString cfg.filterForward ''
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority filter; policy drop;
|
||||||
|
|
||||||
|
ct state vmap { invalid : drop, established : accept, related : accept, * : jump forward-allow } comment "*: new and untracked"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
chain forward-allow {
|
||||||
|
|
||||||
|
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."
|
||||||
|
|
||||||
|
ct status dnat accept comment "allow port forward"
|
||||||
|
|
||||||
|
${cfg.extraForwardRules}
|
||||||
|
|
||||||
|
}
|
||||||
|
''}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
'';
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -1,35 +1,3 @@
|
||||||
/* This module enables a simple firewall.
|
|
||||||
|
|
||||||
The firewall can be customised in arbitrary ways by setting
|
|
||||||
‘networking.firewall.extraCommands’. For modularity, the firewall
|
|
||||||
uses several chains:
|
|
||||||
|
|
||||||
- ‘nixos-fw’ is the main chain for input packet processing.
|
|
||||||
|
|
||||||
- ‘nixos-fw-accept’ is called for accepted packets. If you want
|
|
||||||
additional logging, or want to reject certain packets anyway, you
|
|
||||||
can insert rules at the start of this chain.
|
|
||||||
|
|
||||||
- ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for
|
|
||||||
refused packets. (The former jumps to the latter after logging
|
|
||||||
the packet.) If you want additional logging, or want to accept
|
|
||||||
certain packets anyway, you can insert rules at the start of
|
|
||||||
this chain.
|
|
||||||
|
|
||||||
- ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table,
|
|
||||||
called from the built-in ‘PREROUTING’ chain. If the kernel
|
|
||||||
supports it and `cfg.checkReversePath` is set this chain will
|
|
||||||
perform a reverse path filter test.
|
|
||||||
|
|
||||||
- ‘nixos-drop’ is used while reloading the firewall in order to drop
|
|
||||||
all traffic. Since reloading isn't implemented in an atomic way
|
|
||||||
this'll prevent any traffic from leaking through while reloading
|
|
||||||
the firewall. However, if the reloading fails, the ‘firewall-stop’
|
|
||||||
script will be called which in return will effectively disable the
|
|
||||||
complete firewall (in the default configuration).
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
@ -38,216 +6,6 @@ let
|
||||||
|
|
||||||
cfg = config.networking.firewall;
|
cfg = config.networking.firewall;
|
||||||
|
|
||||||
inherit (config.boot.kernelPackages) kernel;
|
|
||||||
|
|
||||||
kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
|
|
||||||
|
|
||||||
helpers = import ./helpers.nix { inherit config lib; };
|
|
||||||
|
|
||||||
writeShScript = name: text: let dir = pkgs.writeScriptBin name ''
|
|
||||||
#! ${pkgs.runtimeShell} -e
|
|
||||||
${text}
|
|
||||||
''; in "${dir}/bin/${name}";
|
|
||||||
|
|
||||||
defaultInterface = { default = mapAttrs (name: value: cfg.${name}) commonOptions; };
|
|
||||||
allInterfaces = defaultInterface // cfg.interfaces;
|
|
||||||
|
|
||||||
startScript = writeShScript "firewall-start" ''
|
|
||||||
${helpers}
|
|
||||||
|
|
||||||
# Flush the old firewall rules. !!! Ideally, updating the
|
|
||||||
# firewall would be atomic. Apparently that's possible
|
|
||||||
# with iptables-restore.
|
|
||||||
ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
|
|
||||||
for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
|
|
||||||
ip46tables -F "$chain" 2> /dev/null || true
|
|
||||||
ip46tables -X "$chain" 2> /dev/null || true
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
# The "nixos-fw-accept" chain just accepts packets.
|
|
||||||
ip46tables -N nixos-fw-accept
|
|
||||||
ip46tables -A nixos-fw-accept -j ACCEPT
|
|
||||||
|
|
||||||
|
|
||||||
# The "nixos-fw-refuse" chain rejects or drops packets.
|
|
||||||
ip46tables -N nixos-fw-refuse
|
|
||||||
|
|
||||||
${if cfg.rejectPackets then ''
|
|
||||||
# Send a reset for existing TCP connections that we've
|
|
||||||
# somehow forgotten about. Send ICMP "port unreachable"
|
|
||||||
# for everything else.
|
|
||||||
ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
|
|
||||||
ip46tables -A nixos-fw-refuse -j REJECT
|
|
||||||
'' else ''
|
|
||||||
ip46tables -A nixos-fw-refuse -j DROP
|
|
||||||
''}
|
|
||||||
|
|
||||||
|
|
||||||
# The "nixos-fw-log-refuse" chain performs logging, then
|
|
||||||
# jumps to the "nixos-fw-refuse" chain.
|
|
||||||
ip46tables -N nixos-fw-log-refuse
|
|
||||||
|
|
||||||
${optionalString cfg.logRefusedConnections ''
|
|
||||||
ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
|
|
||||||
''}
|
|
||||||
${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
|
|
||||||
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
|
|
||||||
-j LOG --log-level info --log-prefix "refused broadcast: "
|
|
||||||
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
|
|
||||||
-j LOG --log-level info --log-prefix "refused multicast: "
|
|
||||||
''}
|
|
||||||
ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
|
|
||||||
${optionalString cfg.logRefusedPackets ''
|
|
||||||
ip46tables -A nixos-fw-log-refuse \
|
|
||||||
-j LOG --log-level info --log-prefix "refused packet: "
|
|
||||||
''}
|
|
||||||
ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
|
|
||||||
|
|
||||||
|
|
||||||
# The "nixos-fw" chain does the actual work.
|
|
||||||
ip46tables -N nixos-fw
|
|
||||||
|
|
||||||
# Clean up rpfilter rules
|
|
||||||
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
|
|
||||||
ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
|
|
||||||
ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
|
|
||||||
|
|
||||||
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
|
|
||||||
# Perform a reverse-path test to refuse spoofers
|
|
||||||
# For now, we just drop, as the mangle table doesn't have a log-refuse yet
|
|
||||||
ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
|
|
||||||
ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN
|
|
||||||
|
|
||||||
# Allows this host to act as a DHCP4 client without first having to use APIPA
|
|
||||||
iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
|
|
||||||
|
|
||||||
# Allows this host to act as a DHCPv4 server
|
|
||||||
iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
|
|
||||||
|
|
||||||
${optionalString cfg.logReversePathDrops ''
|
|
||||||
ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
|
|
||||||
''}
|
|
||||||
ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
|
|
||||||
|
|
||||||
ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
|
|
||||||
''}
|
|
||||||
|
|
||||||
# Accept all traffic on the trusted interfaces.
|
|
||||||
${flip concatMapStrings cfg.trustedInterfaces (iface: ''
|
|
||||||
ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
|
|
||||||
'')}
|
|
||||||
|
|
||||||
# Accept packets from established or related connections.
|
|
||||||
ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
|
|
||||||
|
|
||||||
# Accept connections to the allowed TCP ports.
|
|
||||||
${concatStrings (mapAttrsToList (iface: cfg:
|
|
||||||
concatMapStrings (port:
|
|
||||||
''
|
|
||||||
ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
|
||||||
''
|
|
||||||
) cfg.allowedTCPPorts
|
|
||||||
) allInterfaces)}
|
|
||||||
|
|
||||||
# Accept connections to the allowed TCP port ranges.
|
|
||||||
${concatStrings (mapAttrsToList (iface: cfg:
|
|
||||||
concatMapStrings (rangeAttr:
|
|
||||||
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
|
|
||||||
''
|
|
||||||
ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
|
||||||
''
|
|
||||||
) cfg.allowedTCPPortRanges
|
|
||||||
) allInterfaces)}
|
|
||||||
|
|
||||||
# Accept packets on the allowed UDP ports.
|
|
||||||
${concatStrings (mapAttrsToList (iface: cfg:
|
|
||||||
concatMapStrings (port:
|
|
||||||
''
|
|
||||||
ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
|
||||||
''
|
|
||||||
) cfg.allowedUDPPorts
|
|
||||||
) allInterfaces)}
|
|
||||||
|
|
||||||
# Accept packets on the allowed UDP port ranges.
|
|
||||||
${concatStrings (mapAttrsToList (iface: cfg:
|
|
||||||
concatMapStrings (rangeAttr:
|
|
||||||
let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
|
|
||||||
''
|
|
||||||
ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
|
|
||||||
''
|
|
||||||
) cfg.allowedUDPPortRanges
|
|
||||||
) allInterfaces)}
|
|
||||||
|
|
||||||
# Optionally respond to ICMPv4 pings.
|
|
||||||
${optionalString cfg.allowPing ''
|
|
||||||
iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
|
|
||||||
"-m limit ${cfg.pingLimit} "
|
|
||||||
}-j nixos-fw-accept
|
|
||||||
''}
|
|
||||||
|
|
||||||
${optionalString config.networking.enableIPv6 ''
|
|
||||||
# Accept all ICMPv6 messages except redirects and node
|
|
||||||
# information queries (type 139). See RFC 4890, section
|
|
||||||
# 4.4.
|
|
||||||
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
|
|
||||||
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
|
|
||||||
ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
|
|
||||||
|
|
||||||
# Allow this host to act as a DHCPv6 client
|
|
||||||
ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
|
|
||||||
''}
|
|
||||||
|
|
||||||
${cfg.extraCommands}
|
|
||||||
|
|
||||||
# Reject/drop everything else.
|
|
||||||
ip46tables -A nixos-fw -j nixos-fw-log-refuse
|
|
||||||
|
|
||||||
|
|
||||||
# Enable the firewall.
|
|
||||||
ip46tables -A INPUT -j nixos-fw
|
|
||||||
'';
|
|
||||||
|
|
||||||
stopScript = writeShScript "firewall-stop" ''
|
|
||||||
${helpers}
|
|
||||||
|
|
||||||
# Clean up in case reload fails
|
|
||||||
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
|
|
||||||
|
|
||||||
# Clean up after added ruleset
|
|
||||||
ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
|
|
||||||
|
|
||||||
${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
|
|
||||||
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
|
|
||||||
''}
|
|
||||||
|
|
||||||
${cfg.extraStopCommands}
|
|
||||||
'';
|
|
||||||
|
|
||||||
reloadScript = writeShScript "firewall-reload" ''
|
|
||||||
${helpers}
|
|
||||||
|
|
||||||
# Create a unique drop rule
|
|
||||||
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
|
|
||||||
ip46tables -F nixos-drop 2>/dev/null || true
|
|
||||||
ip46tables -X nixos-drop 2>/dev/null || true
|
|
||||||
ip46tables -N nixos-drop
|
|
||||||
ip46tables -A nixos-drop -j DROP
|
|
||||||
|
|
||||||
# Don't allow traffic to leak out until the script has completed
|
|
||||||
ip46tables -A INPUT -j nixos-drop
|
|
||||||
|
|
||||||
${cfg.extraStopCommands}
|
|
||||||
|
|
||||||
if ${startScript}; then
|
|
||||||
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
|
|
||||||
else
|
|
||||||
echo "Failed to reload firewall... Stopping"
|
|
||||||
${stopScript}
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
canonicalizePortList =
|
canonicalizePortList =
|
||||||
ports: lib.unique (builtins.sort builtins.lessThan ports);
|
ports: lib.unique (builtins.sort builtins.lessThan ports);
|
||||||
|
|
||||||
|
@ -257,22 +15,20 @@ let
|
||||||
default = [ ];
|
default = [ ];
|
||||||
apply = canonicalizePortList;
|
apply = canonicalizePortList;
|
||||||
example = [ 22 80 ];
|
example = [ 22 80 ];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
List of TCP ports on which incoming connections are
|
||||||
List of TCP ports on which incoming connections are
|
accepted.
|
||||||
accepted.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
allowedTCPPortRanges = mkOption {
|
allowedTCPPortRanges = mkOption {
|
||||||
type = types.listOf (types.attrsOf types.port);
|
type = types.listOf (types.attrsOf types.port);
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ { from = 8999; to = 9003; } ];
|
example = [{ from = 8999; to = 9003; }];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
A range of TCP ports on which incoming connections are
|
||||||
A range of TCP ports on which incoming connections are
|
accepted.
|
||||||
accepted.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
allowedUDPPorts = mkOption {
|
allowedUDPPorts = mkOption {
|
||||||
|
@ -280,20 +36,18 @@ let
|
||||||
default = [ ];
|
default = [ ];
|
||||||
apply = canonicalizePortList;
|
apply = canonicalizePortList;
|
||||||
example = [ 53 ];
|
example = [ 53 ];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
List of open UDP ports.
|
||||||
List of open UDP ports.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
allowedUDPPortRanges = mkOption {
|
allowedUDPPortRanges = mkOption {
|
||||||
type = types.listOf (types.attrsOf types.port);
|
type = types.listOf (types.attrsOf types.port);
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ { from = 60000; to = 61000; } ];
|
example = [{ from = 60000; to = 61000; }];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Range of open UDP ports.
|
||||||
Range of open UDP ports.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -301,240 +55,222 @@ in
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
###### interface
|
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
enable = mkOption {
|
enable = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Whether to enable the firewall. This is a simple stateful
|
||||||
Whether to enable the firewall. This is a simple stateful
|
firewall that blocks connection attempts to unauthorised TCP
|
||||||
firewall that blocks connection attempts to unauthorised TCP
|
or UDP ports on this machine.
|
||||||
or UDP ports on this machine. It does not affect packet
|
'';
|
||||||
forwarding.
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
package = mkOption {
|
package = mkOption {
|
||||||
type = types.package;
|
type = types.package;
|
||||||
default = pkgs.iptables;
|
default = if config.networking.nftables.enable then pkgs.nftables else pkgs.iptables;
|
||||||
defaultText = literalExpression "pkgs.iptables";
|
defaultText = literalExpression ''if config.networking.nftables.enable then "pkgs.nftables" else "pkgs.iptables"'';
|
||||||
example = literalExpression "pkgs.iptables-legacy";
|
example = literalExpression "pkgs.iptables-legacy";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The package to use for running the firewall service.
|
||||||
The iptables package to use for running the firewall service.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logRefusedConnections = mkOption {
|
logRefusedConnections = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Whether to log rejected or dropped incoming connections.
|
||||||
Whether to log rejected or dropped incoming connections.
|
Note: The logs are found in the kernel logs, i.e. dmesg
|
||||||
Note: The logs are found in the kernel logs, i.e. dmesg
|
or journalctl -k.
|
||||||
or journalctl -k.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logRefusedPackets = mkOption {
|
logRefusedPackets = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Whether to log all rejected or dropped incoming packets.
|
||||||
Whether to log all rejected or dropped incoming packets.
|
This tends to give a lot of log messages, so it's mostly
|
||||||
This tends to give a lot of log messages, so it's mostly
|
useful for debugging.
|
||||||
useful for debugging.
|
Note: The logs are found in the kernel logs, i.e. dmesg
|
||||||
Note: The logs are found in the kernel logs, i.e. dmesg
|
or journalctl -k.
|
||||||
or journalctl -k.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logRefusedUnicastsOnly = mkOption {
|
logRefusedUnicastsOnly = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
If {option}`networking.firewall.logRefusedPackets`
|
||||||
If {option}`networking.firewall.logRefusedPackets`
|
and this option are enabled, then only log packets
|
||||||
and this option are enabled, then only log packets
|
specifically directed at this machine, i.e., not broadcasts
|
||||||
specifically directed at this machine, i.e., not broadcasts
|
or multicasts.
|
||||||
or multicasts.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rejectPackets = mkOption {
|
rejectPackets = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
If set, refused packets are rejected rather than dropped
|
||||||
If set, refused packets are rejected rather than dropped
|
(ignored). This means that an ICMP "port unreachable" error
|
||||||
(ignored). This means that an ICMP "port unreachable" error
|
message is sent back to the client (or a TCP RST packet in
|
||||||
message is sent back to the client (or a TCP RST packet in
|
case of an existing connection). Rejecting packets makes
|
||||||
case of an existing connection). Rejecting packets makes
|
port scanning somewhat easier.
|
||||||
port scanning somewhat easier.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trustedInterfaces = mkOption {
|
trustedInterfaces = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ "enp0s2" ];
|
example = [ "enp0s2" ];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Traffic coming in from these interfaces will be accepted
|
||||||
Traffic coming in from these interfaces will be accepted
|
unconditionally. Traffic from the loopback (lo) interface
|
||||||
unconditionally. Traffic from the loopback (lo) interface
|
will always be accepted.
|
||||||
will always be accepted.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
allowPing = mkOption {
|
allowPing = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Whether to respond to incoming ICMPv4 echo requests
|
||||||
Whether to respond to incoming ICMPv4 echo requests
|
("pings"). ICMPv6 pings are always allowed because the
|
||||||
("pings"). ICMPv6 pings are always allowed because the
|
larger address space of IPv6 makes network scanning much
|
||||||
larger address space of IPv6 makes network scanning much
|
less effective.
|
||||||
less effective.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pingLimit = mkOption {
|
pingLimit = mkOption {
|
||||||
type = types.nullOr (types.separatedString " ");
|
type = types.nullOr (types.separatedString " ");
|
||||||
default = null;
|
default = null;
|
||||||
example = "--limit 1/minute --limit-burst 5";
|
example = "--limit 1/minute --limit-burst 5";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
If pings are allowed, this allows setting rate limits on them.
|
||||||
If pings are allowed, this allows setting rate limits
|
|
||||||
on them. If non-null, this option should be in the form of
|
For the iptables based firewall, it should be set like
|
||||||
flags like "--limit 1/minute --limit-burst 5"
|
"--limit 1/minute --limit-burst 5".
|
||||||
'';
|
|
||||||
|
For the nftables based firewall, it should be set like
|
||||||
|
"2/second" or "1/minute burst 5 packets".
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
checkReversePath = mkOption {
|
checkReversePath = mkOption {
|
||||||
type = types.either types.bool (types.enum ["strict" "loose"]);
|
type = types.either types.bool (types.enum [ "strict" "loose" ]);
|
||||||
default = kernelHasRPFilter;
|
default = true;
|
||||||
defaultText = literalMD "`true` if supported by the chosen kernel";
|
defaultText = literalMD "`true` except if the iptables based firewall is in use and the kernel lacks rpfilter support";
|
||||||
example = "loose";
|
example = "loose";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Performs a reverse path filter test on a packet. If a reply
|
||||||
Performs a reverse path filter test on a packet. If a reply
|
to the packet would not be sent via the same interface that
|
||||||
to the packet would not be sent via the same interface that
|
the packet arrived on, it is refused.
|
||||||
the packet arrived on, it is refused.
|
|
||||||
|
|
||||||
If using asymmetric routing or other complicated routing, set
|
If using asymmetric routing or other complicated routing, set
|
||||||
this option to loose mode or disable it and setup your own
|
this option to loose mode or disable it and setup your own
|
||||||
counter-measures.
|
counter-measures.
|
||||||
|
|
||||||
This option can be either true (or "strict"), "loose" (only
|
This option can be either true (or "strict"), "loose" (only
|
||||||
drop the packet if the source address is not reachable via any
|
drop the packet if the source address is not reachable via any
|
||||||
interface) or false. Defaults to the value of
|
interface) or false.
|
||||||
kernelHasRPFilter.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logReversePathDrops = mkOption {
|
logReversePathDrops = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Logs dropped packets failing the reverse path filter test if
|
||||||
Logs dropped packets failing the reverse path filter test if
|
the option networking.firewall.checkReversePath is enabled.
|
||||||
the option networking.firewall.checkReversePath is enabled.
|
'';
|
||||||
'';
|
};
|
||||||
|
|
||||||
|
filterForward = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Enable filtering in IP forwarding.
|
||||||
|
|
||||||
|
This option only works with the nftables based firewall.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
connectionTrackingModules = mkOption {
|
connectionTrackingModules = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
|
example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
List of connection-tracking helpers that are auto-loaded.
|
||||||
List of connection-tracking helpers that are auto-loaded.
|
The complete list of possible values is given in the example.
|
||||||
The complete list of possible values is given in the example.
|
|
||||||
|
|
||||||
As helpers can pose as a security risk, it is advised to
|
As helpers can pose as a security risk, it is advised to
|
||||||
set this to an empty list and disable the setting
|
set this to an empty list and disable the setting
|
||||||
networking.firewall.autoLoadConntrackHelpers unless you
|
networking.firewall.autoLoadConntrackHelpers unless you
|
||||||
know what you are doing. Connection tracking is disabled
|
know what you are doing. Connection tracking is disabled
|
||||||
by default.
|
by default.
|
||||||
|
|
||||||
Loading of helpers is recommended to be done through the
|
Loading of helpers is recommended to be done through the
|
||||||
CT target. More info:
|
CT target. More info:
|
||||||
https://home.regit.org/netfilter-en/secure-use-of-helpers/
|
https://home.regit.org/netfilter-en/secure-use-of-helpers/
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
autoLoadConntrackHelpers = mkOption {
|
autoLoadConntrackHelpers = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Whether to auto-load connection-tracking helpers.
|
||||||
Whether to auto-load connection-tracking helpers.
|
See the description at networking.firewall.connectionTrackingModules
|
||||||
See the description at networking.firewall.connectionTrackingModules
|
|
||||||
|
|
||||||
(needs kernel 3.5+)
|
(needs kernel 3.5+)
|
||||||
'';
|
'';
|
||||||
};
|
|
||||||
|
|
||||||
extraCommands = mkOption {
|
|
||||||
type = types.lines;
|
|
||||||
default = "";
|
|
||||||
example = "iptables -A INPUT -p icmp -j ACCEPT";
|
|
||||||
description =
|
|
||||||
lib.mdDoc ''
|
|
||||||
Additional shell commands executed as part of the firewall
|
|
||||||
initialisation script. These are executed just before the
|
|
||||||
final "reject" firewall rule is added, so they can be used
|
|
||||||
to allow packets that would otherwise be refused.
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extraPackages = mkOption {
|
extraPackages = mkOption {
|
||||||
type = types.listOf types.package;
|
type = types.listOf types.package;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = literalExpression "[ pkgs.ipset ]";
|
example = literalExpression "[ pkgs.ipset ]";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Additional packages to be included in the environment of the system
|
||||||
Additional packages to be included in the environment of the system
|
as well as the path of networking.firewall.extraCommands.
|
||||||
as well as the path of networking.firewall.extraCommands.
|
'';
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
extraStopCommands = mkOption {
|
|
||||||
type = types.lines;
|
|
||||||
default = "";
|
|
||||||
example = "iptables -P INPUT ACCEPT";
|
|
||||||
description =
|
|
||||||
lib.mdDoc ''
|
|
||||||
Additional shell commands executed as part of the firewall
|
|
||||||
shutdown script. These are executed just after the removal
|
|
||||||
of the NixOS input rule, or if the service enters a failed
|
|
||||||
state.
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interfaces = mkOption {
|
interfaces = mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
type = with types; attrsOf (submodule [ { options = commonOptions; } ]);
|
type = with types; attrsOf (submodule [{ options = commonOptions; }]);
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Interface-specific open ports.
|
||||||
Interface-specific open ports.
|
'';
|
||||||
'';
|
};
|
||||||
|
|
||||||
|
allInterfaces = mkOption {
|
||||||
|
internal = true;
|
||||||
|
visible = false;
|
||||||
|
default = { default = mapAttrs (name: value: cfg.${name}) commonOptions; } // cfg.interfaces;
|
||||||
|
type = with types; attrsOf (submodule [{ options = commonOptions; }]);
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
All open ports.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
} // commonOptions;
|
} // commonOptions;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
###### implementation
|
|
||||||
|
|
||||||
# FIXME: Maybe if `enable' is false, the firewall should still be
|
|
||||||
# built but not started by default?
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.filterForward -> config.networking.nftables.enable;
|
||||||
|
message = "filterForward only works with the nftables based firewall";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
networking.firewall.trustedInterfaces = [ "lo" ];
|
networking.firewall.trustedInterfaces = [ "lo" ];
|
||||||
|
|
||||||
environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
|
environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
|
||||||
|
@ -545,40 +281,6 @@ in
|
||||||
options nf_conntrack nf_conntrack_helper=1
|
options nf_conntrack nf_conntrack_helper=1
|
||||||
'';
|
'';
|
||||||
|
|
||||||
assertions = [
|
|
||||||
# This is approximately "checkReversePath -> kernelHasRPFilter",
|
|
||||||
# but the checkReversePath option can include non-boolean
|
|
||||||
# values.
|
|
||||||
{ assertion = cfg.checkReversePath == false || kernelHasRPFilter;
|
|
||||||
message = "This kernel does not support rpfilter"; }
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.firewall = {
|
|
||||||
description = "Firewall";
|
|
||||||
wantedBy = [ "sysinit.target" ];
|
|
||||||
wants = [ "network-pre.target" ];
|
|
||||||
before = [ "network-pre.target" ];
|
|
||||||
after = [ "systemd-modules-load.service" ];
|
|
||||||
|
|
||||||
path = [ cfg.package ] ++ cfg.extraPackages;
|
|
||||||
|
|
||||||
# FIXME: this module may also try to load kernel modules, but
|
|
||||||
# containers don't have CAP_SYS_MODULE. So the host system had
|
|
||||||
# better have all necessary modules already loaded.
|
|
||||||
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
|
|
||||||
unitConfig.DefaultDependencies = false;
|
|
||||||
|
|
||||||
reloadIfChanged = true;
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
ExecStart = "@${startScript} firewall-start";
|
|
||||||
ExecReload = "@${reloadScript} firewall-reload";
|
|
||||||
ExecStop = "@${stopScript} firewall-stop";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
191
nixos/modules/services/networking/nat-iptables.nix
Normal file
191
nixos/modules/services/networking/nat-iptables.nix
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
# This module enables Network Address Translation (NAT).
|
||||||
|
# XXX: todo: support multiple upstream links
|
||||||
|
# see http://yesican.chsoft.biz/lartc/MultihomedLinuxNetworking.html
|
||||||
|
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.networking.nat;
|
||||||
|
|
||||||
|
mkDest = externalIP:
|
||||||
|
if externalIP == null
|
||||||
|
then "-j MASQUERADE"
|
||||||
|
else "-j SNAT --to-source ${externalIP}";
|
||||||
|
dest = mkDest cfg.externalIP;
|
||||||
|
destIPv6 = mkDest cfg.externalIPv6;
|
||||||
|
|
||||||
|
# Whether given IP (plus optional port) is an IPv6.
|
||||||
|
isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
|
||||||
|
|
||||||
|
helpers = import ./helpers.nix { inherit config lib; };
|
||||||
|
|
||||||
|
flushNat = ''
|
||||||
|
${helpers}
|
||||||
|
ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
|
||||||
|
ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
|
||||||
|
ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
|
||||||
|
ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
|
||||||
|
ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
|
||||||
|
ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
|
||||||
|
ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
|
||||||
|
ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
|
||||||
|
ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
|
||||||
|
|
||||||
|
${cfg.extraStopCommands}
|
||||||
|
'';
|
||||||
|
|
||||||
|
mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
|
||||||
|
# We can't match on incoming interface in POSTROUTING, so
|
||||||
|
# mark packets coming from the internal interfaces.
|
||||||
|
${concatMapStrings (iface: ''
|
||||||
|
${iptables} -w -t nat -A nixos-nat-pre \
|
||||||
|
-i '${iface}' -j MARK --set-mark 1
|
||||||
|
'') cfg.internalInterfaces}
|
||||||
|
|
||||||
|
# NAT the marked packets.
|
||||||
|
${optionalString (cfg.internalInterfaces != []) ''
|
||||||
|
${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
|
||||||
|
${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
|
||||||
|
''}
|
||||||
|
|
||||||
|
# NAT packets coming from the internal IPs.
|
||||||
|
${concatMapStrings (range: ''
|
||||||
|
${iptables} -w -t nat -A nixos-nat-post \
|
||||||
|
-s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
|
||||||
|
'') internalIPs}
|
||||||
|
|
||||||
|
# NAT from external ports to internal ports.
|
||||||
|
${concatMapStrings (fwd: ''
|
||||||
|
${iptables} -w -t nat -A nixos-nat-pre \
|
||||||
|
-i ${toString cfg.externalInterface} -p ${fwd.proto} \
|
||||||
|
--dport ${builtins.toString fwd.sourcePort} \
|
||||||
|
-j DNAT --to-destination ${fwd.destination}
|
||||||
|
|
||||||
|
${concatMapStrings (loopbackip:
|
||||||
|
let
|
||||||
|
matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
|
||||||
|
m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
|
||||||
|
destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
|
||||||
|
destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
|
||||||
|
in ''
|
||||||
|
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
|
||||||
|
${iptables} -w -t nat -A nixos-nat-out \
|
||||||
|
-d ${loopbackip} -p ${fwd.proto} \
|
||||||
|
--dport ${builtins.toString fwd.sourcePort} \
|
||||||
|
-j DNAT --to-destination ${fwd.destination}
|
||||||
|
|
||||||
|
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
|
||||||
|
${iptables} -w -t nat -A nixos-nat-pre \
|
||||||
|
-d ${loopbackip} -p ${fwd.proto} \
|
||||||
|
--dport ${builtins.toString fwd.sourcePort} \
|
||||||
|
-j DNAT --to-destination ${fwd.destination}
|
||||||
|
|
||||||
|
${iptables} -w -t nat -A nixos-nat-post \
|
||||||
|
-d ${destinationIP} -p ${fwd.proto} \
|
||||||
|
--dport ${destinationPorts} \
|
||||||
|
-j SNAT --to-source ${loopbackip}
|
||||||
|
'') fwd.loopbackIPs}
|
||||||
|
'') forwardPorts}
|
||||||
|
'';
|
||||||
|
|
||||||
|
setupNat = ''
|
||||||
|
${helpers}
|
||||||
|
# Create subchains where we store rules
|
||||||
|
ip46tables -w -t nat -N nixos-nat-pre
|
||||||
|
ip46tables -w -t nat -N nixos-nat-post
|
||||||
|
ip46tables -w -t nat -N nixos-nat-out
|
||||||
|
|
||||||
|
${mkSetupNat {
|
||||||
|
iptables = "iptables";
|
||||||
|
inherit dest;
|
||||||
|
inherit (cfg) internalIPs;
|
||||||
|
forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
|
||||||
|
}}
|
||||||
|
|
||||||
|
${optionalString cfg.enableIPv6 (mkSetupNat {
|
||||||
|
iptables = "ip6tables";
|
||||||
|
dest = destIPv6;
|
||||||
|
internalIPs = cfg.internalIPv6s;
|
||||||
|
forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
|
||||||
|
})}
|
||||||
|
|
||||||
|
${optionalString (cfg.dmzHost != null) ''
|
||||||
|
iptables -w -t nat -A nixos-nat-pre \
|
||||||
|
-i ${toString cfg.externalInterface} -j DNAT \
|
||||||
|
--to-destination ${cfg.dmzHost}
|
||||||
|
''}
|
||||||
|
|
||||||
|
${cfg.extraCommands}
|
||||||
|
|
||||||
|
# Append our chains to the nat tables
|
||||||
|
ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
|
||||||
|
ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
|
||||||
|
ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
networking.nat.extraCommands = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = "iptables -A INPUT -p icmp -j ACCEPT";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Additional shell commands executed as part of the nat
|
||||||
|
initialisation script.
|
||||||
|
|
||||||
|
This option is incompatible with the nftables based nat module.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.nat.extraStopCommands = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = "iptables -D INPUT -p icmp -j ACCEPT || true";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Additional shell commands executed as part of the nat
|
||||||
|
teardown script.
|
||||||
|
|
||||||
|
This option is incompatible with the nftables based nat module.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
config = mkIf (!config.networking.nftables.enable)
|
||||||
|
(mkMerge [
|
||||||
|
({ networking.firewall.extraCommands = mkBefore flushNat; })
|
||||||
|
(mkIf config.networking.nat.enable {
|
||||||
|
|
||||||
|
networking.firewall = mkIf config.networking.firewall.enable {
|
||||||
|
extraCommands = setupNat;
|
||||||
|
extraStopCommands = flushNat;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services = mkIf (!config.networking.firewall.enable) {
|
||||||
|
nat = {
|
||||||
|
description = "Network Address Translation";
|
||||||
|
wantedBy = [ "network.target" ];
|
||||||
|
after = [ "network-pre.target" "systemd-modules-load.service" ];
|
||||||
|
path = [ config.networking.firewall.package ];
|
||||||
|
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
script = flushNat + setupNat;
|
||||||
|
|
||||||
|
postStop = flushNat;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
184
nixos/modules/services/networking/nat-nftables.nix
Normal file
184
nixos/modules/services/networking/nat-nftables.nix
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.networking.nat;
|
||||||
|
|
||||||
|
mkDest = externalIP:
|
||||||
|
if externalIP == null
|
||||||
|
then "masquerade"
|
||||||
|
else "snat ${externalIP}";
|
||||||
|
dest = mkDest cfg.externalIP;
|
||||||
|
destIPv6 = mkDest cfg.externalIPv6;
|
||||||
|
|
||||||
|
toNftSet = list: concatStringsSep ", " list;
|
||||||
|
toNftRange = ports: replaceStrings [ ":" ] [ "-" ] (toString ports);
|
||||||
|
|
||||||
|
ifaceSet = toNftSet (map (x: ''"${x}"'') cfg.internalInterfaces);
|
||||||
|
ipSet = toNftSet cfg.internalIPs;
|
||||||
|
ipv6Set = toNftSet cfg.internalIPv6s;
|
||||||
|
oifExpr = optionalString (cfg.externalInterface != null) ''oifname "${cfg.externalInterface}"'';
|
||||||
|
|
||||||
|
# Whether given IP (plus optional port) is an IPv6.
|
||||||
|
isIPv6 = ip: length (lib.splitString ":" ip) > 2;
|
||||||
|
|
||||||
|
splitIPPorts = IPPorts:
|
||||||
|
let
|
||||||
|
matchIP = if isIPv6 IPPorts then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
|
||||||
|
m = builtins.match "${matchIP}:([0-9-]+)" IPPorts;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
IP = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 0;
|
||||||
|
ports = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
mkTable = { ipVer, dest, ipSet, forwardPorts, dmzHost }:
|
||||||
|
let
|
||||||
|
# nftables does not support both port and port range as values in a dnat map.
|
||||||
|
# e.g. "dnat th dport map { 80 : 10.0.0.1 . 80, 443 : 10.0.0.2 . 900-1000 }"
|
||||||
|
# So we split them.
|
||||||
|
fwdPorts = filter (x: length (splitString "-" x.destination) == 1) forwardPorts;
|
||||||
|
fwdPortsRange = filter (x: length (splitString "-" x.destination) > 1) forwardPorts;
|
||||||
|
|
||||||
|
# nftables maps for port forward
|
||||||
|
# l4proto . dport : addr . port
|
||||||
|
toFwdMap = forwardPorts: toNftSet (map
|
||||||
|
(fwd:
|
||||||
|
with (splitIPPorts fwd.destination);
|
||||||
|
"${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
|
||||||
|
)
|
||||||
|
forwardPorts);
|
||||||
|
fwdMap = toFwdMap fwdPorts;
|
||||||
|
fwdRangeMap = toFwdMap fwdPortsRange;
|
||||||
|
|
||||||
|
# nftables maps for port forward loopback dnat
|
||||||
|
# daddr . l4proto . dport : addr . port
|
||||||
|
toFwdLoopDnatMap = forwardPorts: toNftSet (concatMap
|
||||||
|
(fwd: map
|
||||||
|
(loopbackip:
|
||||||
|
with (splitIPPorts fwd.destination);
|
||||||
|
"${loopbackip} . ${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
|
||||||
|
)
|
||||||
|
fwd.loopbackIPs)
|
||||||
|
forwardPorts);
|
||||||
|
fwdLoopDnatMap = toFwdLoopDnatMap fwdPorts;
|
||||||
|
fwdLoopDnatRangeMap = toFwdLoopDnatMap fwdPortsRange;
|
||||||
|
|
||||||
|
# nftables set for port forward loopback snat
|
||||||
|
# daddr . l4proto . dport
|
||||||
|
fwdLoopSnatSet = toNftSet (map
|
||||||
|
(fwd:
|
||||||
|
with (splitIPPorts fwd.destination);
|
||||||
|
"${IP} . ${fwd.proto} . ${ports}"
|
||||||
|
)
|
||||||
|
forwardPorts);
|
||||||
|
in
|
||||||
|
''
|
||||||
|
chain pre {
|
||||||
|
type nat hook prerouting priority dstnat;
|
||||||
|
|
||||||
|
${optionalString (fwdMap != "") ''
|
||||||
|
iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdMap} } comment "port forward"
|
||||||
|
''}
|
||||||
|
${optionalString (fwdRangeMap != "") ''
|
||||||
|
iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdRangeMap} } comment "port forward"
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString (fwdLoopDnatMap != "") ''
|
||||||
|
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from other hosts behind NAT"
|
||||||
|
''}
|
||||||
|
${optionalString (fwdLoopDnatRangeMap != "") ''
|
||||||
|
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from other hosts behind NAT"
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString (dmzHost != null) ''
|
||||||
|
iifname "${cfg.externalInterface}" dnat ${dmzHost} comment "dmz"
|
||||||
|
''}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain post {
|
||||||
|
type nat hook postrouting priority srcnat;
|
||||||
|
|
||||||
|
${optionalString (ifaceSet != "") ''
|
||||||
|
iifname { ${ifaceSet} } ${oifExpr} ${dest} comment "from internal interfaces"
|
||||||
|
''}
|
||||||
|
${optionalString (ipSet != "") ''
|
||||||
|
${ipVer} saddr { ${ipSet} } ${oifExpr} ${dest} comment "from internal IPs"
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString (fwdLoopSnatSet != "") ''
|
||||||
|
iifname != "${cfg.externalInterface}" ${ipVer} daddr . meta l4proto . th dport { ${fwdLoopSnatSet} } masquerade comment "port forward loopback snat"
|
||||||
|
''}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain out {
|
||||||
|
type nat hook output priority mangle;
|
||||||
|
|
||||||
|
${optionalString (fwdLoopDnatMap != "") ''
|
||||||
|
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from the host itself"
|
||||||
|
''}
|
||||||
|
${optionalString (fwdLoopDnatRangeMap != "") ''
|
||||||
|
dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from the host itself"
|
||||||
|
''}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
config = mkIf (config.networking.nftables.enable && cfg.enable) {
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.extraCommands == "";
|
||||||
|
message = "extraCommands is incompatible with the nftables based nat module: ${cfg.extraCommands}";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.extraStopCommands == "";
|
||||||
|
message = "extraStopCommands is incompatible with the nftables based nat module: ${cfg.extraStopCommands}";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = config.networking.nftables.rulesetFile == null;
|
||||||
|
message = "networking.nftables.rulesetFile conflicts with the nat module";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.nftables.ruleset = ''
|
||||||
|
table ip nixos-nat {
|
||||||
|
${mkTable {
|
||||||
|
ipVer = "ip";
|
||||||
|
inherit dest ipSet;
|
||||||
|
forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
|
||||||
|
inherit (cfg) dmzHost;
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
${optionalString cfg.enableIPv6 ''
|
||||||
|
table ip6 nixos-nat {
|
||||||
|
${mkTable {
|
||||||
|
ipVer = "ip6";
|
||||||
|
dest = destIPv6;
|
||||||
|
ipSet = ipv6Set;
|
||||||
|
forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
|
||||||
|
dmzHost = null;
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
|
||||||
|
networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward ''
|
||||||
|
${optionalString (ifaceSet != "") ''
|
||||||
|
iifname { ${ifaceSet} } ${oifExpr} accept comment "from internal interfaces"
|
||||||
|
''}
|
||||||
|
${optionalString (ipSet != "") ''
|
||||||
|
ip saddr { ${ipSet} } ${oifExpr} accept comment "from internal IPs"
|
||||||
|
''}
|
||||||
|
${optionalString (ipv6Set != "") ''
|
||||||
|
ip6 saddr { ${ipv6Set} } ${oifExpr} accept comment "from internal IPv6s"
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
|
@ -7,219 +7,95 @@
|
||||||
with lib;
|
with lib;
|
||||||
|
|
||||||
let
|
let
|
||||||
|
|
||||||
cfg = config.networking.nat;
|
cfg = config.networking.nat;
|
||||||
|
|
||||||
mkDest = externalIP: if externalIP == null
|
|
||||||
then "-j MASQUERADE"
|
|
||||||
else "-j SNAT --to-source ${externalIP}";
|
|
||||||
dest = mkDest cfg.externalIP;
|
|
||||||
destIPv6 = mkDest cfg.externalIPv6;
|
|
||||||
|
|
||||||
# Whether given IP (plus optional port) is an IPv6.
|
|
||||||
isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
|
|
||||||
|
|
||||||
helpers = import ./helpers.nix { inherit config lib; };
|
|
||||||
|
|
||||||
flushNat = ''
|
|
||||||
${helpers}
|
|
||||||
ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
|
|
||||||
ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
|
|
||||||
ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
|
|
||||||
ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
|
|
||||||
ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
|
|
||||||
ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
|
|
||||||
ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
|
|
||||||
ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
|
|
||||||
ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
|
|
||||||
|
|
||||||
${cfg.extraStopCommands}
|
|
||||||
'';
|
|
||||||
|
|
||||||
mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
|
|
||||||
# We can't match on incoming interface in POSTROUTING, so
|
|
||||||
# mark packets coming from the internal interfaces.
|
|
||||||
${concatMapStrings (iface: ''
|
|
||||||
${iptables} -w -t nat -A nixos-nat-pre \
|
|
||||||
-i '${iface}' -j MARK --set-mark 1
|
|
||||||
'') cfg.internalInterfaces}
|
|
||||||
|
|
||||||
# NAT the marked packets.
|
|
||||||
${optionalString (cfg.internalInterfaces != []) ''
|
|
||||||
${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
|
|
||||||
${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
|
|
||||||
''}
|
|
||||||
|
|
||||||
# NAT packets coming from the internal IPs.
|
|
||||||
${concatMapStrings (range: ''
|
|
||||||
${iptables} -w -t nat -A nixos-nat-post \
|
|
||||||
-s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
|
|
||||||
'') internalIPs}
|
|
||||||
|
|
||||||
# NAT from external ports to internal ports.
|
|
||||||
${concatMapStrings (fwd: ''
|
|
||||||
${iptables} -w -t nat -A nixos-nat-pre \
|
|
||||||
-i ${toString cfg.externalInterface} -p ${fwd.proto} \
|
|
||||||
--dport ${builtins.toString fwd.sourcePort} \
|
|
||||||
-j DNAT --to-destination ${fwd.destination}
|
|
||||||
|
|
||||||
${concatMapStrings (loopbackip:
|
|
||||||
let
|
|
||||||
matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
|
|
||||||
m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
|
|
||||||
destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
|
|
||||||
destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
|
|
||||||
in ''
|
|
||||||
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
|
|
||||||
${iptables} -w -t nat -A nixos-nat-out \
|
|
||||||
-d ${loopbackip} -p ${fwd.proto} \
|
|
||||||
--dport ${builtins.toString fwd.sourcePort} \
|
|
||||||
-j DNAT --to-destination ${fwd.destination}
|
|
||||||
|
|
||||||
# Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
|
|
||||||
${iptables} -w -t nat -A nixos-nat-pre \
|
|
||||||
-d ${loopbackip} -p ${fwd.proto} \
|
|
||||||
--dport ${builtins.toString fwd.sourcePort} \
|
|
||||||
-j DNAT --to-destination ${fwd.destination}
|
|
||||||
|
|
||||||
${iptables} -w -t nat -A nixos-nat-post \
|
|
||||||
-d ${destinationIP} -p ${fwd.proto} \
|
|
||||||
--dport ${destinationPorts} \
|
|
||||||
-j SNAT --to-source ${loopbackip}
|
|
||||||
'') fwd.loopbackIPs}
|
|
||||||
'') forwardPorts}
|
|
||||||
'';
|
|
||||||
|
|
||||||
setupNat = ''
|
|
||||||
${helpers}
|
|
||||||
# Create subchains where we store rules
|
|
||||||
ip46tables -w -t nat -N nixos-nat-pre
|
|
||||||
ip46tables -w -t nat -N nixos-nat-post
|
|
||||||
ip46tables -w -t nat -N nixos-nat-out
|
|
||||||
|
|
||||||
${mkSetupNat {
|
|
||||||
iptables = "iptables";
|
|
||||||
inherit dest;
|
|
||||||
inherit (cfg) internalIPs;
|
|
||||||
forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
|
|
||||||
}}
|
|
||||||
|
|
||||||
${optionalString cfg.enableIPv6 (mkSetupNat {
|
|
||||||
iptables = "ip6tables";
|
|
||||||
dest = destIPv6;
|
|
||||||
internalIPs = cfg.internalIPv6s;
|
|
||||||
forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
|
|
||||||
})}
|
|
||||||
|
|
||||||
${optionalString (cfg.dmzHost != null) ''
|
|
||||||
iptables -w -t nat -A nixos-nat-pre \
|
|
||||||
-i ${toString cfg.externalInterface} -j DNAT \
|
|
||||||
--to-destination ${cfg.dmzHost}
|
|
||||||
''}
|
|
||||||
|
|
||||||
${cfg.extraCommands}
|
|
||||||
|
|
||||||
# Append our chains to the nat tables
|
|
||||||
ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
|
|
||||||
ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
|
|
||||||
ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
|
|
||||||
'';
|
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
###### interface
|
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
|
|
||||||
networking.nat.enable = mkOption {
|
networking.nat.enable = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Whether to enable Network Address Translation (NAT).
|
||||||
Whether to enable Network Address Translation (NAT).
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.enableIPv6 = mkOption {
|
networking.nat.enableIPv6 = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
Whether to enable IPv6 NAT.
|
||||||
Whether to enable IPv6 NAT.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.internalInterfaces = mkOption {
|
networking.nat.internalInterfaces = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [];
|
default = [ ];
|
||||||
example = [ "eth0" ];
|
example = [ "eth0" ];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The interfaces for which to perform NAT. Packets coming from
|
||||||
The interfaces for which to perform NAT. Packets coming from
|
these interface and destined for the external interface will
|
||||||
these interface and destined for the external interface will
|
be rewritten.
|
||||||
be rewritten.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.internalIPs = mkOption {
|
networking.nat.internalIPs = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [];
|
default = [ ];
|
||||||
example = [ "192.168.1.0/24" ];
|
example = [ "192.168.1.0/24" ];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The IP address ranges for which to perform NAT. Packets
|
||||||
The IP address ranges for which to perform NAT. Packets
|
coming from these addresses (on any interface) and destined
|
||||||
coming from these addresses (on any interface) and destined
|
for the external interface will be rewritten.
|
||||||
for the external interface will be rewritten.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.internalIPv6s = mkOption {
|
networking.nat.internalIPv6s = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [];
|
default = [ ];
|
||||||
example = [ "fc00::/64" ];
|
example = [ "fc00::/64" ];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The IPv6 address ranges for which to perform NAT. Packets
|
||||||
The IPv6 address ranges for which to perform NAT. Packets
|
coming from these addresses (on any interface) and destined
|
||||||
coming from these addresses (on any interface) and destined
|
for the external interface will be rewritten.
|
||||||
for the external interface will be rewritten.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.externalInterface = mkOption {
|
networking.nat.externalInterface = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "eth1";
|
example = "eth1";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The name of the external network interface.
|
||||||
The name of the external network interface.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.externalIP = mkOption {
|
networking.nat.externalIP = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "203.0.113.123";
|
example = "203.0.113.123";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The public IP address to which packets from the local
|
||||||
The public IP address to which packets from the local
|
network are to be rewritten. If this is left empty, the
|
||||||
network are to be rewritten. If this is left empty, the
|
IP address associated with the external interface will be
|
||||||
IP address associated with the external interface will be
|
used.
|
||||||
used.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.externalIPv6 = mkOption {
|
networking.nat.externalIPv6 = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "2001:dc0:2001:11::175";
|
example = "2001:dc0:2001:11::175";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The public IPv6 address to which packets from the local
|
||||||
The public IPv6 address to which packets from the local
|
network are to be rewritten. If this is left empty, the
|
||||||
network are to be rewritten. If this is left empty, the
|
IP address associated with the external interface will be
|
||||||
IP address associated with the external interface will be
|
used.
|
||||||
used.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.forwardPorts = mkOption {
|
networking.nat.forwardPorts = mkOption {
|
||||||
|
@ -246,122 +122,75 @@ in
|
||||||
|
|
||||||
loopbackIPs = mkOption {
|
loopbackIPs = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [];
|
default = [ ];
|
||||||
example = literalExpression ''[ "55.1.2.3" ]'';
|
example = literalExpression ''[ "55.1.2.3" ]'';
|
||||||
description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
|
description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
default = [];
|
default = [ ];
|
||||||
example = [
|
example = [
|
||||||
{ sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
|
{ sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
|
||||||
{ sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
|
{ sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
|
||||||
];
|
];
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
List of forwarded ports from the external interface to
|
||||||
List of forwarded ports from the external interface to
|
internal destinations by using DNAT. Destination can be
|
||||||
internal destinations by using DNAT. Destination can be
|
IPv6 if IPv6 NAT is enabled.
|
||||||
IPv6 if IPv6 NAT is enabled.
|
'';
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.nat.dmzHost = mkOption {
|
networking.nat.dmzHost = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "10.0.0.1";
|
example = "10.0.0.1";
|
||||||
description =
|
description = lib.mdDoc ''
|
||||||
lib.mdDoc ''
|
The local IP address to which all traffic that does not match any
|
||||||
The local IP address to which all traffic that does not match any
|
forwarding rule is forwarded.
|
||||||
forwarding rule is forwarded.
|
'';
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.nat.extraCommands = mkOption {
|
|
||||||
type = types.lines;
|
|
||||||
default = "";
|
|
||||||
example = "iptables -A INPUT -p icmp -j ACCEPT";
|
|
||||||
description =
|
|
||||||
lib.mdDoc ''
|
|
||||||
Additional shell commands executed as part of the nat
|
|
||||||
initialisation script.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.nat.extraStopCommands = mkOption {
|
|
||||||
type = types.lines;
|
|
||||||
default = "";
|
|
||||||
example = "iptables -D INPUT -p icmp -j ACCEPT || true";
|
|
||||||
description =
|
|
||||||
lib.mdDoc ''
|
|
||||||
Additional shell commands executed as part of the nat
|
|
||||||
teardown script.
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
###### implementation
|
config = mkIf config.networking.nat.enable {
|
||||||
|
|
||||||
config = mkMerge [
|
assertions = [
|
||||||
{ networking.firewall.extraCommands = mkBefore flushNat; }
|
{
|
||||||
(mkIf config.networking.nat.enable {
|
assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
|
||||||
|
message = "networking.nat.enableIPv6 requires networking.enableIPv6";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
|
||||||
|
message = "networking.nat.dmzHost requires networking.nat.externalInterface";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = (cfg.forwardPorts != [ ]) -> (cfg.externalInterface != null);
|
||||||
|
message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
assertions = [
|
# Use the same iptables package as in config.networking.firewall.
|
||||||
{ assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
|
# When the firewall is enabled, this should be deduplicated without any
|
||||||
message = "networking.nat.enableIPv6 requires networking.enableIPv6";
|
# error.
|
||||||
}
|
environment.systemPackages = [ config.networking.firewall.package ];
|
||||||
{ assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
|
|
||||||
message = "networking.nat.dmzHost requires networking.nat.externalInterface";
|
|
||||||
}
|
|
||||||
{ assertion = (cfg.forwardPorts != []) -> (cfg.externalInterface != null);
|
|
||||||
message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
# Use the same iptables package as in config.networking.firewall.
|
boot = {
|
||||||
# When the firewall is enabled, this should be deduplicated without any
|
kernelModules = [ "nf_nat_ftp" ];
|
||||||
# error.
|
kernel.sysctl = {
|
||||||
environment.systemPackages = [ config.networking.firewall.package ];
|
"net.ipv4.conf.all.forwarding" = mkOverride 99 true;
|
||||||
|
"net.ipv4.conf.default.forwarding" = mkOverride 99 true;
|
||||||
|
} // optionalAttrs cfg.enableIPv6 {
|
||||||
|
# Do not prevent IPv6 autoconfiguration.
|
||||||
|
# See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
|
||||||
|
"net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
|
||||||
|
"net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
|
||||||
|
|
||||||
boot = {
|
# Forward IPv6 packets.
|
||||||
kernelModules = [ "nf_nat_ftp" ];
|
"net.ipv6.conf.all.forwarding" = mkOverride 99 true;
|
||||||
kernel.sysctl = {
|
"net.ipv6.conf.default.forwarding" = mkOverride 99 true;
|
||||||
"net.ipv4.conf.all.forwarding" = mkOverride 99 true;
|
|
||||||
"net.ipv4.conf.default.forwarding" = mkOverride 99 true;
|
|
||||||
} // optionalAttrs cfg.enableIPv6 {
|
|
||||||
# Do not prevent IPv6 autoconfiguration.
|
|
||||||
# See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
|
|
||||||
"net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
|
|
||||||
"net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
|
|
||||||
|
|
||||||
# Forward IPv6 packets.
|
|
||||||
"net.ipv6.conf.all.forwarding" = mkOverride 99 true;
|
|
||||||
"net.ipv6.conf.default.forwarding" = mkOverride 99 true;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
networking.firewall = mkIf config.networking.firewall.enable {
|
};
|
||||||
extraCommands = setupNat;
|
|
||||||
extraStopCommands = flushNat;
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services = mkIf (!config.networking.firewall.enable) { nat = {
|
|
||||||
description = "Network Address Translation";
|
|
||||||
wantedBy = [ "network.target" ];
|
|
||||||
after = [ "network-pre.target" "systemd-modules-load.service" ];
|
|
||||||
path = [ config.networking.firewall.package ];
|
|
||||||
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
script = flushNat + setupNat;
|
|
||||||
|
|
||||||
postStop = flushNat;
|
|
||||||
}; };
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,9 @@ in
|
||||||
default = false;
|
default = false;
|
||||||
description =
|
description =
|
||||||
lib.mdDoc ''
|
lib.mdDoc ''
|
||||||
Whether to enable nftables. nftables is a Linux-based packet
|
Whether to enable nftables and use nftables based firewall if enabled.
|
||||||
filtering framework intended to replace frameworks like iptables.
|
nftables is a Linux-based packet filtering framework intended to
|
||||||
|
replace frameworks like iptables.
|
||||||
This conflicts with the standard networking firewall, so make sure to
|
|
||||||
disable it before using nftables.
|
|
||||||
|
|
||||||
Note that if you have Docker enabled you will not be able to use
|
Note that if you have Docker enabled you will not be able to use
|
||||||
nftables without intervention. Docker uses iptables internally to
|
nftables without intervention. Docker uses iptables internally to
|
||||||
|
@ -79,19 +77,17 @@ in
|
||||||
lib.mdDoc ''
|
lib.mdDoc ''
|
||||||
The ruleset to be used with nftables. Should be in a format that
|
The ruleset to be used with nftables. Should be in a format that
|
||||||
can be loaded using "/bin/nft -f". The ruleset is updated atomically.
|
can be loaded using "/bin/nft -f". The ruleset is updated atomically.
|
||||||
|
This option conflicts with rulesetFile.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
networking.nftables.rulesetFile = mkOption {
|
networking.nftables.rulesetFile = mkOption {
|
||||||
type = types.path;
|
type = types.nullOr types.path;
|
||||||
default = pkgs.writeTextFile {
|
default = null;
|
||||||
name = "nftables-rules";
|
|
||||||
text = cfg.ruleset;
|
|
||||||
};
|
|
||||||
defaultText = literalMD ''a file with the contents of {option}`networking.nftables.ruleset`'';
|
|
||||||
description =
|
description =
|
||||||
lib.mdDoc ''
|
lib.mdDoc ''
|
||||||
The ruleset file to be used with nftables. Should be in a format that
|
The ruleset file to be used with nftables. Should be in a format that
|
||||||
can be loaded using "nft -f". The ruleset is updated atomically.
|
can be loaded using "nft -f". The ruleset is updated atomically.
|
||||||
|
This option conflicts with ruleset and nftables based firewall.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -99,10 +95,6 @@ in
|
||||||
###### implementation
|
###### implementation
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
assertions = [{
|
|
||||||
assertion = config.networking.firewall.enable == false;
|
|
||||||
message = "You can not use nftables and iptables at the same time. networking.firewall.enable must be set to false.";
|
|
||||||
}];
|
|
||||||
boot.blacklistedKernelModules = [ "ip_tables" ];
|
boot.blacklistedKernelModules = [ "ip_tables" ];
|
||||||
environment.systemPackages = [ pkgs.nftables ];
|
environment.systemPackages = [ pkgs.nftables ];
|
||||||
networking.networkmanager.firewallBackend = mkDefault "nftables";
|
networking.networkmanager.firewallBackend = mkDefault "nftables";
|
||||||
|
@ -116,7 +108,9 @@ in
|
||||||
rulesScript = pkgs.writeScript "nftables-rules" ''
|
rulesScript = pkgs.writeScript "nftables-rules" ''
|
||||||
#! ${pkgs.nftables}/bin/nft -f
|
#! ${pkgs.nftables}/bin/nft -f
|
||||||
flush ruleset
|
flush ruleset
|
||||||
include "${cfg.rulesetFile}"
|
${if cfg.rulesetFile != null then ''
|
||||||
|
include "${cfg.rulesetFile}"
|
||||||
|
'' else cfg.ruleset}
|
||||||
'';
|
'';
|
||||||
in {
|
in {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
|
|
|
@ -211,7 +211,8 @@ in {
|
||||||
firefox-esr = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job
|
firefox-esr = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job
|
||||||
firefox-esr-102 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-102; };
|
firefox-esr-102 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-102; };
|
||||||
firejail = handleTest ./firejail.nix {};
|
firejail = handleTest ./firejail.nix {};
|
||||||
firewall = handleTest ./firewall.nix {};
|
firewall = handleTest ./firewall.nix { nftables = false; };
|
||||||
|
firewall-nftables = handleTest ./firewall.nix { nftables = true; };
|
||||||
fish = handleTest ./fish.nix {};
|
fish = handleTest ./fish.nix {};
|
||||||
flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
|
flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
|
||||||
fluentd = handleTest ./fluentd.nix {};
|
fluentd = handleTest ./fluentd.nix {};
|
||||||
|
@ -412,6 +413,9 @@ in {
|
||||||
nat.firewall = handleTest ./nat.nix { withFirewall = true; };
|
nat.firewall = handleTest ./nat.nix { withFirewall = true; };
|
||||||
nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; };
|
nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; };
|
||||||
nat.standalone = handleTest ./nat.nix { withFirewall = false; };
|
nat.standalone = handleTest ./nat.nix { withFirewall = false; };
|
||||||
|
nat.nftables.firewall = handleTest ./nat.nix { withFirewall = true; nftables = true; };
|
||||||
|
nat.nftables.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; nftables = true; };
|
||||||
|
nat.nftables.standalone = handleTest ./nat.nix { withFirewall = false; nftables = true; };
|
||||||
nats = handleTest ./nats.nix {};
|
nats = handleTest ./nats.nix {};
|
||||||
navidrome = handleTest ./navidrome.nix {};
|
navidrome = handleTest ./navidrome.nix {};
|
||||||
nbd = handleTest ./nbd.nix {};
|
nbd = handleTest ./nbd.nix {};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Test the firewall module.
|
# Test the firewall module.
|
||||||
|
|
||||||
import ./make-test-python.nix ( { pkgs, ... } : {
|
import ./make-test-python.nix ( { pkgs, nftables, ... } : {
|
||||||
name = "firewall";
|
name = "firewall" + pkgs.lib.optionalString nftables "-nftables";
|
||||||
meta = with pkgs.lib.maintainers; {
|
meta = with pkgs.lib.maintainers; {
|
||||||
maintainers = [ eelco ];
|
maintainers = [ eelco ];
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{ networking.firewall.enable = true;
|
{ networking.firewall.enable = true;
|
||||||
networking.firewall.logRefusedPackets = true;
|
networking.firewall.logRefusedPackets = true;
|
||||||
|
networking.nftables.enable = nftables;
|
||||||
services.httpd.enable = true;
|
services.httpd.enable = true;
|
||||||
services.httpd.adminAddr = "foo@example.org";
|
services.httpd.adminAddr = "foo@example.org";
|
||||||
};
|
};
|
||||||
|
@ -23,6 +24,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{ networking.firewall.enable = true;
|
{ networking.firewall.enable = true;
|
||||||
networking.firewall.rejectPackets = true;
|
networking.firewall.rejectPackets = true;
|
||||||
|
networking.nftables.enable = nftables;
|
||||||
};
|
};
|
||||||
|
|
||||||
attacker =
|
attacker =
|
||||||
|
@ -35,10 +37,11 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||||
|
|
||||||
testScript = { nodes, ... }: let
|
testScript = { nodes, ... }: let
|
||||||
newSystem = nodes.walled2.config.system.build.toplevel;
|
newSystem = nodes.walled2.config.system.build.toplevel;
|
||||||
|
unit = if nftables then "nftables" else "firewall";
|
||||||
in ''
|
in ''
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
walled.wait_for_unit("firewall")
|
walled.wait_for_unit("${unit}")
|
||||||
walled.wait_for_unit("httpd")
|
walled.wait_for_unit("httpd")
|
||||||
attacker.wait_for_unit("network.target")
|
attacker.wait_for_unit("network.target")
|
||||||
|
|
||||||
|
@ -54,12 +57,12 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||||
walled.succeed("ping -c 1 attacker >&2")
|
walled.succeed("ping -c 1 attacker >&2")
|
||||||
|
|
||||||
# If we stop the firewall, then connections should succeed.
|
# If we stop the firewall, then connections should succeed.
|
||||||
walled.stop_job("firewall")
|
walled.stop_job("${unit}")
|
||||||
attacker.succeed("curl -v http://walled/ >&2")
|
attacker.succeed("curl -v http://walled/ >&2")
|
||||||
|
|
||||||
# Check whether activation of a new configuration reloads the firewall.
|
# Check whether activation of a new configuration reloads the firewall.
|
||||||
walled.succeed(
|
walled.succeed(
|
||||||
"${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF firewall.service"
|
"${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF ${unit}.service"
|
||||||
)
|
)
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,14 +3,16 @@
|
||||||
# client on the inside network, a server on the outside network, and a
|
# client on the inside network, a server on the outside network, and a
|
||||||
# router connected to both that performs Network Address Translation
|
# router connected to both that performs Network Address Translation
|
||||||
# for the client.
|
# for the client.
|
||||||
import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false, ... }:
|
import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false, nftables ? false, ... }:
|
||||||
let
|
let
|
||||||
unit = if withFirewall then "firewall" else "nat";
|
unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat");
|
||||||
|
|
||||||
routerBase =
|
routerBase =
|
||||||
lib.mkMerge [
|
lib.mkMerge [
|
||||||
{ virtualisation.vlans = [ 2 1 ];
|
{ virtualisation.vlans = [ 2 1 ];
|
||||||
networking.firewall.enable = withFirewall;
|
networking.firewall.enable = withFirewall;
|
||||||
|
networking.firewall.filterForward = nftables;
|
||||||
|
networking.nftables.enable = nftables;
|
||||||
networking.nat.internalIPs = [ "192.168.1.0/24" ];
|
networking.nat.internalIPs = [ "192.168.1.0/24" ];
|
||||||
networking.nat.externalInterface = "eth1";
|
networking.nat.externalInterface = "eth1";
|
||||||
}
|
}
|
||||||
|
@ -21,7 +23,8 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
name = "nat" + (if withFirewall then "WithFirewall" else "Standalone")
|
name = "nat" + (lib.optionalString nftables "Nftables")
|
||||||
|
+ (if withFirewall then "WithFirewall" else "Standalone")
|
||||||
+ (lib.optionalString withConntrackHelpers "withConntrackHelpers");
|
+ (lib.optionalString withConntrackHelpers "withConntrackHelpers");
|
||||||
meta = with pkgs.lib.maintainers; {
|
meta = with pkgs.lib.maintainers; {
|
||||||
maintainers = [ eelco rob ];
|
maintainers = [ eelco rob ];
|
||||||
|
@ -34,6 +37,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
|
||||||
{ virtualisation.vlans = [ 1 ];
|
{ virtualisation.vlans = [ 1 ];
|
||||||
networking.defaultGateway =
|
networking.defaultGateway =
|
||||||
(pkgs.lib.head nodes.router.config.networking.interfaces.eth2.ipv4.addresses).address;
|
(pkgs.lib.head nodes.router.config.networking.interfaces.eth2.ipv4.addresses).address;
|
||||||
|
networking.nftables.enable = nftables;
|
||||||
}
|
}
|
||||||
(lib.optionalAttrs withConntrackHelpers {
|
(lib.optionalAttrs withConntrackHelpers {
|
||||||
networking.firewall.connectionTrackingModules = [ "ftp" ];
|
networking.firewall.connectionTrackingModules = [ "ftp" ];
|
||||||
|
@ -111,7 +115,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
|
||||||
# FIXME: this should not be necessary, but nat.service is not started because
|
# FIXME: this should not be necessary, but nat.service is not started because
|
||||||
# network.target is not triggered
|
# network.target is not triggered
|
||||||
# (https://github.com/NixOS/nixpkgs/issues/16230#issuecomment-226408359)
|
# (https://github.com/NixOS/nixpkgs/issues/16230#issuecomment-226408359)
|
||||||
${lib.optionalString (!withFirewall) ''
|
${lib.optionalString (!withFirewall && !nftables) ''
|
||||||
router.succeed("systemctl start nat.service")
|
router.succeed("systemctl start nat.service")
|
||||||
''}
|
''}
|
||||||
client.succeed("curl --fail http://server/ >&2")
|
client.succeed("curl --fail http://server/ >&2")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue