Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions modules/services/iptables/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# iptables

Service module for [iptables](https://www.netfilter.org/projects/iptables/), the classic Linux packet filtering framework.

Loads IPv4 and IPv6 rulesets at boot via a finit task and tears them down cleanly on stop. Also supplies a `providers.firewall` implementation, making it a drop-in alternative to the nftables backend for systems that require iptables.

## Basic usage

```nix
{
services.iptables = {
enable = true;
rulesetV4 = ''
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp --dport 22 -j ACCEPT
COMMIT
'';
};
}
```

## Using the firewall provider

When `services.iptables.enable` is true, this module registers itself as the `providers.firewall` backend (at a lower priority than nftables, so nftables wins if both are enabled). You can use the standard firewall provider options to open ports declaratively:

```nix
{
services.iptables.enable = true;
providers.firewall = {
enable = true;
allowedTCPPorts = [ 22 80 443 ];
allowedUDPPorts = [ 53 ];
};
}
```
238 changes: 238 additions & 0 deletions modules/services/iptables/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.iptables;

kernel = config.boot.kernelPackages.kernel;

kernelHasRPFilter =
((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER")
|| (kernel.features.netfilterRPFilter or false);

helpers = ''
# Helper command to manipulate both the IPv4 and IPv6 tables.
ip46tables() {
${cfg.package}/bin/iptables -w "$@"
${cfg.package}/bin/ip6tables -w "$@"
}
'';

acceptAllRules = ''
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
'';

rulesV4 = pkgs.writeText "iptables-rules.v4" cfg.rulesetV4;
rulesV6 = pkgs.writeText "iptables-rules.v6" cfg.rulesetV6;

flushRules = pkgs.writeText "iptables-flush" acceptAllRules;

startScript = pkgs.writeShellScript "iptables-start.sh" ''
${helpers}

${cfg.package}/bin/iptables-restore ${rulesV4}
${cfg.package}/bin/ip6tables-restore ${rulesV6}

# Clean up rpfilter rules
ip46tables -t mangle -D PREROUTING -j finix-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -F finix-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -X finix-fw-rpfilter 2> /dev/null || true

${lib.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 finix-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -A finix-fw-rpfilter -m rpfilter --validmark ${
lib.optionalString (cfg.checkReversePath == "loose") "--loose"
} -j RETURN

# Allows this host to act as a DHCP4 client without first having to use APIPA
${cfg.package}/bin/iptables -w -t mangle -A finix-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN

# Allows this host to act as a DHCPv4 server
${cfg.package}/bin/iptables -w -t mangle -A finix-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN

${lib.optionalString cfg.logReversePathDrops ''
ip46tables -t mangle -A finix-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
''}
ip46tables -t mangle -A finix-fw-rpfilter -j DROP

ip46tables -t mangle -A PREROUTING -j finix-fw-rpfilter
''}

${cfg.extraCommands}
'';

stopScript = pkgs.writeShellScript "iptables-stop.sh" ''
${helpers}

${cfg.package}/bin/iptables-restore ${flushRules}
${cfg.package}/bin/ip6tables-restore ${flushRules}

ip46tables -t mangle -D PREROUTING -j finix-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -F finix-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -X finix-fw-rpfilter 2> /dev/null || true

${cfg.extraStopCommands}
'';
in
{
imports = [
./providers.firewall.nix
];

options.services.iptables = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable [iptables](${pkgs.iptables.meta.homepage}) as a system service.
'';
};

package = lib.mkOption {
type = lib.types.package;
default = pkgs.iptables;
defaultText = lib.literalExpression "pkgs.iptables";
description = ''
The package to use for `iptables`.
'';
};

extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression "[ pkgs.ipset ]";
description = ''
Additional packages to be included in the system environment alongside
the iptables package.
'';
};

trustedInterfaces = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "lo" ];
example = [
"lo"
"enp0s2"
];
description = ''
Traffic from these interfaces will be accepted unconditionally
by the {option}`providers.firewall` implementation. The loopback
interface is trusted by default.
'';
};

allowPing = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether the {option}`providers.firewall` implementation responds
to incoming ICMPv4 echo requests ("pings").
'';
};

rulesetV4 = lib.mkOption {
type = lib.types.lines;
default = acceptAllRules;
description = ''
The IPv4 ruleset, in {manpage}`iptables-restore(8)` format.
'';
};

rulesetV6 = lib.mkOption {
type = lib.types.lines;
default = acceptAllRules;
description = ''
The IPv6 ruleset, in {manpage}`ip6tables-restore(8)` format.
'';
};

checkReversePath = lib.mkOption {
type = lib.types.either lib.types.bool (
lib.types.enum [
"strict"
"loose"
]
);
default = false;
example = "loose";
description = ''
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.

If using asymmetric routing or other complicated routing, set
this option to loose mode or disable it and setup your own
counter-measures.

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.
'';
};

logReversePathDrops = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Logs dropped packets failing the reverse path filter test if
the option {option}`checkReversePath` is enabled.
'';
};

extraCommands = lib.mkOption {
type = lib.types.lines;
default = "";
example = "iptables -A INPUT -p icmp -j ACCEPT";
description = ''
Additional shell commands executed as part of the iptables
initialisation script, after the rulesets have been loaded.
An `ip46tables` helper is available to run a command against
both the IPv4 and IPv6 tables.
'';
};

extraStopCommands = lib.mkOption {
type = lib.types.lines;
default = "";
example = "iptables -P INPUT ACCEPT";
description = ''
Additional shell commands executed as part of the iptables
shutdown script, after the rulesets have been reset.
An `ip46tables` helper is available to run a command against
both the IPv4 and IPv6 tables.
'';
};
};

config = lib.mkIf cfg.enable {
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";
}
];

environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;

finit.tasks.iptables = {
runlevels = "23";
conditions = "service/syslogd/ready";
command = startScript;
post = stopScript;
log = true;
remain = true;
};
};
}
91 changes: 91 additions & 0 deletions modules/services/iptables/providers.firewall.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
config,
lib,
...
}:
let
cfg = config.providers.firewall;
svcCfg = config.services.iptables;

refuseRules =
if cfg.rejectPackets then
''
-A finix-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
-A finix-fw-refuse -j REJECT
''
else
''
-A finix-fw-refuse -j DROP
'';

commonRules = ''
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-N finix-fw-accept
-N finix-fw-refuse
-N finix-fw-log-refuse
-N finix-fw
-A finix-fw-accept -j ACCEPT
${refuseRules}
-A finix-fw-log-refuse -j finix-fw-refuse
${lib.concatMapStrings (iface: ''
-A finix-fw -i ${iface} -j finix-fw-accept
'') svcCfg.trustedInterfaces}
-A finix-fw -m conntrack --ctstate ESTABLISHED,RELATED -j finix-fw-accept
${lib.concatMapStrings (port: ''
-A finix-fw -p tcp --dport ${toString port} -j finix-fw-accept
'') cfg.allowedTCPPorts}
${lib.concatMapStrings ({ from, to }: ''
-A finix-fw -p tcp --dport ${toString from}:${toString to} -j finix-fw-accept
'') cfg.allowedTCPPortRanges}
${lib.concatMapStrings (port: ''
-A finix-fw -p udp --dport ${toString port} -j finix-fw-accept
'') cfg.allowedUDPPorts}
${lib.concatMapStrings ({ from, to }: ''
-A finix-fw -p udp --dport ${toString from}:${toString to} -j finix-fw-accept
'') cfg.allowedUDPPortRanges}
'';
in
{
options.providers.firewall = {
backend = lib.mkOption {
type = lib.types.enum [ "iptables" ];
};
};

config = lib.mkMerge [
(lib.mkIf config.services.iptables.enable {
# this module supplies an implementation for `providers.firewall`.
# weaker than the nftables backend's mkDefault so that enabling both
# services does not conflict: nftables wins unless the backend is set
# explicitly
providers.firewall.backend = lib.mkOverride 1250 "iptables";
})

(lib.mkIf (cfg.enable && cfg.backend == "iptables") {
services.iptables.rulesetV4 =
commonRules
+ lib.optionalString svcCfg.allowPing ''
-A finix-fw -p icmp --icmp-type echo-request -j finix-fw-accept
''
+ ''
-A finix-fw -j finix-fw-log-refuse
-A INPUT -j finix-fw
COMMIT
'';

services.iptables.rulesetV6 =
commonRules
+ lib.optionalString svcCfg.allowPing ''
-A finix-fw -p icmpv6 --icmpv6-type echo-request -j finix-fw-accept
''
+ ''
-A finix-fw -j finix-fw-log-refuse
-A INPUT -j finix-fw
COMMIT
'';
})
];
}
Loading