Forward SSH through a controller firewall with DNAT and SNAT

5 min read · Published November 10, 2026

Forward one TCP port through a controller firewall with a pair of NAT rules: DNAT rewrites the destination to the device at 172.17.26.146 port 22, SNAT rewrites the source to the controller's device-side address so replies return, and the forward filter gets one accept. A masquerade rule then gives the device outbound access.

Forwarding SSH to a device that sits behind an industrial controller takes four rules, not one. A destination NAT rule rewrites the incoming port to the device's address. A source NAT rule makes the controller the source, so the reply comes back. The forward filter gets one accept for that pair. A masquerade rule gives the device outbound access through the controller's other interface.

The case below forwards external TCP 9022 to SSH on TCP 22 at a Linux device at 172.17.26.146. The controller's device-side interface holds 172.17.26.211, and its LAN side sits on 192.168.47.0/24. If your forwarding already exists and has stopped working, start with testing a port-forward link one hop at a time; this page builds the rules from scratch.

What you need

  • A controller with two network interfaces and a firewall that does NAT. The LAN-side interface lives on 192.168.47.0/24; the device-side interface holds 172.17.26.211.
  • The device itself, at a fixed address of 172.17.26.146 on that segment, with its SSH service listening on TCP 22.
  • IP forwarding enabled on both interfaces in the controller's settings.
  • The controller's web UI, reached over HTTPS on TCP 443.
  • A client on 192.168.47.0/24 for testing.

Hanging a service device on the controller's second interface is the same move as adding an operator interface when the PLC ports are full: you give it its own network instead of loading a busy one.

What each rule does

A destination NAT (DNAT) rule rewrites the destination of an incoming packet. Yours says: TCP packets aimed at external port 9022 are redirected to 172.17.26.146 port 22. A source NAT (SNAT) rule rewrites the source. Packets from 172.17.26.146 port 22 leave carrying 172.17.26.211, the controller's own device-side address, so the device answers to the controller and the controller can hand the reply back to the client. This DNAT plus SNAT pair is the standard pattern in the iptables NAT table, documented in the netfilter NAT how-to.

The forward filter carries the decision. Its default policy is Drop, so nothing crosses between the interfaces until you add an accept for a named flow. Masquerade, last, is source NAT for outbound traffic. It sends the device's connections out through the LAN-side interface under that interface's address, which is what lets a device on a private segment reach the internet.

Create the rules

  1. Open Settings > Firewall > Routing. Add a destination NAT rule: protocol IPv4 TCP, destination port 9022, translate to 172.17.26.146 port 22. Enable it.
  2. In the same view add the source NAT rule: protocol IPv4 TCP, source address 172.17.26.146, port 22, translate source to 172.17.26.211. Enable it.
  3. Open Settings > Firewall > Forward Filter > DefaultChain. Confirm the policy is Drop and that the rule accepting connections in the established,related state sits first.
  4. Add an accept for the forwarded SSH pair: destination 172.17.26.146, TCP port 22. The destination rewrite happens before this chain, so the rule matches the translated destination, not port 9022.
  5. Check Settings > Firewall > Input Filter > DefaultChain. Policy Drop; the loopback rule ip-input-00000000 present; a rule accepting TCP 443 for the web UI; the established-connections rule that lets return traffic back in. Add nothing here.
  6. Check Settings > Firewall > Output Filter > DefaultChain. Policy Accept, with the loopback rule ip-output-00000000. The controller itself can initiate outbound connections; nothing to add.
  7. Back in Settings > Firewall > Routing, add the outbound rule: protocol IPv4, source address 172.17.26.146, NAT type Masquerade. Name it something you will recognize a year from now.
  8. Add its accept in Settings > Firewall > Forward Filter > DefaultChain: source 172.17.26.146, action Accept, placed below the established,related rule.
  9. Save the firewall configuration.

Keep the forward filter at Drop and open one named flow per rule. A broad allow-all in that chain is a hole through the machine's firewall, and every service you add later inherits it.

Worked example: forwarding TCP 9022 to SSH port 22 at 172.17.26.146

Inputs:

  • External port 9022, chosen for this service.
  • Device at 172.17.26.146, SSH on TCP 22.
  • Controller device-side interface at 172.17.26.211.
  • LAN 192.168.47.0/24; the controller answers on that side at 192.168.47.228, the address the test below uses.
  • Test timeout of 5 seconds per connection attempt, and 3 test pings at 1-second intervals.

The rule in words: DNAT changes where the packet is going, SNAT changes where it appears to come from, and the connection tracker reverses both rewrites on the reply.

The arithmetic, one packet out and one back:

  1. A client on 192.168.47.0/24 sends to destination 192.168.47.228:9022.
  2. DNAT rewrites the destination: 192.168.47.228:9022 becomes 172.17.26.146:22.
  3. SNAT rewrites the source to 172.17.26.211.
  4. The forward filter now sees 172.17.26.211 going to 172.17.26.146:22, which is exactly the pair the accept rule from step 4 matches.
  5. The device replies to 172.17.26.211. The tracker reverses both rewrites, so the client receives its answer from 192.168.47.228:9022.

Result: an SSH client that connects to port 9022 on 192.168.47.228 lands on the device's port 22, with no route changes on the client and no second address on the device.

Decision: nc -zv -w5 192.168.47.228 9022 must report the port open inside its 5-second window, and an SSH login on port 9022 must complete. If the port opens and the session then stalls, DNAT and the filter are working and the failure is on the return path: the SNAT rule, or the established,related accept.

How to check it worked

From the client on 192.168.47.0/24, run nc -zv -w5 192.168.47.228 9022. The -w5 caps the attempt at 5 seconds; an open result means the DNAT rule and the forward-filter accept are both doing their jobs. Then run the real test, ssh -p 9022 to 192.168.47.228, and log in.

From the device, after the masquerade rule and its accept are saved, run 3 pings at 1-second intervals to any host on the far side. Both must fail before step 8 and pass after it, which pins the change on the masquerade pair and nothing else.

Once SSH reaches the device, the same link carries more. X11 forwarding over SSH puts the device's configuration tools on your own screen. On screen-equipped devices you can blank a Wayland kiosk screen over SSH and wake it later without walking to it.

Recommendation: Build every forwarded service as one DNAT rule plus one SNAT rule to the device-side interface address, with one accept in the forward filter, and keep the established,related accept first in that chain. Use one external port per device. When a device also needs outbound access, add one masquerade rule scoped to its address, 172.17.26.146 in this case, not to the whole subnet.

Questions

Why does SSH forwarding need an SNAT rule as well as DNAT?

DNAT only changes where the packet is going. The device still sees the client's real address on 192.168.47.0/24, and if its route back does not pass through the controller, the reply leaves another way and the session stalls with the port showing open. SNAT makes the controller's device-side address, 172.17.26.211, the source, so the reply returns to the controller, where the connection tracker reverses both rewrites.

Why does the forward-filter rule match port 22 and not 9022?

The destination rewrite happens before the forward filter, so that chain already sees the translated destination, 172.17.26.146 on TCP port 22. An accept written against 9022 in the forward filter never matches anything and the flow stays dropped. Match the translated address and port in the forward filter, and match 9022 only in the destination NAT rule.

Can two devices share external port 9022?

No. One destination NAT rule maps one external port to one internal address and port. Give each device its own external port, and give each port its own DNAT rule, its own SNAT rule to the device-side interface address, and its own accept in the forward filter. One service per rule pair also keeps the firewall list readable when you audit it later.

Does enabling IP forwarding open the whole network?

No. Forwarding only lets the controller pass packets between its two interfaces; the forward filter decides what actually crosses, and its default policy is Drop. Until you add an explicit accept, a flow is blocked in both directions. Enable forwarding first, then open exactly the flows you have named, one rule at a time.

Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.

Keep reading

Build your signAsk for a quote