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:
@@ -506,6 +506,222 @@ let
|
||||
};
|
||||
expect = [ "is not inside any network of zone `s2s`" ];
|
||||
};
|
||||
wanValid = {
|
||||
router = {
|
||||
lans.main = lan { internet = true; };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
dnat = true;
|
||||
toPort = 8081;
|
||||
sources = [ "198.51.100.0/24" ];
|
||||
rateLimit = "3/minute";
|
||||
log = true;
|
||||
hairpin = true;
|
||||
}
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "2001:db8:1::5";
|
||||
protocol = "tcp";
|
||||
ports = [ "9000-9002" ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ ];
|
||||
};
|
||||
wanNoDestination = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = null;
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "must name a destination" ];
|
||||
};
|
||||
wanAnyProtocol = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "any";
|
||||
ports = [ ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "must name a protocol" ];
|
||||
};
|
||||
wanNoPorts = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "must list the ports" ];
|
||||
};
|
||||
toWan = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "main";
|
||||
to = "wan";
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "`wan` cannot be a destination" ];
|
||||
};
|
||||
dnatNeedsHost = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.0/24";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
dnat = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "dnat needs a single IPv4 host" ];
|
||||
};
|
||||
dnatV6Sources = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
dnat = true;
|
||||
sources = [ "2001:db8::/32" ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "its sources must be IPv4" ];
|
||||
};
|
||||
toPortWithoutDnat = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
toPort = 8081;
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "toPort needs dnat" ];
|
||||
};
|
||||
hairpinNoDnat = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
hairpin = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [
|
||||
"hairpin only applies to dnat"
|
||||
"hairpin needs at least one"
|
||||
];
|
||||
};
|
||||
hairpinNoInternet = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
dnat = true;
|
||||
hairpin = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "hairpin needs at least one" ];
|
||||
};
|
||||
badPortRange = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ "9002-9000" ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "out of range" ];
|
||||
};
|
||||
badSource = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "192.168.10.5";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
sources = [ "nonsense" ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "every source must be" ];
|
||||
};
|
||||
badV6Destination = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "wan";
|
||||
to = "main";
|
||||
destination = "2001:db8::zz";
|
||||
protocol = "tcp";
|
||||
ports = [ 8080 ];
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "not a valid IPv6" ];
|
||||
};
|
||||
nonOverlappingLans = {
|
||||
router.lans = {
|
||||
a = lan { };
|
||||
@@ -552,6 +768,7 @@ let
|
||||
"vlansOnOneTrunk"
|
||||
"ruleValid"
|
||||
"wgValid"
|
||||
"wanValid"
|
||||
];
|
||||
|
||||
subnet =
|
||||
|
||||
Reference in New Issue
Block a user