FirewallD vs. masquerade

Hi,

I just installed Rocky Linux 8 on an old PC Engines routerboard (using the serial port and the VNC installer, that was fun).

I’m using two of the three network interfaces:

# nmcli con show
NAME    UUID                                  TYPE      DEVICE 
WAN     176fb3b6-26a1-41d7-b2ee-840ff2b11dd7  ethernet  enp1s0 
LAN     11b1608a-7c91-4166-b1c5-bead5f5e2ae9  ethernet  enp2s0

As you may have guessed from the respective connection names, WAN (enp1s0) is connected to my box, whereas LAN (enp2s0) is my local network.

Here’s what my current FirewallD configuration looks like:

# firewall-cmd --list-all --zone=external
external (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp1s0
  sources: 
  services: ssh
  ports: 
  protocols: 
  forward: no
  masquerade: yes
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:

# firewall-cmd --list-all --zone=internal
internal (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp2s0
  sources: 
  services: ssh
  ports: 
  protocols: 
  forward: no
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:

The machine is supposed to act as a router between two networks: 192.168.2.0/24 and 192.168.3.0/24. I wonder what’s the correct way to enable packet forwarding using FirewallD.

I know I have to set masquerade to yes, but I don’t know in which zone:

  • external only?
  • internal only?
  • both?

Any suggestions?

Niki

1 Like

Yes

Routing requires two things:

  1. That kernel forwards packets to begin with
  2. That the firewall rules permit forwarding

Forward

For IPv4 – I don’t actually know what IPv6 uses – the net.ipv4.ip_forward has to be ‘1’ for the first.
You can check with: sysctl net.ipv4.ip_forward

In my experience, merely the use of zone ‘external’ does set that. If it doesn’t, you can drop the config in:

# cat /etc/sysctl.d/i_am_router.conf 
net.ipv4.ip_forward = 1

Filter

For the second, we need firewall rules. Again, I would first check what the current, actual rules are:

nft list ruleset

Alas, that is a mouthful since FirewallD is somewhat verbose when it writes rules. (For its own benefit, naturally.)

Until rather recently the FirewallD in RHEL (and hence Rocky) did lack proper support for router rules. Now there are policy objects. Chapter 40. Using and configuring firewalld Red Hat Enterprise Linux 8 | Red Hat Customer Portal

Therefore,

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 --complete-reload

Which should make the chain filter_FORWARD look effectively (after dereferencing all jumps and gotos to auxiliary chains) something like:

ct state established,related accept
ct status dnat accept
iifname "lo" accept
ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 addr-unreachable
iifname "enp2s0" oifname "enp1s0" accept
meta l4proto { icmp, ipv6-icmp } counter packets 0 bytes 0 accept
ct state invalid drop
reject with icmpx admin-prohibited

There is (Ansible) RHEL System Role for firewall, but what features it supports and what not, you better check yourself: Chapter 40. Using and configuring firewalld Red Hat Enterprise Linux 8 | Red Hat Customer Portal


NAT

The masquerade, which is a form of sNAT is enabled in the ‘external’ zone default.
Your ruleset should already have oifname "enp1s0" masquerade in within postrouting chain(s).
Quick check:

nft list ruleset | grep -10 masquerade

A router does not need masquerade, but it can make config in other machines simpler.

Lets have: WAN 192.168.2.0/24, LAN 192.168.3.0/24
The router has addresses 192.168.2.1 and 192.168.3.1.
The WAN has “upstream” router at 192.168.2.254, which is also the “gateway” for our router.

The router knows how to send to LAN, to WAN, and everything else goes to 192.168.2.254.

A client in LAN, 192.168.3.42, has gateway (i.e. default route) 192.168.3.1. It knows how to send to LAN and forwards everything else to 192.168.3.1.

A client in WAN, 192.168.2.7, has gateway 192.168.2.254. It knows how to send to WAN and forwards everything else to 192.168.2.254.

How does 192.168.2.7 send to 192.168.3.42? To 192.168.3.0/24?

Case A: router does not have sNAT

If packets are sent to 192.168.2.254, the same question repeats: How does 192.168.2.x send to 192.168.3.0/24?

These WAN clients (other than our router) need a “static route”: to 192.168.3.0/24 via 192.168.2.1
With that, the 192.168.2.7 would send packet to 192.168.2.1, rather than to 192.168.2.254, and 192.168.2.1 knows how to send to its LAN “neighbours”.

The positive side is that they could start a new connection from WAN to LAN. Well, they could if the router’s firewall would allow that. The policy above allows LAN to talk to WAN, but only replies from WAN to LAN.

Case B: router does have sNAT

The 192.168.3.42 did send a packet to 192.168.2.7.
The packet had SRC=192.168.3.42 DST=192.168.2.7
The masquerade at the router modifies the packet to read: SRC=192.168.2.1 DST=192.168.2.7
The 192.168.2.7 sends a reply that has SRC=192.168.2.7 DST=192.168.2.1
The router notes that this is a reply and modifies the packet to read: SRC=192.168.2.7 DST=192.168.3.42 (in prerouting)
Since the reply is not for the router, it is tossed out from the LAN port for 192.168.3.42 to pick up.

Nobody outside of LAN knows that the LAN exists.

2 Likes

Thanks very much for that detailed explanation. Though I must admit I vaguely feel like someone having asked for an ID photo in a photo booth who’s given a 2 x 3 meters oil portrait. The resemblance is striking, and the brush strokes are executed in excruciating detail. :slightly_smiling_face:

1 Like