Masquerade NAT firewall rule not working

I'm trying to rewrite an internal port to an external port for some specific devices through the firewall so I can achieve open NAT type on multiple games consoles. Sadly I can not get the masquerading action work whereas SNAT works perfectly fine.

Call of Duty typically uses UDP port 3074 to achieve open NAT type and I have typically let UPnP handle this. When the same game is being played on more than one device at one time port, 3074 can only be network translated to one device simultaneously so one device can always have open NAT leaving other devices on moderate NAT type. UPnP cleverly gets around this by redirecting the internal port to another external port so that no two devices clash on the same port(s) when translating from a private network to the WAN.

I've some what managed to get this working without the need of UPnP using Port Forwards (DNAT) and NAT Rules (SNAT). However, when my public IP address changes the port redirection stops working and I'm back to moderate NAT type.

Port Forward

config redirect
	option src 'wan'
	option target 'DNAT'
	option dest 'lan'
	option dest_port '3074'
	option name 'CoD-PS4'
	option src_dport '3080'
	option dest_ip '192.168.5.11'

When I change the action from SNAT as shown below

NAT Rule (SNAT action)

config nat
	option src_port '3074'
	option name 'CoD-PS4'
	option snat_port '3080'
	option src 'wan'
	option src_ip '192.168.5.11'
	list proto 'udp'
	option target 'SNAT'
	option snat_ip '123.254.123.254'

to Masquerade I still end up with moderate NAT type.

**NAT Rule (Masquerade action)

config nat
	option src_port '3074'
	option name 'CoD-PS4'
	option snat_port '3080'
	option src 'wan'
	option src_ip '192.168.5.11'
	list proto 'udp'
	option target 'MASQUERADE'

N.B If you're wondering where I pulled port 3074 from, this was one of the numbers miniupnpd chose when I simulated two devices (PS3 and PS4 on two different Call of Duty games) trying to NAT on port 3074 at the same time.

2 Likes

whu do you use masquerade ?

change maybe to outbound zone to lan but note sure

You cannot make the masquerading rule work, because it is incompatible with the option "Rewrite Port".
Although LuCI allows you to enter a rewrite port value (when MASQUERADE is selected), the rule won't work, and if you restart the firewall from the command line, you'll see a warning like this:

Warning: Section @nat[x] (Rule Name) must not use 'snat_port' for non-SNAT

So, your only choice is to use SNAT.
Because SNAT requires a static IP address, the rule should be created dynamically, using the current wan address. If the wan IP address changes only after router restart, you could create a custom firewall rule in /etc/firewall.user

. /lib/functions/network.sh
network_flush_cache
network_find_wan NET_IF
network_get_ipaddr NET_ADDR "${NET_IF}"
iptables -t nat -A zone_wan_postrouting -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "${NET_ADDR}":3080

But if the wan IP address changes during normal router operation, you will have to create an additional script, that periodically checks for address changes and replaces the SNAT rule(s) when necessary.

[EDIT:]

Please also add the following /etc/rc.local

sleep 30 && fw3 restart

3 Likes

Sorry for the late reply. I'm been busy with other projects and this slipped my mind.

In regards to creating a script to check for WAN IP address changes I had a look at the OpenWrt network scripts here. Acquiring the IPv4 address worked but sadly the IPv6 address came back blank. Eventually I came up with this:

/etc/retrieve_wan_ip.sh

```
echo -n "Retrieving WAN IPv4 address... " 
WAN_IPv4=$(. /lib/functions/network.sh; network_flush_cache; network_find_wan NET_IF; network_get_ipaddr NET_ADDR "${NET_IF}"; echo "${NET_ADDR}") 
echo "$WAN_IPv4" | tee /tmp/WAN_IPv4 
chmod 777 /tmp/WAN_IPv4 
echo "Done"

echo -n "Retrieving WAN IPv6 address... " 
NET_IF6=wan_6 
WAN_IPv6=$(. /lib/functions/network.sh 
network_flush_cache 
network_get_prefix6 NET_PFX6 "${NET_IF6}" 
echo "${NET_PFX6%/*}") 
echo "$WAN_IPv6" | tee /tmp/WAN_IPv6 
chmod 777 /tmp/WAN_IPv6 
echo "Done" 
exit 0
```

This script dumps the IP addresses into two files under the /tmp directory

  • /tmp/WAN_IPv4
  • /tmp/WAN_IPv6

To keep these two files up-to-date I run a cronjob every hour. The problem I'm having now is getting iptables to read the IP addresses from the files. Any idea how I can do that?

Here is an example how to update one iptables (IPv4) rule when the IP address changes. Multiply it according to your needs.
If you use ip6tables rules, you should change the sed arguments to extract the IP address from the rule (due to the multiple colons in the address).

#!/bin/sh

ipv4=$(cat /tmp/WAN_IPv4) #Get the IP address, stored in /tmp/WAN_IPv4
lastipv4=$(iptables -t nat -L | grep CoD-PS4-UDP | sed 's/.* to\://' | sed 's/\:.*//') #Get the current IP address from the iptables rule

#Compare the addresses and if they do not match, update the iptables rule

if [ "$ipv4" = "$lastipv4" ]; then

        exit 0

else

	rn=$(iptables -t nat -L --line-numbers | grep CoD-PS4-UDP | awk '{ print $1 }')
	iptables -t nat -R zone_wan_postrouting "$rn" -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "$ipv4":3080

fi
exit 0

There's a simpler method automatically fetching the interface address:

uci set firewall.@nat[0].snat_ip="wan"
uci commit firewall
/etc/init.d/firewall restart
3 Likes

This works really nice with OpenWrt as I can simply ask my script to update the firewall using UCI commands and then restart the firewall service.

If you have multiple games consoles that require a firewall update for example, you can give the firewall rule(s) a UCI named section and then apply the command to the specific rules.

Update firewall rules
I would assume the below code would have to be periodically run from a script using crontabs to keep the firewall up-to-date?

uci set firewall.foo.snat_ip="$(cat /tmp/WAN_IPv4)"
uci set firewall.bar.snat_ip="$(cat /tmp/WAN_IPv4)"
uci commit firewall
/etc/init.d/firewall restart

/etc/config/firewall

# Port Forwarding
config redirect 'foo'
	option src 'wan'
	option target 'DNAT'
	option dest 'lan'
	option dest_port '3074'
	option name 'CoD_PS4_2'
	option src_dport '3080'
	option dest_ip '192.168.5.11'
	option enabled '1'

config redirect 'bar' 
	option src 'wan' 
	option target 'DNAT' 
	option dest 'lan' 
	option dest_port '3074' 
	option name 'CoD_PS4_2' 
	option src_dport '3081' 
	option dest_ip '192.168.5.12' 
	option enabled '1'

# NAT Rules
config nat 'foo'
	option src_port '3074'
	option proto 'tcp udp'
	option name 'CoD-PS4_1'
	option snat_port '3080'
	option target 'SNAT'
	option src 'wan'
	option src_ip '192.168.5.11'
	option snat_ip '123.254.12.254'
	option enabled '1'

config nat 'bar' 
	option src_port '3074' 
	option proto 'tcp udp' 
	option name 'CoD-PS4_2' 
	option snat_port '3081' 
	option target 'SNAT' 
	option src 'wan' 
	option src_ip '192.168.5.12' 
	option snat_ip '123.254.12.254' 
	option enabled '1'

Specify the name of your WAN interface literally, and it should fetch the IP automatically.

Are you referring to the uci commands and repacing the /tmp/WAN_IPv4 file with the my actual WAN's interface?

If so, this would now become the following:

uci set firewall.foo.snat_ip=pppoe
uci set firewall.bar.snat_ip=pppoe
uci commit firewall
/etc/init.d/firewall restart

https://openwrt.org/docs/guide-developer/network-scripting#get_wan_interface

My script and cronjob actually calls on those functions defined in that link you shared me. I just keep the IP address stored in a plain text file so that iptables could read it for example

Would the script become this instead?

. /lib/functions/network.sh
network_flush_cache
network_find_wan NET_IF
network_find_wan6 NET_IF6
echo "${NET_IF}"
echo "${NET_IF6}"
uci set firewall.foo.snat_ip=${NET_IF}
uci set firewall.bar.snat_ip=${NET_IF}
uci commit firewall
/etc/init.d/firewall restart
1 Like

Better specify the interface name once and statically according to the code output.
Then you can simply restart the firewall on schedule, or with hotplug, or with DHCP scripts.
Perhaps you don't even need to restart it since there's a built-in hotplug script.

1 Like

This is the output of the network script.

root@OpenWrt-AP1:~# (
> . /lib/functions/network.sh
> network_flush_cache
> network_find_wan NET_IF
> network_find_wan6 NET_IF6
> echo "${NET_IF}"
> echo "${NET_IF6}"
> )
wan
wan_6

I can confirm that specifying the direct WAN interface does work when set via CLI.

However, typing the WAN interface into the box LuCI expects an IP address not a string

This is the tcp or udp port.

That screenshot is coming from the Rewrite IP Address box.

Right, it was perfectly aligned to the port and fooled me. Does it work with @wan maybe? Otherwise you can leave it as it is configured from UCI.

That LuCI form limits the datatype not allowing interface names:

1 Like

@pavelgl

iptables is still fairly new to me so I don't know all of the parameters.

In the code below this first line essentially sets the variable rn with command substitution to the output of iptables with a table filter, lists all the rules in the chain, shows line numbers, filters out the CoD-PS4-UDP rule and finally prints the first column?

rn=$(iptables -t nat -L --line-numbers | grep CoD-PS4-UDP | awk '{ print $1 }')

iptables -t nat -R zone_wan_postrouting "$rn" -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "$ipv4":3080 

With some reverse engineering of the commands I can see that my rule is listed under the zone_wan_postrouting chain and with all the piped commands, this filters it down to the number 5.

rn=$(iptables -t nat -L --line-numbers | grep CoD-PS4-UDP | awk '{ print $1 }')
echo ${rn}
5

Below is the combined both bits of your code with some modifications:

Modified code
# This creates the initial firewall rule 
iptables -t nat -A zone_wan_postrouting -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "${WAN_IPv4}":3080

# IP address is listed in this file
WAN_IPv4="$(cat /tmp/WAN_IPv4)"

# Get the current IP address from the iptables rule
current_IPv4=$(iptables -t nat -L | grep CoD-PS4-UDP | sed 's/.* to\://' | sed 's/\:.*//')

if [ "${WAN_IPv4}" = "${current_IPv4}" ]; then 
    exit 0 
 else
    rule_number=$(iptables -t nat -L zone_wan_postrouting --line-numbers | grep CoD-PS4-UDP | awk '{ print $1 }')
    iptables -t nat -R zone_wan_postrouting "${rule_number}" -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "${WAN_IPv4}":3080
fi
exit 0

Is the whole idea of the iptables' replace parameter to simply update the existing rules by a specific number in the chain?

iptables --help
...
 --replace -R chain rulenum      Replace rule rulenum (1 = first) in chain
...

What's the difference between deleting the rule, re-appending it and restarting the firewall service?

delete chain rule-specification
-D, --delete chain rulenum

I was thinking of trying to match the majority of the rule specification except skipping the source WAN IP address but that gives me an error.

iptables -t nat -D zone_wan_postrouting -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT 
iptables v1.8.3 (legacy): SNAT: option "--to-source" must be specified

Is there a wildcard I can use or is it a case of finding the current IP address from the current rule as variable and calling that from the command above?

# Get the current IP address from the iptables rule 
current_IPv4=$(iptables -t nat -L | grep CoD-PS4-UDP | sed 's/.* to\://' | sed 's/\:.*//')

iptables -t nat -D zone_wan_postrouting -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --c 
omment "CoD-PS4-UDP" -j SNAT ${current_IPv4}:3080

In other words, if I was apply the same parameters that I appended to iptables and changed -A for -D I would end up with the command:

Commands and output
iptables -t nat -D zone_wan_postrouting -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "${WAN_IPv4}":3080

The only problem I've found is you can easily end up with duplicate rules in iptables as will explain below:

Creating first rule

root@OpenWrt-AP1:~# iptables -t nat -A zone_wan_postrouting -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "${WAN_ IPv4}":3080 

I then list the rule including showing the line number

root@OpenWrt-AP1:~# iptables -t nat -L zone_wan_postrouting --line-numbers | grep CoD-PS4-UDP 
7    SNAT       udp  --  William-PS4.lan      anywhere             udp spt:3074 /* CoD-PS4-UDP */ to:109.149.182.250:3080

Creating second rule

iptables -t nat -A zone_wan_postrouting -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "${WAN_ IPv4}":3080

I then list the rule(s) again

root@OpenWrt-AP1:~# iptables -t nat -L zone_wan_postrouting --line-numbers | grep CoD-PS4-UDP 
7    SNAT       udp  --  William-PS4.lan      anywhere             udp spt:3074 /* CoD-PS4-UDP */ to:109.149.182.250:3080 
8    SNAT       udp  --  William-PS4.lan      anywhere             udp spt:3074 /* CoD-PS4-UDP */ to:109.149.182.250:3080

As you can see above there are two duplicate rules. These duplicates wouldn't be a problem if the delete parameter deleted both rules together in a single command but it does't. I suppose one way around this is to run a while loop until the iptables returns zero.

However, the chances of duplicates with the script is very slim as they only way this could occur is if the delete parameter failed to run but carries on to the append parameter, and each time the script is ran it keeps appending the same rule.

Could I use the delete and append options in iptables within a for loop?

rule_1=$(iptables -t nat -L zone_wan_postrouting --line-numbers | grep CoD-PS4-UDP | awk '{ print $1 }')
rule_2=$(iptables -t nat -L zone_wan_postrouting --line-numbers | grep CoD-XBOX-UDP | awk '{ print $1 }')

for rule in ${rule_1} ${rule_2}
do
    iptables -t nat -R zone_wan_postrouting "${rule}" -s 192.168.5.11/32 -p udp -m udp --sport 3074 -m comment --comment "CoD-PS4-UDP" -j SNAT --to-source "${WAN_IPv4}":3080
done

What's the difference between the replace parameter in comparison to delete and append or do they relatively do the same function in this case?

I take it because iptables files are treated like scripts, does that mean I could use a cronjob to periodically refresh the iptables commands? What shebang does iptables conform to?

Sorry for the long reply. I've been working on constructing this reply for a while and I wanted to make sure I asked the right questions. At the end of the day It helps me but helps others who may discover this thread/forums. :slight_smile:

1 Like