One line on a PPP interface settles most of the routing argument on a VPN box: the inner address and the peer printed beside it. Read that pair as a tiny subnet and you will look for a gateway that does not exist. Read it as the two ends of a single link and the next hop becomes obvious. The comparison here is between those two readings, because every table you build afterwards inherits whichever one you picked. Nothing about the outer VPN endpoint is decided by either of them.

Two readings of one interface line

Take a session where the interface reports inet 10.64.3.9 peer 10.64.3.1/32. The subnet reading treats 10.64.3.9 as a lone host on a /32 island and then hunts for a router to reach anything beyond it. Since the only other address on the line is the peer, that reading ends up proposing 10.64.3.9 itself as the gateway, which is circular.

The endpoint reading is the documented one. On a point-to-point interface, peer names the remote end of that one link, so 10.64.3.1 is the usable next hop and 10.64.3.9 is only ever a source address.

That difference governs what may appear in via. Writing the local address there produces a route the kernel cannot resolve to a neighbour, so the entry either fails validation or blackholes traffic while looking perfectly healthy in a listing. The peer works because it is a real destination on the link. Both readings agree on the outer VPN endpoint, which is a separate identity entirely: it is reached through the underlay, not through the tunnel it terminates.

What the session already installed

Before writing any table of your own, print what came with the link. A supported PPP integration usually installs a device route and a peer route during session setup, and those two entries already encode the correct reading.

ip -4 addr show dev ppp0
ip route show dev ppp0
ip route show table main | head -n 5

A device route such as 10.64.3.1 dev ppp0 proto kernel scope link src 10.64.3.9 carries no gateway because the interface itself is the path. A route that names the peer with via is a different object with a different lifetime, and it disappears when the session drops. Copying either form into a persistent table without checking how it is reinstalled is the usual reason a working tunnel loses its route after a reconnect.

Compare the two listings rather than trusting memory. If the peer route exists in main but your extra table is empty, you have learned something the address line never told you.

A rule and a table are separate decisions

Selecting a table and filling it are independent steps, and the comparison matters here because the two are often confused during debugging. A rule that sends marked traffic to table 100 does nothing if table 100 has no matching route, and a well-populated table is unreachable if no rule points at it.

Half a default route is not a default route. A single 0.0.0.0/1 covers the lower half of the IPv4 space, so a policy that intends to capture everything needs the matching 128.0.0.0/1 as well, or a genuine default. Reading a working configuration and copying one of the two halves is how people end up with a routing loop that only breaks on certain destinations.

Leave deliberate exceptions for the addresses that must stay on the LAN, and decide what happens when the lookup finds nothing in the selected table. An explicit fallback route is easier to reason about than an empty table, which returns an unreachable result and makes the application look broken rather than misrouted.

Ask the lookup the question your application asks

ip route get resolves a path; ip route show lists stored entries, and the two can disagree without either being wrong. Include the source context, because source-based rules only match when the lookup carries a source.

ip route get 198.51.100.23 from 10.64.3.9
ip route get 198.51.100.23 from 10.64.3.9 iif ppp0
ip -4 rule show

The first command asks the host-originated question. The second adds an ingress interface, which is closer to what forwarded traffic experiences, and the answer will often point at a different table. Neither destination is a live test service; 198.51.100.23 is a documentation address, so substitute a host you are allowed to probe.

Even a correct lookup proves only that a path was chosen. It says nothing about whether the far side accepts the traffic or whether the reply can return through the same reasoning.

Where the comparison flips

Host traffic and forwarded traffic do not just take different lookups; they meet different filters on the way back. Linux applies reverse-path validation using the stricter of the all-interface and receiving-interface settings, so changing one interface's value may have no visible effect. Mark-based routing adds the src_valid_mark relationship, where the mark has to match for a source to be considered valid.

If replies vanish while outbound routing looks right, read the effective values before touching them, and change them only with a stated policy reason.

sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.ppp0.rp_filter
sysctl net.ipv4.conf.all.src_valid_mark

Choose between the two designs on evidence: if the application binds to the PPP address, a source rule plus a table holding the peer route is enough. If the box forwards for a LAN behind it, you also need the ingress context and the return path to agree. When they disagree, fix the ownership of the route rather than loosening validation for the whole machine.

Inyandiko zihuye