Add a generic NAT table: port forward, source NAT, masquerade
router.firewall.nat replaces the dnat/toPort/hairpin fields of the filter rules. Filter rules gain natOnly, and may use wan with log and rate limit in the web UI. The Firewall page edits the NAT table and can add the matching allow rule. Settings schema 1 (migrations/1.jq) converts old forwards. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
+47
-20
@@ -121,9 +121,7 @@ let
|
||||
label = "${r.from}->${r.to}";
|
||||
fromDevs = if r.from == "wan" then uplinks else zoneDevices r.from;
|
||||
toDevs = zoneDevices r.to;
|
||||
# After DNAT the packet carries the translated port.
|
||||
fwdPorts = if r.dnat && r.toPort != null then [ r.toPort ] else r.ports;
|
||||
base = "iifname ${set fromDevs} oifname ${set toDevs} ${daddrOf r}${l4 r.protocol fwdPorts r.sourcePorts}${lib.optionalString r.dnat "ct status dnat "}";
|
||||
base = "iifname ${set fromDevs} oifname ${set toDevs} ${daddrOf r}${l4 r.protocol r.ports r.sourcePorts}${lib.optionalString r.natOnly "ct status dnat "}";
|
||||
logPrefix = what: ''log prefix "router fwd#${id} ${label} ${what}: " '';
|
||||
limitLines =
|
||||
v:
|
||||
@@ -147,32 +145,60 @@ let
|
||||
v:
|
||||
lib.optional r.log "${base}${v.src}ct state new ${logPrefix "accepted"}"
|
||||
++ [ "${base}${v.src}accept # ${label}" ];
|
||||
dnatTarget = "dnat to ${r.destination}${
|
||||
lib.optionalString (r.toPort != null) ":${toString r.toPort}"
|
||||
}";
|
||||
in
|
||||
{
|
||||
# A zone nothing is assigned to yet has no devices; the rule waits for one.
|
||||
forward = lib.optionals (fromDevs != [ ] && toDevs != [ ]) (
|
||||
lib.concatMap (v: limitLines v ++ acceptLines v) (variantsOf r)
|
||||
);
|
||||
prerouting =
|
||||
lib.optionals r.dnat (
|
||||
map (v: "iifname ${set uplinks} ${v.src}${l4 r.protocol r.ports [ ]}${dnatTarget} # ${label}") (
|
||||
variantsOf r
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
# Address translation (router.firewall.nat). Indexed like the rules.
|
||||
natParts =
|
||||
i: n:
|
||||
let
|
||||
label = "nat ${n.type} ${toString n.from}->${n.to}";
|
||||
fromDevs =
|
||||
if n.from == null then
|
||||
[ ]
|
||||
else if n.from == "wan" then
|
||||
uplinks
|
||||
else
|
||||
zoneDevices n.from;
|
||||
toDevs = if n.to == "wan" then uplinks else zoneDevices n.to;
|
||||
iif = lib.optionalString (n.from != null) "iifname ${set fromDevs} ";
|
||||
saddr = lib.optionalString (n.sources != [ ]) "ip saddr ${setOf n.sources} ";
|
||||
daddr = lib.optionalString (n.destination != null) "ip daddr ${n.destination} ";
|
||||
match = "${iif}${saddr}${daddr}${l4 n.protocol n.ports n.sourcePorts}";
|
||||
target = "${n.toAddress}${lib.optionalString (n.toPort != null) ":${toString n.toPort}"}";
|
||||
# Zones with nothing assigned yet have no devices; the rule waits for one.
|
||||
live = (n.from == null || fromDevs != [ ]) && toDevs != [ ];
|
||||
in
|
||||
{
|
||||
prerouting = lib.optionals (live && n.type == "dnat") (
|
||||
[
|
||||
# Without a destination, from the uplink anything that arrives is
|
||||
# translated; from inside only what is addressed to the router.
|
||||
"${match}${
|
||||
lib.optionalString (n.destination == null && n.from != "wan") "fib daddr type local "
|
||||
}dnat to ${target} # ${label}"
|
||||
]
|
||||
# NAT loopback: internal clients addressing the router's public address.
|
||||
++
|
||||
lib.optional (r.dnat && r.hairpin)
|
||||
"iifname ${set internetIfaces} fib daddr type local ip daddr != ${setOf internalAddrs} ${l4 r.protocol r.ports [ ]}${dnatTarget} # hairpin ${label}";
|
||||
hairpinForward = lib.optional (r.dnat && r.hairpin && toDevs != [ ]) (
|
||||
"iifname ${set internetIfaces} oifname ${set toDevs} ip daddr ${r.destination} ${l4 r.protocol fwdPorts [ ]}ct status dnat accept # hairpin ${label}"
|
||||
++ lib.optional n.hairpin "iifname ${set internetIfaces} ${saddr}${daddr}fib daddr type local ip daddr != ${setOf internalAddrs} ${l4 n.protocol n.ports [ ]}dnat to ${target} # hairpin ${label}"
|
||||
);
|
||||
# The hairpin allow: these LANs may reach the host through the router's address.
|
||||
hairpinForward = lib.optional (live && n.type == "dnat" && n.hairpin) "iifname ${set internetIfaces} oifname ${set toDevs} ip daddr ${n.toAddress} ${l4 n.protocol (if n.toPort != null then [ n.toPort ] else n.ports) [ ]}ct status dnat accept # hairpin ${label}";
|
||||
postrouting = lib.optional (live && n.type == "masquerade") "${
|
||||
lib.optionalString (n.from != null) "iifname ${set fromDevs} "
|
||||
}oifname ${set toDevs} ${saddr}${daddr}${l4 n.protocol n.ports n.sourcePorts}masquerade # ${label}"
|
||||
++ lib.optional (
|
||||
live && n.type == "snat"
|
||||
) "${iif}oifname ${set toDevs} ${saddr}${daddr}${l4 n.protocol n.ports n.sourcePorts}snat to ${n.toAddress} # ${label}";
|
||||
};
|
||||
|
||||
parts = lib.imap0 ruleParts cfg.firewall.rules;
|
||||
anyHairpin = lib.any (r: r.dnat && r.hairpin) cfg.firewall.rules;
|
||||
natRules = lib.imap0 natParts cfg.firewall.nat;
|
||||
anyHairpin = lib.any (n: n.type == "dnat" && n.hairpin) cfg.firewall.nat;
|
||||
# Addresses the router itself holds on internal interfaces; anything else
|
||||
# that is local is a public (uplink) address.
|
||||
internalAddrs =
|
||||
@@ -261,7 +287,7 @@ in
|
||||
|
||||
# Zones: free inside a zone, denied between zones unless a rule says so.
|
||||
${lib.concatStringsSep "\n " (
|
||||
intraZone ++ lib.concatMap (p: p.hairpinForward ++ p.forward) parts
|
||||
intraZone ++ lib.concatMap (p: p.hairpinForward) natRules ++ lib.concatMap (p: p.forward) parts
|
||||
)}
|
||||
}
|
||||
'';
|
||||
@@ -272,11 +298,12 @@ in
|
||||
content = ''
|
||||
chain prerouting {
|
||||
type nat hook prerouting priority dstnat; policy accept;
|
||||
${lib.concatStringsSep "\n " (lib.concatMap (p: p.prerouting) parts)}
|
||||
${lib.concatStringsSep "\n " (lib.concatMap (p: p.prerouting) natRules)}
|
||||
}
|
||||
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority srcnat; policy accept;
|
||||
${lib.concatStringsSep "\n " (lib.concatMap (p: p.postrouting) natRules)}
|
||||
oifname "${uplink4}" masquerade
|
||||
${lib.optionalString anyHairpin "ct status dnat iifname ${set memberIfaces} oifname ${set memberIfaces} masquerade # hairpin"}
|
||||
${lib.optionalString (
|
||||
|
||||
Reference in New Issue
Block a user