Add port forwards and WAN-originated firewall rules
Extend router.firewall.rules with a reserved `wan` source zone: DNAT (dnat, toPort), port ranges, source allow-lists, per-source rate limits, logging and optional hairpin NAT. IPv6 is opened only by explicit pinholes. Build-time assertions require WAN rules to name a destination, protocol and ports so a typo cannot expose a whole zone. Covered by the basic/pppoe VM tests and the assertion tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -317,14 +317,18 @@ in
|
||||
description = ''
|
||||
Allow rules between zones, on top of the default (deny between zones).
|
||||
Rules are one-directional; return traffic is allowed by connection
|
||||
tracking. Both ends must be zones of LANs defined in `router.lans`.
|
||||
tracking. `to` must be the zone of a LAN or tunnel; `from` may also be
|
||||
the reserved zone `wan` (traffic arriving on the uplinks), which
|
||||
publishes services to the outside: such rules must name a
|
||||
`destination` and protocol (and ports for tcp/udp), so a typo cannot
|
||||
expose a whole zone.
|
||||
'';
|
||||
type = types.listOf (
|
||||
types.submodule {
|
||||
options = {
|
||||
from = mkOption {
|
||||
type = types.str;
|
||||
description = "Source zone.";
|
||||
description = "Source zone, or `wan`.";
|
||||
};
|
||||
to = mkOption {
|
||||
type = types.str;
|
||||
@@ -340,9 +344,17 @@ in
|
||||
default = "any";
|
||||
};
|
||||
ports = mkOption {
|
||||
type = types.listOf types.port;
|
||||
type = types.listOf (types.either types.port (types.strMatching "[0-9]+-[0-9]+"));
|
||||
default = [ ];
|
||||
description = "Destination ports (tcp/udp only); empty means all ports.";
|
||||
example = [
|
||||
443
|
||||
"50000-50100"
|
||||
];
|
||||
description = ''
|
||||
Destination ports or ranges (tcp/udp only); empty means all
|
||||
ports. With `dnat` these are the ports on the router's
|
||||
public address.
|
||||
'';
|
||||
};
|
||||
destination = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
@@ -350,9 +362,50 @@ in
|
||||
example = "192.168.10.5";
|
||||
description = ''
|
||||
Restrict the rule to an IPv4 address or CIDR inside the
|
||||
destination zone; null means the whole zone.
|
||||
destination zone; null means the whole zone. IPv6 addresses
|
||||
are accepted too (for pinholes) but cannot be checked against
|
||||
the zone, because delegated prefixes are not known at build time.
|
||||
'';
|
||||
};
|
||||
sources = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "198.51.100.0/24" ];
|
||||
description = "Only match traffic from these IPv4/IPv6 addresses or CIDRs; empty means any source.";
|
||||
};
|
||||
dnat = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Port forward: traffic from `wan` to `ports` on the router's
|
||||
public IPv4 address is redirected to `destination` (one IPv4
|
||||
host). The source address is preserved.
|
||||
'';
|
||||
};
|
||||
toPort = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = "Port on `destination` when it differs from the public port (single-port forwards only).";
|
||||
};
|
||||
hairpin = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Let LANs with `internet = true` reach this forward through the
|
||||
router's public address (NAT loopback, source is masqueraded).
|
||||
'';
|
||||
};
|
||||
rateLimit = mkOption {
|
||||
type = types.nullOr (types.strMatching "[0-9]+/(second|minute|hour)");
|
||||
default = null;
|
||||
example = "10/minute";
|
||||
description = "Limit new connections per source address; the excess is dropped.";
|
||||
};
|
||||
log = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Log new connections accepted by this rule, and connections dropped by `rateLimit` (kernel log).";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user