Thread (7 messages) 7 messages, 2 authors, 10h ago

ipv4: icmp: icmp_route_lookup() relookups pick wrong netdev with policy routing + strict rp_filter

From: Muhammad Ziad <hidden>
Date: 2026-07-15 15:30:47
Also in: lkml

Hello,

There appears to be a bug in mainline Linux in ICMP reverse-path
relookup logic inside icmp_route_lookup() (called by __icmp_send())
when strict rp_filter setting is in place.

When Linux forwards a packet between two interfaces and needs to
generate an ICMP error, icmp_route_lookup() performs a "secondary"
reverse-path lookup to find a suitable route back towards the original
source via ip_route_input(). To simulate the reverse path, the kernel
derives the incoming netdev by calling ip_route_output_key() with a
decoy flow that has *only* daddr assigned in it:

    struct flowi4 fl4_2 = {};
    fl4_2.daddr = fl4_dec.saddr;
    rt2 = ip_route_output_key(net, &fl4_2); /* no saddr */
    ...
    ip_route_input(skb_in, fl4_dec.daddr, fl4_dec.saddr,
                    dscp, rt2->dst.dev);

This can lead to a mismatch between rt2->dst.dev and the netdev the
real reverse packet would use once routing policy rules are in
effect. With strict rp_filter, passing the wrong netdev to
ip_route_input() causes the relookup to fail and a "martian source"
message to be logged, after which icmp_route_lookup() falls back to
the earlier output route lookup (relookup_failed).

In such a scenario, I would expect the relookup to use a canonical
netdev and the kernel to not produce spurious "martian source" log
messages as a result. This suggests the decoy flow would possibly
need to carry saddr too so that ip_route_output_key() is able to
resolve the right netdev.

I tested this on: Ubuntu kernel 6.17.0-35-generic.

Here is a reproducer script that sets up two net namespaces: a
"forwarder" with two routes to the same dst in separate routing tables
picked according to saddr, and a "sender" netns behind it which sends
a ping with ttl=1 via the forwarder forcing it to generate an ICMP
error as a response which leads to the result explained above:

    #!/bin/bash

    if [ "${forwarder_ns:-}" != "1" ]; then
    exec env forwarder_ns=1 unshare -Urn bash "$0" "$@"
    fi

    SRC=10.0.1.2
    DST=198.51.100.5

    # Current netns is the "forwarder".
    # Create a second namespace for the sender.
    unshare -n sleep 120 &
    cpid=$!
    trap 'kill "$cpid" 2>/dev/null || true' EXIT
    in_ns() { nsenter -t "$cpid" -n "$@"; }

    # veth r0(router) <-> s0(src)
    ip link add s0 type veth peer name r0
    ip link set s0 netns "$cpid"
    ip link set lo up
    ip link set r0 up
    ip addr add 10.0.1.1/24 dev r0
    ip link add dumA type dummy
    ip addr add 203.0.113.1/24 dev dumA
    ip link set dumA up
    ip link add dumB type dummy
    ip addr add 192.0.2.1/24 dev dumB
    ip link set dumB up

    sysctl -q -w net.ipv4.ip_forward=1
    for c in all default r0 dumA dumB; do
    sysctl -q -w "net.ipv4.conf.$c.rp_filter=1"
    sysctl -q -w "net.ipv4.conf.$c.log_martians=1"
    done

    # Destination reachable two ways.
    # Policy rule diverts traffic FROM src to dumB
    ip route add 198.51.100.0/24 dev dumA
    ip route add 198.51.100.0/24 dev dumB table 100
    ip rule add from "$SRC" lookup 100

    # sender namespace setup.
    in_ns ip link set lo up
    in_ns ip link set s0 up
    in_ns ip addr add 10.0.1.2/24 dev s0
    in_ns ip route add default via 10.0.1.1

    # This will trigger a "martian source" log.
    in_ns ping -q -c1 -W2 -t1 "$DST" &>/dev/null


Happy to test patches or provide additional traces, if needed.

Thank you,
Mohamed Ghazy
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help