Asking for feedback: [PATCH] fw4: add masquerade-prefix snat type

Jonas Lochmann openwrt at jonaslochmann.de
Thu Feb 27 00:32:25 PST 2025


More than a month has passed without any feedback that helped me. There was one
comment regarding an RFC that this does not implement (because its functionality
and goals are similar but different) and the name of this feature that is
considered misleading. Regarding the second point, there was no feedback suggesting
a better name. In reply to that, I stated why I chose this name and consider it
not misleading for this functionality.

This feature is used in production in one multiwan environment. I want to use it in
another production environment soon - as soon as the second ISP starts providing IPv6
instead of only advertising that dual stack connectivity is always included. Having
this upstream makes it easier for me and others to build multiwan setups with IPv6
that do not map everything to one outgoing IP.

On Sun, Jan 12, 2025 at 02:16:35PM +0100, Jonas Lochmann wrote:
> 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
> 
> 
> _______________________________________________
> openwrt-devel mailing list
> openwrt-devel at lists.openwrt.org
> https://lists.openwrt.org/mailman/listinfo/openwrt-devel



More information about the openwrt-devel mailing list