I'm trying to create a static NAT, but when I run the command ip route add nat via, it gives me the error Error: Invalid scope., and it gives the same error when specifying a global scope. I'm trying to replicate what's described on the Stateless NAT with iproute2 page.

My network setup consists of two physical interfaces on the same subnet (ens5, ens6), and one virtual interface for my tinc vpn (vpn). The address is reachable on from the tinc interface. The objective is to forward the traffic from to

From what I understand from the IP Address page of the documentation, global should be "valid everywhere". Is this incorrect?

The relevant section of ip a:

2: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether [...]
    inet brd scope global ens5
       valid_lft forever preferred_lft forever
3: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether [...]
    inet brd scope global ens6
       valid_lft forever preferred_lft forever
4: vpn: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    inet scope global homeforward
       valid_lft forever preferred_lft forever

My routing tables:

$ ip r
default via dev ens5 proto static dev homeforward proto kernel scope link src dev ens6 proto kernel scope link src dev ens5 proto kernel scope link src
$ ip r ls table 1000
default via dev ens5 proto static dev ens5 proto static scope link
$ ip r ls table 1001
default via dev ens6 proto static dev ens6 proto static scope link

2 Answers 2


Linux' route stateless NAT aka "IP NAT dumb" doesn't work (anymore) for a simple reason: the man page tells:

Warning: Route NAT is no longer supported in Linux 2.6.

So examples like this one:

[root@masq-gw]# ip route add nat via
[root@masq-gw]# ip rule add nat from
[root@masq-gw]# ip route flush cache
[root@masq-gw]# ip route show table all | grep ^nat
nat via  table local  scope host
[root@masq-gw]# ip rule show
0:      from all lookup local 
32765:  from lookup main map-to 
32766:  from all lookup main 
32767:  from all lookup 253

won't work anymore.

This feature has been added in kernel 2.1.15 (1996) and removed in kernel 2.6.9 (2004) (but probably a few kernels before):

<[email protected]>
  [IPV4]: Kill remnant of ip_nat_dumb

  This line in net/ipv4/Makefile was left behind when the rest of the
  dumb NAT option was taken out.

  Signed-off-by: Herbert Xu <[email protected]>
  Signed-off-by: David S. Miller <[email protected]>

The explanation is given in an alternate stateless NAT using tc nat added back in kernel 2.6.24:

Previously we had stateless NAT functionality which was integrated into the IPv4 routing subsystem. This was a great solution as long as the NAT worked on a subnet to subnet basis such that the number of NAT rules was relatively small. The reason is that for SNAT the routing based system had to perform a linear scan through the rules.

If the number of rules is large then major renovations would have take place in the routing subsystem to make this practical.


Here's a basic DNAT example I made. The interface where stateless NAT is performed is ens5 (I will not use your setup with two NICs in the same LAN: without additional settings, there would be additional problems related to ARP flux and reverse path filtering. It appears you're aware of this since you have additional routing tables, but let's keep this example simple), local addresses on it are and Any ingress traffic to is DNATed to destination, any egress traffic from is SNATed to source As often done, the prio qdisc is used for its simplicity, not for its priority features.

tc qdisc add dev ens5 ingress
tc filter add dev ens5 ingress protocol ip matchall \
   action nat ingress

tc qdisc add dev ens5 root handle 1: prio
tc filter add dev ens5 parent 1: protocol ip matchall \
   action nat egress

There might be unforeseen interactions with netfilter's stateful connection tracking.


It's also possible instead to do the same using nftables as described in nftables' wiki.

nft add table ip natdumb

nft add chain ip natdumb prerouting '{ type filter hook prerouting priority -350; policy accept; }'
nft add rule ip natdumb prerouting ip daddr ip daddr set

nft add chain ip natdumb postrouting '{ type filter hook postrouting priority 350; policy accept; }'
nft add rule ip natdumb postrouting ip saddr ip saddr set

Again, there might be interactions with netfilter's conntrack. I removed any notrack which might get it still working if there is no netfilter NAT kernel module loaded or at least activated in the current namespace (while having notrack would get all reply packets seen as NEW flow rather than ESTABLISHED). Feel free to experiment.


When you add a route, the kernel checks if it has access to the new scope through a lower scope, because route gateways must be directly reachable on the same L2 (Data Link Layer) connection.

This answer (https://superuser.com/a/1389304) has a good quick clarification of this scope concept and why/when you need to set it up:

These routes are how you tell the Linux kernel what subnet(s) you're in.

This information isn't stored in a hidden "my current subnet" field when you add an IP address; instead it's always converted to a 'scope link' route with no gateway specified (also called "device route" or "interface route"), and whenever the kernel needs to determine whether some address is directly reachable, it just performs a routing table check.

Normally those routes are added automatically as soon as you configure an IP address – for example, running ip addr add assigns the IP address and creates a subnet route for So it's not necessary to add these routes in normal usage.

But when you blindly "delete all routes", you also end up deleting these automatically created routes, and the kernel's "is this address in my subnet" checks no longer work. That's why you end up having to re-add the routes manually.

(The kernel needs to perform this check during ip route add because route gateways (nexthops) must be directly reachable on the same L2 connection – they cannot be behind another gateway. That is, the gateway must be in your subnet.

Route scopes are a generic mechanism to express this restriction: the new route's nexthop needs to be reachable through an existing route with a lower scope. In other words, you must go through a local host (link scope) before you can reach a remote host (global scope).)


But it was not enough: I've been trying to make it work in a test environment without success so far. Error: Invalid scope. keeps happening too.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .