Modern proxy (IPv4) ARP and proxy IPv6 NDP on Linux

November 21, 2023

Suppose, not hypothetically, that you have a remote system (on the other side of some tunnel or other connection) that wants to pretend to be on the local network, for either or both of IPv4 and IPv6. To make this work smoothly, this remote system's gateway (on the local network) needs to answer ARP requests for this remote system's IPv4 address and/or NDP requests for the remote system's IPv6 address. This is called 'proxy ARP' or 'proxy NDP', because the gateway is acting as an ARP or NDP proxy for the remote system.

At this point my memories are vague, but I think that in the old days, configuring proxy ARP on Linux was somewhat challenging and obscure, requiring you to add various magic settings in various places. These days it has gotten much easier and more uniform, and there are at least two approaches, the by hand one and the systemd one, although it turns out I don't know how to make systemd work for the IPv4 proxy ARP case.

The by hand approach is with the ip neighbour (sub)command. This can be used to add IPv4 or IPv6 proxy announcements to some network, which is normally the network the remote machine is pretending to be on:

ip neigh add proxy 128.X.Y.Z dev em0
ip neigh add proxy 2606:fa00:.... dev em0

# apparently necessary
echo 1 >/proc/sys/net/ipv6/conf/em0/proxy_arp

Here em0 is the interface that the 128.X.Y.0/24 and 2606:fa00:.../64 networks are on, where we want other machines to see 128.X.Y.Z (and its IPv6 version) as being on the network.

You can see these proxies (if any) with 'ip neigh show proxy'. To actually be useful, the system doing proxy ARP also generally needs to have IP forwarding turned on and to have appropriate routes or other ways to get packets to the IP it's proxying for.

Although there is a /proc/sys/net/ipv4/conf/*/proxy_arp setting (cf), it appears to be unimportant in today's modern 'ip neighbour' based setup. One of my machines is happily doing proxy ARP with this at the default of '0' on all interfaces. IPv6 has a similar ipv6/conf/*/proxy_ndp, but unlike with IPv4, the setting here appears to matter and you have to turn it on on the relevant interface; it's on for the relevant interface on my IPv6 gateway and turning it off makes external pings stop working.

(It's possible that other settings are affecting my lack of need for proxy_arp in my IPv4 case.)

The systemd way is to set up a systemd-networkd .network file that has the relevant settings. You set this on the interface where you want the proxy ARP or NDP to be on, not on the tunnel interface to the remote machine (as I found out). For IPv6, you want to set IPv6ProxyNDP= and at least one IPv6ProxyNDPAddress=, although it's not strictly necessary to explicitly set IPv6ProxyNDP (I'd do it for clarity). I was going to write something about how to do this for IPv4, but I can't actually work out how to do the equivalent of 'ip neigh add proxy ...' in systemd .network files; all they appear to do is support turning on proxy ARP in general, and I'm not sure what this does these days.

(If it's like eg this old discussion, then it may cause Linux to do proxy ARP for anything that it has routes for. There's also this Debian Wiki page suggesting the same thing.)

I don't know if NetworkManager has much support for proxy ARP or proxy NDP, since both seem somewhat out of scope for it.

PS: The systemd-networkd approach for IPv6 proxy NDP definitely results in an appropriate entry in 'ip -6 neigh show proxy', so it's not just turning on some form of general proxy NDP and calling it a day. That's certainly what I'd expect given that you list one or more proxy NDP addresses, but I like to verify these things.

Comments on this page:

From at 2023-11-22 04:40:55:

Although there is a /proc/sys/net/ipv4/conf/*/proxy_arp setting (cf), it appears to be unimportant in today's modern 'ip neighbour' based setup

As I understand it, this is still relevant if you want to proxy everything that is not routed through the same interface (according to the routing table), Cisco style. That is, both methods are equally "current" but for different use cases.

(For example, if you have your single /128 routed through wg0, then the proxyndp_ sysctl would presumably do the proxying according to the routing table.)

I never quite got the hang of the sysctls, though. For our main gateway (which has, Because Of Reasons, one subnet split across two broadcast domains), we use parpd (from the dhcpcd developer), which makes it really easy to proxy CIDR ranges rather just individual addresses. The same for IPv6 exists as ndppd or ndpresponder.

(Proxy-NDP has one quirk in that the replies use a different source address than "real" ones. On one of my personal VPS servers, I had to use ndpresponder which handcrafts "real-looking" NDP responses so that I could re-use the /64 for a VPN.)

For IPv6, you want to set IPv6ProxyNDP= and at least one IPv6ProxyNDPAddress=,

Huh, that's a strange way to implement it. They should've added Proxy= as an option under the [Neighbor] section...

Written on 21 November 2023.
« Why I'm still using the "Certainly Something" addon for Firefox
Understanding and sorting out ZFS pool features »

Page tools: View Source, View Normal, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Tue Nov 21 23:32:40 2023
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.