In my box, I have two ethernet interfaces. Due to the constraints of the system I am dealing with, I need all ICMP response messages to have the source address of 1 of the interfaces.

An example:

A packet with a TTL of 0 comes in on interface 0, it gets dropped and an ICMP response is generated with the source address of 0 and is routed out 0. Another packet comes in with a TTL of 0 on interface 1, it gets dropped and an ICMP response is generated. Instead of the source address being interface 1, I need it to be interface 0 and still be routed out interface 1. Is this possible?

I don't want it to affect the source addresses of other protocols coming out of either interface.


Here is the current setup of the particular box:

ip -br link

lo           UNKNOWN   00:00:00:00:00:00 <LOOPBACK, UP, LOWER_UP>

ip -4 -br addr

lo           UNKNOWN
np0          UNKNOWN
np1          UNKNOWN
eth0         UP

ip route

default dev np1 scope linke dev np0 proto kernel scope link src dev np1 proto kernel scope link src dev eth0 proto kernel scope link src

The system is not setup to run a VPN. The system has 1 physical ethernet (eth0) and 2 virtual ethernet interfaces (np0 and np1).

Using the naming from above, let interface 0 be eth0 and interface 1 be np1

  • Can you edit your question and provide your current setup on this box (eg: ip -br link; ip -4 -br addr; ip route)? You can obfuscate public addresses (eg: using dedicated or if needed. This would make easier to illustrate an explanation and avoid missing something that wouldn't be expected. Details might matter for this. Especially if you omitted some information (eg: using a VPN, etc.)
    – A.B
    Oct 21, 2023 at 22:19
  • No information to provide? Not interested anymore?
    – A.B
    Oct 28, 2023 at 15:42
  • Sorry, I got caught up with work and couldn't get access to the box. I have updated the question with system configuration
    – WakkaTrout
    Oct 28, 2023 at 20:41
  • I wonder why the obfuscation is so obfuscate: obviously above eth0 is not Ethernet but a layer 3 interface (with a tunnel going on) or it wouldn't be POINTOPOINT + NOARP.
    – A.B
    Oct 28, 2023 at 21:34
  • is managed by SUNET in Sweden Oct 28, 2023 at 22:08

1 Answer 1


Routing rules based on L4 protocol

Linux kernel >= 4.17 can do policy routing based on IP's layer 4 protocol:

  • Extends fib rule match support to include sport, dport and ip proto match (to complete the 5-tuple match support). Common use-cases of Policy based routing in the data center require 5-tuple match


Initially, any ICMP response generated from the system (named Box) back toward the original source node would use the address on the interface facing this source just like any emitted traffic from the host to this node.

# ip route get to dev np1 src uid 0 
# ip route get to dev eth0 src uid 0 

One can add a routing table that will present a duplicated but modified entry of the relevant route, with a different hinted source address than what would normally be used, then select this routing table with a routing rule, but only when the protocol is ICMP. Note that this routing table should be populated after addresses and (normal) routes are configured, so it won't be rejected as an impossible route when added. As usual if this route disappears for any reason (eg: interface brought down then up), contrary to proto kernel routes in the main routing table, it won't be added back: it must be added again whenever such event happens.

Here the behavior to change on the duplicated route for traffic that goes out using the np1 interface is to hint a different source address ( instead of

ip route add dev np1 src table 1000

And this should be applied to override the main table for ICMP only:

ip rule add pref 100 ipproto icmp lookup 1000

Nothing changes for non-ICMP traffic to np1:

# ip route get to dev np1 src uid 0 

but does for ICMP traffic to np1:

# ip route get to ipproto icmp dev np1 table 1000 src uid 0 

So the two cases in OP's question are fulfilled: ICMP, such as ICMP Time exceeded, are replied from hinted eth0's address whether replying on eth0 or on np1.

This also affects locally-initiated ICMP such as pings (but see later).

Caveat and details

  • caveat: (layer 2) Ethernet interfaces and ARP

    If np1 were an Ethernet interface, ARP requests directly triggered from such ICMP errors also follow routing rules and will appear above as requesting for example from rather than from, but only if there's no ARP entry for the node existing yet on Box and if the node didn't do itself an ARP request to resolve Box right before (ie this will happen only if the node did have Box in ARP cache but Box didn't have the node in ARP cache, which usually is not the case). Other Linux systems won't care by default (they follow the weak host model and accept any such ARP requests by default). Some other OS might care and not reply this single "wrong" ARP request.

    If this really matters, this already rare case can be completely prevented with arp_announce=1:

    - 1 - Try to avoid local addresses that are not in the target's
      subnet for this interface. [...]

    So for the np1 interface:

    sysctl -w net.ipv4.conf.np1.arp_announce=1
  • The ping command will also use the changed source address

    ping is not an ICMP response, so should be left alone working as usual.

    One can still restrict the ICMP types (and codes) affected with a not-really-documented option: by mapping ICMP type+code as destination port in the routing rule's selector, according to how it's handled in sources:


    union flowi_uli {
        struct {
            __be16  dport;
            __be16  sport;
        } ports;
        struct {
            __u8    type;
            __u8    code;
        } icmpt;
    union flowi_uli     uli;
    #define fl4_sport       uli.ports.sport
    #define fl4_dport       uli.ports.dport
    #define fl4_icmp_type       uli.icmpt.type
    #define fl4_icmp_code       uli.icmpt.code


        if (fib_rule_port_range_set(&rule->dport_range) &&
            !fib_rule_port_inrange(&rule->dport_range, fl4->fl4_dport))
            return 0;

    Because of the union, fl4->fl4_dport <=> fl4_icmp_type * 256 + fl4_icmp_code: ip rule ... ipproto icmp dport ... can be used for type+code which doesn't have a dedicated syntax.

    So just limiting it to anything but type 8 (ICMP request) means limiting it to 0-7 + 9-255. Once multiplied by 256 and adequate end interval adjusted (+255) this gives 1-2047 (because 0 is invalid, starting at 1. ICMP reply which is type 0 code 0 won't match but doesn't have to match, because as an actual reply it will never use the hinted source, but the actual local destination that was used in the remote ICMP request and that will be used as reply source) + 2304-65534 (ditto for 65535). Starting from the original routing rule:

    ip rule add pref 100 ipproto icmp lookup 1000
    • either replace it with two rules:

      ip rule del pref 100
      ip rule add pref 100 ipproto icmp dport 1-2047 lookup 1000
      ip rule add pref 101 ipproto icmp dport 2304-65534 lookup 1000
    • or else keep the initial rule and add rules around it to skip it (could have been simplified to only 2048 since ICMP Echo request's code is always 0):

      ip rule del pref 100
      ip rule del pref 101
      ip rule add pref 100 ipproto icmp lookup 1000
      ip rule add pref 101 lookup 424242
      ip rule add pref 99 ipproto icmp dport 2048-2303 goto 101

    With either choices a ping command won't be changed anymore:

    # ip route get to ipproto icmp dport 2048 dev np1 src uid 0 

    ICMP redirect (type 5) should probably also be added to the exceptions in a similar way.

You must log in to answer this question.

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