mirror of
https://github.com/ilyakooo0/nixpkgs.git
synced 2024-09-11 07:15:54 +03:00
nixos/{firewall, nat}: add a nftables based implementation
This commit is contained in:
parent
2379de680d
commit
a43c7b2a70
@ -303,6 +303,13 @@
|
||||
the Nix store.
|
||||
</para>
|
||||
</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>
|
||||
<para>
|
||||
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.
|
||||
|
||||
- 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 `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/fireqos.nix
|
||||
./services/networking/firewall.nix
|
||||
./services/networking/firewall-iptables.nix
|
||||
./services/networking/firewall-nftables.nix
|
||||
./services/networking/flannel.nix
|
||||
./services/networking/freenet.nix
|
||||
./services/networking/freeradius.nix
|
||||
@ -891,6 +893,8 @@
|
||||
./services/networking/namecoind.nix
|
||||
./services/networking/nar-serve.nix
|
||||
./services/networking/nat.nix
|
||||
./services/networking/nat-iptables.nix
|
||||
./services/networking/nat-nftables.nix
|
||||
./services/networking/nats.nix
|
||||
./services/networking/nbd.nix
|
||||
./services/networking/ncdns.nix
|
||||
|
@ -53,13 +53,18 @@ in {
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
|
||||
allowedUDPPorts = [ 9003 ];
|
||||
extraCommands = ''
|
||||
extraCommands = optionalString (!config.networking.nftables.enable) ''
|
||||
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 -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 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; }
|
||||
];
|
||||
allowedUDPPorts = [ 9003 ];
|
||||
extraCommands = ''
|
||||
extraCommands = optionalString (!config.networking.nftables.enable) ''
|
||||
## IGMP / Broadcast ##
|
||||
iptables -A INPUT -s 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 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, ... }:
|
||||
|
||||
with lib;
|
||||
@ -38,216 +6,6 @@ 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}";
|
||||
|
||||
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 =
|
||||
ports: lib.unique (builtins.sort builtins.lessThan ports);
|
||||
|
||||
@ -257,8 +15,7 @@ let
|
||||
default = [ ];
|
||||
apply = canonicalizePortList;
|
||||
example = [ 22 80 ];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
List of TCP ports on which incoming connections are
|
||||
accepted.
|
||||
'';
|
||||
@ -268,8 +25,7 @@ let
|
||||
type = types.listOf (types.attrsOf types.port);
|
||||
default = [ ];
|
||||
example = [{ from = 8999; to = 9003; }];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
A range of TCP ports on which incoming connections are
|
||||
accepted.
|
||||
'';
|
||||
@ -280,8 +36,7 @@ let
|
||||
default = [ ];
|
||||
apply = canonicalizePortList;
|
||||
example = [ 53 ];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
List of open UDP ports.
|
||||
'';
|
||||
};
|
||||
@ -290,8 +45,7 @@ let
|
||||
type = types.listOf (types.attrsOf types.port);
|
||||
default = [ ];
|
||||
example = [{ from = 60000; to = 61000; }];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Range of open UDP ports.
|
||||
'';
|
||||
};
|
||||
@ -301,39 +55,33 @@ in
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
networking.firewall = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Whether to enable the firewall. This is a simple stateful
|
||||
firewall that blocks connection attempts to unauthorised TCP
|
||||
or UDP ports on this machine. It does not affect packet
|
||||
forwarding.
|
||||
or UDP ports on this machine.
|
||||
'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.iptables;
|
||||
defaultText = literalExpression "pkgs.iptables";
|
||||
default = if config.networking.nftables.enable then pkgs.nftables else pkgs.iptables;
|
||||
defaultText = literalExpression ''if config.networking.nftables.enable then "pkgs.nftables" else "pkgs.iptables"'';
|
||||
example = literalExpression "pkgs.iptables-legacy";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
The iptables package to use for running the firewall service.
|
||||
description = lib.mdDoc ''
|
||||
The package to use for running the firewall service.
|
||||
'';
|
||||
};
|
||||
|
||||
logRefusedConnections = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Whether to log rejected or dropped incoming connections.
|
||||
Note: The logs are found in the kernel logs, i.e. dmesg
|
||||
or journalctl -k.
|
||||
@ -343,8 +91,7 @@ in
|
||||
logRefusedPackets = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Whether to log all rejected or dropped incoming packets.
|
||||
This tends to give a lot of log messages, so it's mostly
|
||||
useful for debugging.
|
||||
@ -356,8 +103,7 @@ in
|
||||
logRefusedUnicastsOnly = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
If {option}`networking.firewall.logRefusedPackets`
|
||||
and this option are enabled, then only log packets
|
||||
specifically directed at this machine, i.e., not broadcasts
|
||||
@ -368,8 +114,7 @@ in
|
||||
rejectPackets = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
If set, refused packets are rejected rather than dropped
|
||||
(ignored). This means that an ICMP "port unreachable" error
|
||||
message is sent back to the client (or a TCP RST packet in
|
||||
@ -382,8 +127,7 @@ in
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "enp0s2" ];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Traffic coming in from these interfaces will be accepted
|
||||
unconditionally. Traffic from the loopback (lo) interface
|
||||
will always be accepted.
|
||||
@ -393,8 +137,7 @@ in
|
||||
allowPing = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Whether to respond to incoming ICMPv4 echo requests
|
||||
("pings"). ICMPv6 pings are always allowed because the
|
||||
larger address space of IPv6 makes network scanning much
|
||||
@ -406,21 +149,23 @@ in
|
||||
type = types.nullOr (types.separatedString " ");
|
||||
default = null;
|
||||
example = "--limit 1/minute --limit-burst 5";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
If pings are allowed, this allows setting rate limits
|
||||
on them. If non-null, this option should be in the form of
|
||||
flags like "--limit 1/minute --limit-burst 5"
|
||||
description = lib.mdDoc ''
|
||||
If pings are allowed, this allows setting rate limits on them.
|
||||
|
||||
For the iptables based firewall, it should be set like
|
||||
"--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 {
|
||||
type = types.either types.bool (types.enum [ "strict" "loose" ]);
|
||||
default = kernelHasRPFilter;
|
||||
defaultText = literalMD "`true` if supported by the chosen kernel";
|
||||
default = true;
|
||||
defaultText = literalMD "`true` except if the iptables based firewall is in use and the kernel lacks rpfilter support";
|
||||
example = "loose";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Performs a reverse path filter test on a packet. If a reply
|
||||
to the packet would not be sent via the same interface that
|
||||
the packet arrived on, it is refused.
|
||||
@ -431,27 +176,34 @@ in
|
||||
|
||||
This option can be either true (or "strict"), "loose" (only
|
||||
drop the packet if the source address is not reachable via any
|
||||
interface) or false. Defaults to the value of
|
||||
kernelHasRPFilter.
|
||||
interface) or false.
|
||||
'';
|
||||
};
|
||||
|
||||
logReversePathDrops = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Logs dropped packets failing the reverse path filter test if
|
||||
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 {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
List of connection-tracking helpers that are auto-loaded.
|
||||
The complete list of possible values is given in the example.
|
||||
|
||||
@ -470,8 +222,7 @@ in
|
||||
autoLoadConntrackHelpers = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Whether to auto-load connection-tracking helpers.
|
||||
See the description at networking.firewall.connectionTrackingModules
|
||||
|
||||
@ -479,62 +230,47 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
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 {
|
||||
type = types.listOf types.package;
|
||||
default = [ ];
|
||||
example = literalExpression "[ pkgs.ipset ]";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Additional packages to be included in the environment of the system
|
||||
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 {
|
||||
default = { };
|
||||
type = with types; attrsOf (submodule [{ options = commonOptions; }]);
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
# FIXME: Maybe if `enable' is false, the firewall should still be
|
||||
# built but not started by default?
|
||||
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" ];
|
||||
|
||||
environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
|
||||
@ -545,40 +281,6 @@ in
|
||||
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,136 +7,19 @@
|
||||
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
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
networking.nat.enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Whether to enable Network Address Translation (NAT).
|
||||
'';
|
||||
};
|
||||
@ -144,8 +27,7 @@ in
|
||||
networking.nat.enableIPv6 = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
Whether to enable IPv6 NAT.
|
||||
'';
|
||||
};
|
||||
@ -154,8 +36,7 @@ in
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "eth0" ];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
The interfaces for which to perform NAT. Packets coming from
|
||||
these interface and destined for the external interface will
|
||||
be rewritten.
|
||||
@ -166,8 +47,7 @@ in
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "192.168.1.0/24" ];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
The IP address ranges for which to perform NAT. Packets
|
||||
coming from these addresses (on any interface) and destined
|
||||
for the external interface will be rewritten.
|
||||
@ -178,8 +58,7 @@ in
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "fc00::/64" ];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
The IPv6 address ranges for which to perform NAT. Packets
|
||||
coming from these addresses (on any interface) and destined
|
||||
for the external interface will be rewritten.
|
||||
@ -190,8 +69,7 @@ in
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "eth1";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
The name of the external network interface.
|
||||
'';
|
||||
};
|
||||
@ -200,8 +78,7 @@ in
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "203.0.113.123";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
The public IP address to which packets from the local
|
||||
network are to be rewritten. If this is left empty, the
|
||||
IP address associated with the external interface will be
|
||||
@ -213,8 +90,7 @@ in
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "2001:dc0:2001:11::175";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
The public IPv6 address to which packets from the local
|
||||
network are to be rewritten. If this is left empty, the
|
||||
IP address associated with the external interface will be
|
||||
@ -257,8 +133,7 @@ in
|
||||
{ sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
|
||||
{ sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
|
||||
];
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
List of forwarded ports from the external interface to
|
||||
internal destinations by using DNAT. Destination can be
|
||||
IPv6 if IPv6 NAT is enabled.
|
||||
@ -269,52 +144,28 @@ in
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "10.0.0.1";
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
description = lib.mdDoc ''
|
||||
The local IP address to which all traffic that does not match any
|
||||
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 = mkMerge [
|
||||
{ networking.firewall.extraCommands = mkBefore flushNat; }
|
||||
(mkIf config.networking.nat.enable {
|
||||
config = mkIf config.networking.nat.enable {
|
||||
|
||||
assertions = [
|
||||
{ assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
|
||||
{
|
||||
assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
|
||||
message = "networking.nat.enableIPv6 requires networking.enableIPv6";
|
||||
}
|
||||
{ assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
|
||||
{
|
||||
assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
|
||||
message = "networking.nat.dmzHost requires networking.nat.externalInterface";
|
||||
}
|
||||
{ assertion = (cfg.forwardPorts != []) -> (cfg.externalInterface != null);
|
||||
{
|
||||
assertion = (cfg.forwardPorts != [ ]) -> (cfg.externalInterface != null);
|
||||
message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
|
||||
}
|
||||
];
|
||||
@ -341,27 +192,5 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
Whether to enable nftables. 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.
|
||||
Whether to enable nftables and use nftables based firewall if enabled.
|
||||
nftables is a Linux-based packet filtering framework intended to
|
||||
replace frameworks like iptables.
|
||||
|
||||
Note that if you have Docker enabled you will not be able to use
|
||||
nftables without intervention. Docker uses iptables internally to
|
||||
@ -79,19 +77,17 @@ in
|
||||
lib.mdDoc ''
|
||||
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.
|
||||
This option conflicts with rulesetFile.
|
||||
'';
|
||||
};
|
||||
networking.nftables.rulesetFile = mkOption {
|
||||
type = types.path;
|
||||
default = pkgs.writeTextFile {
|
||||
name = "nftables-rules";
|
||||
text = cfg.ruleset;
|
||||
};
|
||||
defaultText = literalMD ''a file with the contents of {option}`networking.nftables.ruleset`'';
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description =
|
||||
lib.mdDoc ''
|
||||
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.
|
||||
This option conflicts with ruleset and nftables based firewall.
|
||||
'';
|
||||
};
|
||||
};
|
||||
@ -99,10 +95,6 @@ in
|
||||
###### implementation
|
||||
|
||||
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" ];
|
||||
environment.systemPackages = [ pkgs.nftables ];
|
||||
networking.networkmanager.firewallBackend = mkDefault "nftables";
|
||||
@ -116,7 +108,9 @@ in
|
||||
rulesScript = pkgs.writeScript "nftables-rules" ''
|
||||
#! ${pkgs.nftables}/bin/nft -f
|
||||
flush ruleset
|
||||
${if cfg.rulesetFile != null then ''
|
||||
include "${cfg.rulesetFile}"
|
||||
'' else cfg.ruleset}
|
||||
'';
|
||||
in {
|
||||
Type = "oneshot";
|
||||
|
@ -211,7 +211,8 @@ in {
|
||||
firefox-esr = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job
|
||||
firefox-esr-102 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-102; };
|
||||
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 {};
|
||||
flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
|
||||
fluentd = handleTest ./fluentd.nix {};
|
||||
@ -412,6 +413,9 @@ in {
|
||||
nat.firewall = handleTest ./nat.nix { withFirewall = true; };
|
||||
nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; };
|
||||
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 {};
|
||||
navidrome = handleTest ./navidrome.nix {};
|
||||
nbd = handleTest ./nbd.nix {};
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Test the firewall module.
|
||||
|
||||
import ./make-test-python.nix ( { pkgs, ... } : {
|
||||
name = "firewall";
|
||||
import ./make-test-python.nix ( { pkgs, nftables, ... } : {
|
||||
name = "firewall" + pkgs.lib.optionalString nftables "-nftables";
|
||||
meta = with pkgs.lib.maintainers; {
|
||||
maintainers = [ eelco ];
|
||||
};
|
||||
@ -11,6 +11,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||
{ ... }:
|
||||
{ networking.firewall.enable = true;
|
||||
networking.firewall.logRefusedPackets = true;
|
||||
networking.nftables.enable = nftables;
|
||||
services.httpd.enable = true;
|
||||
services.httpd.adminAddr = "foo@example.org";
|
||||
};
|
||||
@ -23,6 +24,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||
{ ... }:
|
||||
{ networking.firewall.enable = true;
|
||||
networking.firewall.rejectPackets = true;
|
||||
networking.nftables.enable = nftables;
|
||||
};
|
||||
|
||||
attacker =
|
||||
@ -35,10 +37,11 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||
|
||||
testScript = { nodes, ... }: let
|
||||
newSystem = nodes.walled2.config.system.build.toplevel;
|
||||
unit = if nftables then "nftables" else "firewall";
|
||||
in ''
|
||||
start_all()
|
||||
|
||||
walled.wait_for_unit("firewall")
|
||||
walled.wait_for_unit("${unit}")
|
||||
walled.wait_for_unit("httpd")
|
||||
attacker.wait_for_unit("network.target")
|
||||
|
||||
@ -54,12 +57,12 @@ import ./make-test-python.nix ( { pkgs, ... } : {
|
||||
walled.succeed("ping -c 1 attacker >&2")
|
||||
|
||||
# If we stop the firewall, then connections should succeed.
|
||||
walled.stop_job("firewall")
|
||||
walled.stop_job("${unit}")
|
||||
attacker.succeed("curl -v http://walled/ >&2")
|
||||
|
||||
# Check whether activation of a new configuration reloads the firewall.
|
||||
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
|
||||
# router connected to both that performs Network Address Translation
|
||||
# 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
|
||||
unit = if withFirewall then "firewall" else "nat";
|
||||
unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat");
|
||||
|
||||
routerBase =
|
||||
lib.mkMerge [
|
||||
{ virtualisation.vlans = [ 2 1 ];
|
||||
networking.firewall.enable = withFirewall;
|
||||
networking.firewall.filterForward = nftables;
|
||||
networking.nftables.enable = nftables;
|
||||
networking.nat.internalIPs = [ "192.168.1.0/24" ];
|
||||
networking.nat.externalInterface = "eth1";
|
||||
}
|
||||
@ -21,7 +23,8 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
|
||||
];
|
||||
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");
|
||||
meta = with pkgs.lib.maintainers; {
|
||||
maintainers = [ eelco rob ];
|
||||
@ -34,6 +37,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
|
||||
{ virtualisation.vlans = [ 1 ];
|
||||
networking.defaultGateway =
|
||||
(pkgs.lib.head nodes.router.config.networking.interfaces.eth2.ipv4.addresses).address;
|
||||
networking.nftables.enable = nftables;
|
||||
}
|
||||
(lib.optionalAttrs withConntrackHelpers {
|
||||
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
|
||||
# network.target is not triggered
|
||||
# (https://github.com/NixOS/nixpkgs/issues/16230#issuecomment-226408359)
|
||||
${lib.optionalString (!withFirewall) ''
|
||||
${lib.optionalString (!withFirewall && !nftables) ''
|
||||
router.succeed("systemctl start nat.service")
|
||||
''}
|
||||
client.succeed("curl --fail http://server/ >&2")
|
||||
|
Loading…
Reference in New Issue
Block a user