Route Local-Interface-Destined Packets to Gateway

Without using raw sockets, is there a way on Linux to force a network packet to be physically sent to a gateway first, even if the destination address is one bound to a local interface?

My system has a single active interface, eno1, which is assigned an IPv4 of 192.168.1.1 with a /20 mask. The gateway has address 192.168.0.1. I have tried adding a route using ip route add 192.168.1.0/24 via 192.168.0.1, which has the highest routing specificity for 192.168.1.1. But calling traceroute 192.168.1.1 still just shows a single hop, indicating it didn't go anywhere. I would expect it to show a first hop of 192.168.0.1, followed by 192.168.1.1. Additional evidence for the route not being followed is that sending packets with SO_TIMESTAMPING results in missing hardware timestamps, indicating the packet wasn't actually put onto the wire.

In case this is an XY problem, I'm trying to build a ping-like measurement tool that measures round-trip latency and packet loss to my ISP's CMTS, but the router there uses rate-limiting on ICMP responses, so I can't measure the latency as frequently as I'd like. Additionally, pings from local devices on my network count against the rate limit, leading to spurious dropped packets (though I suppose I could block forwarding packets with the CMTS as the destination or those with TTL=1). Note that while I'm testing this locally on a system on my private network using my own router as the first hop, in reality I'll be running this on the router itself, replacing the .1.1 address with my public IPv4 address, and the .0.1 address with the upstream router address of the CMTS.

My thought was to send out a packet to the upstream router with a destination of my own public IP, which would hopefully result in it being sent right back to my own router, having traversed each leg once, but appearing to the ISP as generic forwarded traffic rather than something the CMTS responds to directly.

Ideally the solution can be configured once as super-user, then used by userspace jobs, i.e. not require running the tool as super-user (hence the desire to avoid raw-sockets).


Solution 1:

Without using raw sockets, is there a way on Linux to force a network packet to be physically sent to a gateway first, even if the destination address is one bound to a local interface?

Create a separate network namespace:

ip netns add foo

Create a virtual Ethernet interface on top of the physical one, then move it into the netns:

ip link add eno1.foo link eno1 type macvlan mode private
ip link set eno1.foo netns foo

Configure your IP address:

ip netns exec foo ip link set eno1.foo up
ip netns exec foo ip addr add 192.168.1.7/24 eno1.foo

Run the program:

ip netns exec foo ~/pingthing -i eno1.foo

(A 'persistent' namespace is not actually needed – you could use unshare --net <program>, and move the virtual interface into it by PID – but that's somewhat less convenient.)

I have tried adding a route using ip route add 192.168.1.0/24 via 192.168.0.1, which has the highest routing specificity for 192.168.1.1. But calling traceroute 192.168.1.1 still just shows a single hop, indicating it didn't go anywhere.

Before routing, all packets are checked against the rules set in ip -4 rule. By default the first rule sends them first through the special 'local' routing table (which contains loopback routes for all "local" IP addresses).

Routes in the 'main' routing table are only used if there is no match in the 'local' table.

Ideally the solution can be configured once as super-user, then used by userspace jobs, i.e. not require running the tool as super-user (hence the desire to avoid raw-sockets).

Give the tool just the CAP_NET_RAW privilege using setcap cap_net_raw=ep if it's a CLI tool or using systemd's AmbientCapabilities= if it's a service. Then you can use raw sockets without gaining any other root privileges.

The former (setcap on the executable) is often used by ping and mtr tools, so this isn't actually anything new.

(Embedded filesystems might not support file-level "capabilities", but in that case you can still set the setuid bit. It needs more care about safety since your tool does get full root privileges even when run by a normal user.)

setcap/suid only work with compiled binary executables, not with interpreted script executables.