My general question is this: What's the best way (simplest, easiest, quickest, least error-prone, etc.) to verify iptables NAT rules locally on a single host (i.e. without a network connection) at the command-line?

What follows are the details of specific (failed) attempts at checking a simple DNAT rule using NetCat. I am hoping for a resolution of my specific issue in this case, but also for an answer to my general question.


I'm working on a VirtualBox virtual machine running Debian 8 (Jessie). I want to use netcat to perform a basic test of a simple DNAT rule.

For my test, all I want to do is send some data to one local address (e.g. 192.168.0.1) and have it arrive at another local address (e.g. 192.168.0.2).

I've tried several different approaches so far:

  1. Dummy interfaces and the PREROUTING chain

  2. Virtual interfaces and the PREROUTING chain

  3. Using the OUTPUT chain instead of PREROUTING

Dummy interfaces and the PREROUTING chain

My first attempt was to add a DNAT rule to the PREROUTING chain and add two dummy interfaces with the appropriate addresses.

Here is my rule:

sudo iptables \
-t nat \
-A PREROUTING \
-d 192.168.0.1 \
-j DNAT --to-destination 192.168.0.2

There are no other netfilter rules in my firewall. But just to be sure, here is the output from iptables-save:

# Generated by iptables-save v1.4.21

*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -d 192.168.0.1/32 -j DNAT --to-destination 192.168.0.2
COMMIT

*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT

To reiterate, all I want to do is send some data to the 192.168.0.1 address and have it arrive at the 192.168.0.2 address.

It's probably worth mentioning that the 192.168.0.0/24 subnetwork is unused on my VM. First I add a couple of dummy interfaces:

sudo ip link add dummy1 type dummy

sudo ip link add dummy2 type dummy

Next I assign assign the IP addresses to the dummy interfaces on the desired subnetwork range:

sudo ip addr add 192.168.0.1/24 dev dummy1

sudo ip addr add 192.168.0.2/24 dev dummy2

And then I bring the interfaces up:

sudo ip link set dummy1 up

sudo ip link set dummy2 up

Here is what my routing table looks like now:

default via 10.0.2.2 dev eth0
10.0.2.0/24     dev eth0    proto kernel  scope link  src 10.0.2.15
192.168.0.0/24  dev dummy1  proto kernel  scope link  src 192.168.0.1
192.168.0.0/24  dev dummy2  proto kernel  scope link  src 192.168.0.2
192.168.56.0/24 dev eth1    proto kernel  scope link  src 192.168.56.100

Now I listen at the first (source) address using netcat:

nc -l -p 1234 -s 192.168.0.1

And I connect to the netcat server with a netcat client (in a separate terminal window):

nc 192.168.0.1 1234

Text entered in one window appears in the other - just as expected.

I do the same thing with the second address as well:

nc -l -p 1234 -s 192.168.0.2

nc 192.168.0.2 1234

Again, text entered in one window appears in the other - as expected.

Finally, I try to listen on the target (DNAT) address and connect via the source (DNAT) address:

nc -l -p 1234 -s 192.168.0.2

nc 192.168.0.1 1234

Unfortunately the connection fails with the following error:

(UNKNOWN) [192.168.0.1] 1234 (?) : Connection refused

I also tried using ping -c 1 -R 192.168.0.1 to see if the DNAT was taking effect, but it does not look like that's the case:

PING 192.168.0.1 (192.168.0.1) 56(124) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.047 ms
RR:     192.168.0.1
        192.168.0.1
        192.168.0.1
        192.168.0.1


--- 192.168.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.047/0.047/0.047/0.000 ms

Why isn't this working? What am I doing wrong?

Diagnosis with tcpdump

To diagnose this issue, I tried using tcpdump to listen for traffic on the dummy interfaces. I tried listening to all interfaces (and filtering out SSH and DNS):.

sudo tcpdump -i any -e port not 22 and port not 53

Then I pinged the dummy1 interface:

ping -n -c 1 -I dummy1 192.168.0.1

This yielded the following results:

listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
In 00:00:00:00:00:00 (oui Ethernet) ethertype IPv4 (0x0800), length 100: 192.168.0.1 > 192.168.0.1: ICMP echo request, id 8071, seq 1, length 64
In 00:00:00:00:00:00 (oui Ethernet) ethertype IPv4 (0x0800), length 100: 192.168.0.1 > 192.168.0.1: ICMP echo reply, id 8071, seq 1, length 64

So it looks like the dummy interfaces are attached to the loopback interface. This might mean that the iptables rules are being totally circumvented.

Virtual interfaces and the PREROUTING chain

As a second attempt, I tried using so-called virtual IP addresses instead of dummy interfaces.

Here is how I added the "virtual" IP addresses to the eth0 and eth1 interfaces:

sudo ip addr add 192.168.0.100/24 dev eth0
sudo ip addr add 192.168.0.101/24 dev eth1

NOTE: I used different IP addresses for these than I did for the dummy interface.

Then I flushed and updated the iptables NAT rules:

sudo iptables -F -t nat

sudo iptables \
-t nat \
-A PREROUTING \
-d 192.168.0.100 \
-j DNAT --to-destination 192.168.0.101

The I retried the ping test:

ping -n -c 1 -R 192.168.0.100

No dice:

PING 192.168.0.100 (192.168.0.100) 56(124) bytes of data.
64 bytes from 192.168.0.100: icmp_seq=1 ttl=64 time=0.023 ms
RR:     192.168.0.100
        192.168.0.100
        192.168.0.100
        192.168.0.100


--- 192.168.0.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.023/0.023/0.023/0.000 ms

Then the netcat test again. Start the server:

nc -l -p 1234 -s 192.168.0.101

Try to connect the client:

nc 192.168.0.100 1234

Also no dice:

(UNKNOWN) [192.168.0.100] 1234 (?) : Connection refused

Using the OUTPUT chain instead of PREROUTING

Then I tried moving both DNAT rules from the PREROUTING chain to the OUTPUT chain:

sudo iptables -F -t nat

sudo iptables \
-t nat \
-A OUTPUT \
-d 192.168.0.1 \
-j DNAT --to-destination 192.168.0.2

sudo iptables \
-t nat \
-A OUTPUT \
-d 192.168.0.100 \
-j DNAT --to-destination 192.168.0.101

Now I try ping on both the dummy and virtual interfaces:

user@host:~$ ping -c 1 -R 192.168.0.1
PING 192.168.0.1 (192.168.0.1) 56(124) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.061 ms
RR:     192.168.0.1
        192.168.0.2
        192.168.0.2
        192.168.0.1


--- 192.168.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.061/0.061/0.061/0.000 ms

user@host:~$ ping -c 1 -R 192.168.0.100

PING 192.168.0.100 (192.168.0.100) 56(124) bytes of data.
64 bytes from 192.168.0.100: icmp_seq=1 ttl=64 time=0.058 ms
RR:     192.168.0.100
        192.168.0.101
        192.168.0.101
        192.168.0.100


--- 192.168.0.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.058/0.058/0.058/0.000 ms

And I also try the netcat client-server test for each pair of IP addresses:

nc -l -p 1234 -s 192.168.0.2

nc 192.168.0.1 1234

and:

nc -l -p 1234 -s 192.168.0.101

nc 192.168.0.100 1234

This test succeeds as well.

So it looks like both the dummy and virtual interfaces work when the DNAT rule is in the OUTPUT chain instead of the PREROUTING chain.

It seems that part of my problem is that I'm unclear on which packets traverse which chains.

  • Dummy interfaces won't get you anywhere. Simplest setup is to use network namespaces with veth-pairs, and ping. There are quite a few related questions for those topics, but I don't have time to locate them now. – dirkt Nov 15 '17 at 22:58
  • @dirkt I'll have to apologize for my ignorance. Why won't dummy interfaces get me anywhere? – igal Nov 15 '17 at 23:15
  • They are called "dummy" for a reason: They are meant to be bridged to allow a long-lived application bind to them with a constant IP address. Also see this question. And even without dummy interfaces, local-to-local delivery is not easy to setup. – dirkt Nov 16 '17 at 14:55
  • @dirkt Thank you for the comment and for the link to that other post. Unlike the situation there, however, I am able to use the dummy interfaces as I've described in my post. I can ping a dummy interface and setup a client-server communication through a dummy interface with netcat. So it's not clear to me that the dummy interfaces are the problem here. – igal Nov 16 '17 at 15:06
  • @dirkt Maybe it's worth point out that I also ran the same experiment with virtual interfaces instead of dummy interfaces but didn't have any luck with that either. I'll give it another shot (just as a sanity check) and update my question with the details. – igal Nov 16 '17 at 15:08
up vote 0 down vote accepted

Short Explanation: The dummy interfaces and virtual IP addresses send packets through the loopback interface, which isn't affected by the PREROUTING chain. By using network namespaces with veth interfaces we can send traffic from one IP address to another in a way that more accurately models multi-host network traffic and allows us to test the DNAT rule on the PREROUTING chain, as desired.

A more detailed description of the solution follows.


Here is a Bash script that configures a pair of network interfaces and tests that the DNAT rule is functioning as expected:

# Create a network namespace to represent a client
sudo ip netns add 'client'

# Create a network namespace to represent a server
sudo ip netns add 'server'

# Create a veth virtual-interface pair
sudo ip link add 'client-eth0' type veth peer name 'server-eth0'

# Assign the interfaces to the namespaces
sudo ip link set 'client-eth0' netns 'client'
sudo ip link set 'server-eth0' netns 'server'

# Change the names of the interfaces (I prefer to use standard interface names)
sudo ip netns exec 'client' ip link set 'client-eth0' name 'eth0'
sudo ip netns exec 'server' ip link set 'server-eth0' name 'eth0'

# Assign an address to each interface
sudo ip netns exec 'client' ip addr add 192.168.1.1/24 dev eth0
sudo ip netns exec 'server' ip addr add 192.168.2.1/24 dev eth0

# Bring up the interfaces (the veth interfaces the loopback interfaces)
sudo ip netns exec 'client' ip link set 'lo' up
sudo ip netns exec 'client' ip link set 'eth0' up
sudo ip netns exec 'server' ip link set 'lo' up
sudo ip netns exec 'server' ip link set 'eth0' up

# Configure routes
sudo ip netns exec 'client' ip route add default via 192.168.1.1 dev eth0
sudo ip netns exec 'server' ip route add default via 192.168.2.1 dev eth0

# Test the connection (in both directions)
sudo ip netns exec 'client' ping -c 1 192.168.2.1
sudo ip netns exec 'server' ping -c 1 192.168.1.1

# Add a DNAT rule to the server namespace
sudo ip netns exec 'server' \
iptables \
-t nat \
-A PREROUTING \
-d 192.168.2.1 \
-j DNAT --to-destination 192.168.2.2

# Add a dummy interface to the server (we need a target for the destination address)
sudo ip netns exec 'server' ip link add dummy type dummy
sudo ip netns exec 'server' ip addr add 192.168.2.2/24 dev dummy
sudo ip netns exec 'server' ip link set 'dummy' up

# Test the DNAT rule using ping
sudo ip netns exec 'client' ping -c 1 -R 192.168.2.1

The output of the ping test shows that the rule is working:

PING 192.168.2.1 (192.168.2.1) 56(124) bytes of data.
64 bytes from 192.168.2.1: icmp_seq=1 ttl=64 time=0.025 ms
RR:     192.168.1.1
        192.168.2.2
        192.168.2.2
        192.168.1.1


--- 192.168.2.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.025/0.025/0.025/0.000 ms

Now I can also perform my NetCat test. First I listen on the server:

sudo ip netns exec 'server' nc -l -p 1234 -s 192.168.2.2

And then I connect via the client (in a separate terminal window):

sudo ip netns exec 'client' nc 192.168.2.1 1234

Text entered in one terminal window appears in the other - success!

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.