Help needed converting iptables NAT rules

Hello,
I recently changed server hardware and had to reinstall the OS. I had CentOS 7 on the old box, and now I am using Rocky Linux 9. I am used to iptables, and I had that setup to perform NAT on eth1 (my LAN interface) so that devices on the LAN can connect freely to the WAN through eth0. How do I recreate this in firewalld/nftables? I have researched and tried numerous solutions and none seem to work. Help is appreciated.

Using firewalld to do this is possible. There are some folks with experience in that area that can answer to it. For nftables, what I would suggest is to run your current rules through iptables-restore-translate as it will try to create something that should either work or come close to working. Disable the firewalld service, enable nftables, and go from there.

Below is an example of what I’m using for ipv4 and NAT. In /etc/sysconfig/nftables.conf, you can set which files get included in /etc/nftables, and you can continue to include as needed.

table ip nat {

        set port_forward_pc_tcp_ipv4 {
                type inet_service; flags interval;
                elements = {
                        6112, 6113, 10666, 58170, 58171
                }
        }

        set port_forward_pc_udp_ipv4 {
                type inet_service; flags interval;
                elements = {
                        5029, 6112, 6113, 10666
                }
        }

        chain home_pre_in {
                udp dport 53 dnat to $int_router_ip4:53
                tcp dport 53 dnat to $int_router_ip4:53
                tcp dport { 6112, 6113 } dnat to $ws_home_ipv4
                udp dport { 6112, 6113 } dnat to $ws_home_ipv4
                tcp dport 58171 dnat to $ws_home_ipv4:58171
                tcp dport 58170 dnat to $ws_home_ipv4:58170
                tcp dport 25565 dnat to $int_svc_mc_ip4:25565
                tcp dport 19132 dnat to $int_svc_mc_ip4:19132
                udp dport 19132 dnat to $int_svc_mc_ip4:19132
                tcp dport { 5222, 5269 } dnat to $int_svc_xmpp_ip4
        }

        chain iso_pre_in { }

        chain PREROUTING {
                type nat hook prerouting priority -100; policy accept;
                iifname vmap { $nic_modem : jump home_pre_in }
        }
        chain POSTROUTING {
                type nat hook postrouting priority 100; policy accept;
                oifname $nic_modem ip daddr != { $int_home_subnet_0, $int_home_subnet_1} masquerade
        }

        chain OUTPUT {
                type nat hook output priority 0; policy accept;
        }
}
table ip filter {
        chain home_in {
                ip daddr $ws_home_ipv4 tcp dport 6112 accept comment "SC"
                ip daddr $ws_home_ipv4 udp dport 6112 accept comment "SC"
                ip daddr $ws_home_ipv4 udp dport 5029 accept comment "SC"
                ip daddr $router_ipv4 udp dport 53 accept comment "DNS"
                ip daddr $router_ipv4 tcp dport 53 accept comment "DNS"
                ip daddr $router_ipv4 tcp dport 45522 accept comment "SSH"
                ip daddr $xmpp_ipv4 tcp dport 5222 accept comment "XMPP"
                ip daddr $xmpp_ipv4 tcp dport 5269 accept comment "XMPP"
                ip daddr $mc_ipv4 tcp dport 25565 accept comment "Minecraft"
                ip daddr $mc_ipv4 tcp dport 19132 accept comment "Minecraft"
                ip daddr $mc_ipv4 udp dport 19132 accept comment "Minecraft"
                oifname $nic_homebridge ct state vmap { established : accept, related : accept, invalid : drop }
                oifname $nic_isobridge  ct state vmap { established : accept, related : accept, invalid : drop }
        }
        chain home_bridge {
                oifname $nic_modem accept
                oifname $nic_isobridge accept
        }

        chain iso_bridge {
                oifname $nic_modem accept
                oifname $nic_homebridge accept
        }

        chain FORWARD {
                type filter hook forward priority 0; policy accept;
                tcp flags syn tcp option maxseg size set rt mtu;
                iifname vmap { $nic_modem : jump home_in }
                iifname vmap { $nic_homebridge : jump home_bridge }
                iifname vmap { $nic_isobridge : jump iso_bridge }
                drop
        }

        chain INPUT {
                type filter hook input priority 0; policy accept;
                ct state vmap { established : accept, related : accept, invalid : drop }
                iifname "lo" accept
                # Allow pings
                icmp type { destination-unreachable, echo-reply, echo-request, source-quench, time-exceeded } limit rate 5/second accept
                iifname $nic_modem jump IN_TRU comment "Trusted inbound Home"
                iifname $nic_modem jump IN_DMZ comment "DMZ inbound Home"
                iifname $nic_homebridge jump IN_NET comment "Network Traffic VLAN 1000"
                iifname $nic_isobridge jump IN_NET comment "Network Traffic VLAN 1001"
                jump IN_GLO comment "Global Ruleset"
                ct state new drop
        }

        chain OUTPUT {
                type filter hook output priority 0; policy accept;
        }
        chain IN_TRU {
                ip protocol igmp accept
                # ipv6 heartbeat
                ip saddr 66.220.7.82 counter accept comment "IPv6 Heartbeat"
                ip saddr 66.220.2.74 counter accept comment "IPv6 Heartbeat"
                ip saddr $shane_router tcp dport domain accept comment "Shane DNS Slave"
        }

        chain IN_DMZ {
                ip saddr { $BOGONS4 } drop
        }

        set internal_tcp_ipv4 {
                type inet_service; flags interval;
                elements = {
                        22, 53, 139, 445, 3260, 4200, 8080, 9090, 9092, 19999, 111, 2049, 20048
                }
        }

        set internal_udp_ipv4 {
                type inet_service; flags interval;
                elements = {
                        53, 67, 68, 69, 123, 137, 138, 514, 3260, 5900-5950, 111, 2049, 20048
                }
        }

        set isolated_tcp_ipv4 {
                type inet_service; flags interval;
                elements = {
                        53, 80, 443
                }
        }

        set isolated_udp_ipv4 {
                type inet_service; flags interval;
                elements = {
                        53, 69, 123
                }
        }

        chain IN_NET {
                tcp dport @internal_tcp_ipv4 accept
                udp dport @internal_udp_ipv4 accept
                tcp dport 9053 accept comment "unbound"
                udp dport 9053 accept comment "unbound"
                udp dport { netbios-ns, netbios-dgm } accept comment "Samba"
                tcp dport 26697 accept comment "znc"
                tcp dport 6697 accept comment "znc"
                ct state new tcp dport { netbios-ssn, microsoft-ds } accept comment "Samba"
        }

        chain IN_GLO {
                udp dport 53 accept
                tcp dport 80 accept
                tcp dport 443 accept
                tcp dport 8000 accept
                tcp dport 45522 accept
                udp dport 69 drop
        }

        chain IN_ISO {
                tcp dport @isolated_tcp_ipv4 accept
                udp dport @isolated_udp_ipv4 accept
        }

        set ipa_ports_udp {
                type inet_service; flags interval;
                elements = {
                        53, 88, 464
                }
        }

        set ipa_ports_tcp {
                type inet_service; flags interval;
                elements = {
                        53, 80, 88, 389, 443, 464, 636
                }
        }

        chain IN_IPA {
                ip daddr $int_ipa_01 tcp dport @ipa_ports_tcp accept
                ip daddr $int_ipa_01 udp dport @ipa_ports_udp accept
                ip daddr $int_ipa_02 tcp dport @ipa_ports_tcp accept
                ip daddr $int_ipa_02 udp dport @ipa_ports_udp accept
        }
}

There is dNAT and sNAT.
The dNAT, aka port forwarding, is done on incoming port, prerouting.
The sNAT, usually masquerade, is done on outgoing port, postrouting.
Therefore, one does not “NAT on eth1” for eth1->eth0 traffic. One does masquerade on oif.

	chain nat_PREROUTING {
		type nat hook prerouting priority dstnat; policy accept;
	}

	chain nat_POSTROUTING {
		type nat hook postrouting priority srcnat; policy accept;
		oifname "eth0" masquerade
	}

One obviously has to allow (new) traffic out and replies back in:

	chain forward {
		type filter hook forward priority filter; policy accept;
		ct state established,related accept
		ct status dnat accept
		iif "lo" accept
		iifname "eth1" counter accept
		ct state invalid drop
		counter reject with icmp type admin-prohibited
	}

If one had dNAT rules in prerouting, then the ct status dnat accept should allow all such traffic.


There is one thing in NetworkManager-FirewallD combo that I have not figured out with nftables. When NM activates a connection, it tells the name of the interface device to FirewallD, that adds appropriate “oifname” and “iifname” rules. The traditional ethN style names were never predictable. I’ve had VM’s, where the names eth0 and eth1 did swap on every boot. NM passes the current name and all is well.

With more static nftables ruleset you really want the interface names to be predictable.

@nazulika You use variables like $nic_homebridge in your ruleset. Where/how do you define them?

That’s fair, I was wondering if I would indeed have to turn to something other than firewalld for this. Do you know if I could add these rules to nftables, but keep firewalld running for management through the web console? I certainly can edit the file, as I came from iptables, but having the GUI is a nice change.

I wasn’t quite sure of the terminology, I have encountered DNAT and SNAT before with iptables, but the way I’m used to doing it is

-A POSTROUTING -o eth0 -j MASQUERADE
-A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i eth1 -o eth0 -j ACCEPT

I realize nftables and firewalld don’t work the same way, but that’s what I hope to accomplish.

Surprisingly I find the exact opposite. I always hated the “predictable names” like enp0s3, as I have a hard time remembering what’s what, and I find that my interface names never change or get assigned incorrectly, making the ethX names the predictable ones.

Without doing some hacky workarounds, I don’t think this will be possible. I also think the firewalld part of cockpit is limited. As far as I’ve seen, it only handles services/ports, and not complex rules such as forwards.

I also did some digging. If you want to use only firewalld, you could do something like this:

firewall-cmd --permanent --new-policy policy_int_to_ext
firewall-cmd --permanent --policy policy_int_to_ext --add-ingress-zone internal
firewall-cmd --permanent --policy policy_int_to_ext --add-egress-zone external
firewall-cmd --permanent --policy policy_int_to_ext --set-target ACCEPT
firewall-cmd --permanent --zone=external --add-masquerade
firewall-cmd --set-default-zone=internal
firewall-cmd --complete-reload

I believe this would get you started using firewalld and NAT.

I do agree.
Although, I think the zone external does have masquerade by default, so no need to add that.
Furthermore, it (the use of external) did seem to (in el7) to enable the ip_forward too that is required for actually routing traffic through.


They are very similar though. Red Hat documents the use of nftables: Chapter 2. Getting started with nftables Red Hat Enterprise Linux 9 | Red Hat Customer Portal

The nftables does not have any predefined tables or chains, unlike netfilter.
Once you have table and chains, adding actual rules is:

iptables -A POSTROUTING -o eth0 -j MASQUERADE
iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
# vs
nft add rule inet example_table nat_POSTROUTING oifname "eth0" masquerade
nft add rule inet example_table forward iifname "eth0" iifname "eth1" ct state established,related accept
nft add rule inet example_table forward iifname "eth1" iifname "eth0" accept

That worked instantly. I realize nftables would likely be better for this, but like I said, managing ports through the web UI is a nice ability to have. Thank you so much!

If you ever used FirewallD , or any other front-end, for iptables, you still had the chance to see the actual rules with:

iptables -t filter -S
iptables -t nat -S
iptables -t mangle -S

You can do the same on nftables-based systems:

nft list ruleset

(That is how I started with nftables – had FirewallD-generated ruleset, saved it, and “cleaned” it to my liking.)