[PATCH] fw4: add masquerade-prefix snat type

Jonas Lochmann openwrt at jonaslochmann.de
Sun Jan 12 05:16:35 PST 2025


OpenWrt supports requesting IPv6 network prefixes using DHCP. However,
the existing masquerade option delegates the rewriting to nftables that
knows the ip address used by the router itself but not the prefixes that
are only known by netifd and OpenWrt services that use its data.

The masquerade-prefix does the following:

- keep the source address for IPv6 link local addresses
- keep the source address for IPs that are assigned to the interface
- keep the source address if they belong to an assigned IPv6 prefix
- otherwise rewrite the prefix to the shortest assigned prefix
- if there is no assigned prefix: use the regular masquerade

This is useful when devices in the network use a private source address
or a public source address that belongs to another network/uplink. In
simple scenarios, addresses from the current uplink prefix are used by all
devices. This stops working if there are multiple uplinks and the
(OpenWrt) router selects the uplink, e.g. with the help of the mwan3
package. While the traditional masquerade could be used in scenarios like
this, all devices would use the same outgoing IP address in case of
rewriting. Individual snat rules could be used only if all devices are
known when creating the configuration and the assigned prefix is static.

Currently, there are workarounds described in the wiki [1]. They all share
the limitation that they are not well integrated into the firewall. That
is, they are just hooks that are called whenever the firewall configuration
was replaced. Due to that, they do not benefit from the atomic
configuration updates that nftables provides - there is always a short
moment after updating the configuration during that this rules do not take
effect. My previous own solution [2] used other hooks (technically not
necassary) and uses an own nftables table so that it is unaffected
whenever firewall4 sends its new configuration to nftables. This is poorly
integrated with firewall4 and to disable it users need to remove the files
and know nftables well enough (or reboot the device) to stop the effect.

Another limitation of the approaches from the wiki is that they use
"snat ip6 prefix to ip6 saddr". "The optional prefix keyword allows to
map to map n source addresses to n destination addresses." [3]. Altough
not clearly stated, it looks like this needs original and rewritten
prefixes of the same size. Moreover, this requires determining the
possible original source IPs while the suggested implementation does
not need to handle them at all.

[1] https://openwrt.org/docs/guide-user/firewall/fw3_configurations/
    fw3_nat?rev=1736642299#ipv6_npt
[2] https://forum.openwrt.org/t/multiwan-with-ipv6/136965/12
[3] https://www.netfilter.org/projects/nftables/manpage.html

Signed-off-by: Jonas Lochmann <openwrt at jonaslochmann.de>
---
 root/usr/share/ucode/fw4.uc | 148 ++++++++++++++++++++++++++++++++++--
 1 file changed, 143 insertions(+), 5 deletions(-)

diff --git a/root/usr/share/ucode/fw4.uc b/root/usr/share/ucode/fw4.uc
index 2d77146..0c1cd78 100644
--- a/root/usr/share/ucode/fw4.uc
+++ b/root/usr/share/ucode/fw4.uc
@@ -192,7 +192,7 @@ const dscp_classes = {
 	"EF": 0x2e
 };
 
-function to_mask(bits, v6) {
+function to_mask_raw(bits, v6) {
 	let m = [], n = false;
 
 	if (bits < 0) {
@@ -209,7 +209,19 @@ function to_mask(bits, v6) {
 		bits -= b;
 	}
 
-	return arrtoip(m);
+	return m;
+}
+
+function to_mask(bits, v6) {
+	return arrtoip(to_mask_raw(bits, v6));
+}
+
+function to_inverse_mask(bits, v6) {
+	let mask = to_mask_raw(bits, v6);
+
+	mask = map(mask, (v) => v ^ 0xff);
+
+	return arrtoip(mask);
 }
 
 function to_bits(mask) {
@@ -658,6 +670,16 @@ return {
 					}
 				}
 
+				if (type(ifc["ipv6-prefix"]) == "array") {
+					for (let prefix in ifc["ipv6-prefix"]) {
+						push(net.ip6prefixes ||= [], {
+							addr: prefix.address,
+							mask: to_mask(prefix.mask, true),
+							bits: prefix.mask
+						});
+					}
+				}
+
 				if (type(ifc.data?.firewall) == "array") {
 					let n = 0;
 
@@ -1475,6 +1497,7 @@ return {
 			"dnat",
 			"snat",
 			"masquerade",
+			"masquerade-prefix",
 			"accept",
 			"reject",
 			"drop"
@@ -3071,7 +3094,7 @@ return {
 			return;
 		}
 
-		if (!(snat.target in ["accept", "snat", "masquerade"])) {
+		if (!(snat.target in ["accept", "snat", "masquerade", "masquerade-prefix"])) {
 			this.warn_section(data, "has invalid target specified, defaulting to masquerade");
 			snat.target = "masquerade";
 		}
@@ -3108,6 +3131,121 @@ return {
 			delete snat.log;
 		}
 
+		let add_rule_3 = (n) => {
+			push(this.state.redirects ||= [], n);
+		};
+
+		let add_rule_2 = (n) => {
+			if (n.target == "masquerade-prefix") {
+				let output_device_filter = null;
+
+				if (type(n.device) == "string") {
+					output_device_filter = [n.device];
+				} else if (n.src != null && !n.src.any) {
+					// avoid creating rules for interfaces in other zones
+					let zone = n.src.zone;
+
+					let simple = true;
+
+					simple &= zone.device == null || length(zone.device) == 0;
+					simple &= zone.subnet == null || length(zone.subnet) == 0;
+
+					if (simple) {
+						output_device_filter = zone.related_physdevs;
+					}
+				}
+
+				// accept (do nothing) for link local addresses
+				// otherwise, we will get DHCP issues and thus no prefix
+				if (n.family == 6) {
+					let saddrs_masked = slice(n.saddrs_masked || []);
+
+					push(saddrs_masked, {
+						addr: 'fe80::',
+						mask: to_mask(10, true),
+						invert: false
+					});
+
+					add_rule_3({
+						...n,
+						saddrs_masked: saddrs_masked,
+						target: "accept"
+					});
+				}
+
+				for (let name, net in this.state.networks) {
+					if (output_device_filter == null || index(output_device_filter, net.device) != -1) {
+						// accept (do nothing) if the src ip is ok
+						for (let addr in net.ipaddrs) {
+							if (addr.family == n.family) {
+								let saddrs_masked = slice(n.saddrs_masked || []);
+
+								push(saddrs_masked, {
+									addr: addr.addr,
+									// only permit the single ip itself
+									mask: n.family == 4 ? to_mask(32, false) : to_mask(128, true),
+									invert: false
+								});
+
+								add_rule_3({
+									...n,
+									device: net.device,
+									saddrs_masked: saddrs_masked,
+									target: "accept"
+								});
+							}
+						}
+
+						if (n.family == 6) {
+							// accept (do nothing) if the src ip belongs to a prefix
+							for (let prefix in net.ip6prefixes) {
+								let saddrs_masked = slice(n.saddrs_masked || []);
+
+								push(saddrs_masked, {
+									addr: prefix.addr,
+									mask: prefix.mask,
+									invert: false
+								});
+
+								add_rule_3({
+									...n,
+									device: net.device,
+									saddrs_masked: saddrs_masked,
+									target: "accept"
+								});
+							}
+
+							// otherwise rewrite the src ip
+							let best_prefix = null;
+
+							for (let prefix in net.ip6prefixes) {
+								if (best_prefix == null || best_prefix.bits > prefix.bits)
+									best_prefix = prefix;
+							}
+
+							// if there is no prefix, then the masquerade fallback will resolve this
+							if (best_prefix != null) {
+								let base_addr = apply_mask(best_prefix.addr, best_prefix.mask);
+								let suffix_mask = to_inverse_mask(best_prefix.bits, true);
+								let target = "snat ip6 to ip6 saddr and " + suffix_mask + " or " + base_addr;
+
+								add_rule_3({
+									...n,
+									device: net.device,
+									target: target
+								});
+							}
+						}
+					}
+				}
+
+				// use masquerade as fallback
+				add_rule_3({ ...n, target: "masquerade" });
+			} else {
+				add_rule_3(n);
+			}
+		};
+
 		let add_rule = (family, proto, saddrs, daddrs, raddrs, sport, dport, rport, snat) => {
 			let n = {
 				...snat,
@@ -3133,7 +3271,7 @@ return {
 				chain: snat.src?.zone ? `srcnat_${snat.src.zone.name}` : "srcnat"
 			};
 
-			push(this.state.redirects ||= [], n);
+			add_rule_2(n);
 		};
 
 		for (let proto in snat.proto) {
@@ -3182,7 +3320,7 @@ return {
 			}
 
 			/* check if there's no AF specific bits, in this case we can do an AF agnostic rule */
-			if (!family && !length(sip[0]) && !length(sip[1]) && !length(dip[0]) && !length(dip[1]) && !length(rip[0]) && !length(rip[1])) {
+			if (!family && !length(sip[0]) && !length(sip[1]) && !length(dip[0]) && !length(dip[1]) && !length(rip[0]) && !length(rip[1]) && snat.target != "masquerade-prefix") {
 				add_rule(0, proto, [], [], null, sport, dport, rport, snat);
 			}
 
-- 
2.39.5




More information about the openwrt-devel mailing list