[PATCH wireguard] wireguard: device: allow userspace to enable IPv6 link-local addresses

Roman Khromenok roma55592 at yandex.ru
Thu Oct 22 11:00:39 UTC 2026


WireGuard interfaces do not get an IPv6 link-local address by default,
which wg_open() achieves by setting addr_gen_mode to
IN6_ADDR_GEN_MODE_NONE on every ndo_open. A side effect is that any mode
chosen by userspace, via sysctl or IFLA_INET6_ADDR_GEN_MODE, is silently
reset on the next down/up cycle. Users running routing protocols over
WireGuard, such as Babel or OSPFv3, need fe80::/64 addresses and today
have to set the mode again after every ifup, which network managers do
not expect to have to do. For example, systemd-networkd cannot honor
LinkLocalAddressing=ipv6 on WireGuard interfaces because of this.

Instead, set the default once, when the inet6_dev is created. That
happens on NETDEV_REGISTER, both when the device is created and every
time it is moved to another network namespace, since addrconf tears down
the inet6_dev on NETDEV_UNREGISTER. So register a netdevice notifier
with a priority just below addrconf's, so that it runs after the
inet6_dev exists. The default behavior is unchanged, but userspace can
now opt in and the setting persists across down/up.

An alternative approach using IFF_NO_ADDRCONF was proposed in [1], but
it removes the ability to have link-local addresses altogether, and it
was reverted because it broke the selftests.

Add a selftest checking that link-local addresses are off by default,
that they can be enabled and survive down/up, and that the default is
restored after moving the device to another namespace.

Link: https://lists.zx2c4.com/pipermail/wireguard/2026-February/009496.html [1]
Link: https://github.com/systemd/systemd/issues/44005
Signed-off-by: Roman Khromenok <roma55592 at yandex.ru>
---
Notes:

One pre-existing corner case is not addressed: if the MTU of a running
WireGuard interface drops below IPV6_MIN_MTU and is raised again, addrconf
destroys the inet6_dev and allocates a new one on NETDEV_CHANGEMTU, with the
default addr_gen_mode, and generates a link-local address in the same
notifier call, before any driver notifier gets a chance to run. Today that
address goes away on the next down/up, because wg_open() resets the mode;
with this patch it stays until userspace changes the mode. I don't see a
clean way to handle this from the driver, so I left it alone, but I'm happy
to look into it if you think it matters.

Tested with the wireguard selftests in QEMU: the new checks pass with this
patch, and the down/up check fails without it.

 drivers/net/wireguard/device.c             | 37 +++++++++++++++++++---
 tools/testing/selftests/wireguard/netns.sh | 14 ++++++++
 2 files changed, 47 insertions(+), 4 deletions(-)

diff --git a/drivers/net/wireguard/device.c b/drivers/net/wireguard/device.c
index 67b07ee2d660..50f108a82a53 100644
--- a/drivers/net/wireguard/device.c
+++ b/drivers/net/wireguard/device.c
@@ -31,7 +31,6 @@ static LIST_HEAD(device_list);
 static int wg_open(struct net_device *dev)
 {
 	struct in_device *dev_v4 = __in_dev_get_rtnl(dev);
-	struct inet6_dev *dev_v6 = __in6_dev_get(dev);
 	struct wg_device *wg = netdev_priv(dev);
 	struct wg_peer *peer;
 	int ret;
@@ -44,8 +43,6 @@ static int wg_open(struct net_device *dev)
 		IN_DEV_CONF_SET(dev_v4, SEND_REDIRECTS, false);
 		IPV4_DEVCONF_ALL(dev_net(dev), SEND_REDIRECTS) = false;
 	}
-	if (dev_v6)
-		dev_v6->cnf.addr_gen_mode = IN6_ADDR_GEN_MODE_NONE;
 
 	mutex_lock(&wg->device_update_lock);
 	ret = wg_socket_init(wg, wg->incoming_port);
@@ -434,6 +431,31 @@ static struct pernet_operations pernet_ops __read_mostly = {
 	.exit_rtnl = wg_netns_exit_rtnl
 };
 
+static int wg_netdevice_notification(struct notifier_block *nb,
+				     unsigned long action, void *data)
+{
+	struct net_device *dev = netdev_notifier_info_to_dev(data);
+	struct inet6_dev *dev_v6;
+
+	if (action != NETDEV_REGISTER || dev->netdev_ops != &netdev_ops)
+		return NOTIFY_DONE;
+
+	/* Disable IPv6 link-local address generation by default, but only
+	 * when the inet6_dev is created, which happens on device creation and
+	 * on every netns change, so that userspace may still opt in.
+	 */
+	dev_v6 = __in6_dev_get(dev);
+	if (dev_v6)
+		WRITE_ONCE(dev_v6->cnf.addr_gen_mode, IN6_ADDR_GEN_MODE_NONE);
+	return NOTIFY_DONE;
+}
+
+static struct notifier_block netdevice_notifier = {
+	.notifier_call = wg_netdevice_notification,
+	/* Run after addrconf has created the inet6_dev. */
+	.priority = ADDRCONF_NOTIFY_PRIORITY - 1
+};
+
 int __init wg_device_init(void)
 {
 	int ret;
@@ -450,12 +472,18 @@ int __init wg_device_init(void)
 	if (ret)
 		goto error_vm;
 
-	ret = rtnl_link_register(&link_ops);
+	ret = register_netdevice_notifier(&netdevice_notifier);
 	if (ret)
 		goto error_pernet;
 
+	ret = rtnl_link_register(&link_ops);
+	if (ret)
+		goto error_netdevice;
+
 	return 0;
 
+error_netdevice:
+	unregister_netdevice_notifier(&netdevice_notifier);
 error_pernet:
 	unregister_pernet_device(&pernet_ops);
 error_vm:
@@ -468,6 +496,7 @@ int __init wg_device_init(void)
 void wg_device_uninit(void)
 {
 	rtnl_link_unregister(&link_ops);
+	unregister_netdevice_notifier(&netdevice_notifier);
 	unregister_pernet_device(&pernet_ops);
 	unregister_random_vmfork_notifier(&vm_notifier);
 	unregister_pm_notifier(&pm_notifier);
diff --git a/tools/testing/selftests/wireguard/netns.sh b/tools/testing/selftests/wireguard/netns.sh
index a8f550aecb35..747a7e23b5fa 100755
--- a/tools/testing/selftests/wireguard/netns.sh
+++ b/tools/testing/selftests/wireguard/netns.sh
@@ -699,6 +699,20 @@ pp ip netns delete $netns2
 pp ip netns add $netns1
 pp ip netns add $netns2
 
+# Ensure that IPv6 link-local addresses are off by default, but can be enabled
+ip1 link add dev wg3 type wireguard
+ip1 link set up dev wg3
+[[ -z $(ip1 -6 addr show dev wg3 scope link) ]]
+n1 bash -c 'printf 3 > /proc/sys/net/ipv6/conf/wg3/addr_gen_mode'
+[[ $(ip1 -6 addr show dev wg3 scope link) == *fe80::* ]]
+ip1 link set down dev wg3
+ip1 link set up dev wg3
+[[ $(ip1 -6 addr show dev wg3 scope link) == *fe80::* ]]
+ip1 link set wg3 netns $netns2
+ip2 link set up dev wg3
+[[ -z $(ip2 -6 addr show dev wg3 scope link) ]]
+ip2 link del wg3
+
 # Ensure there aren't circular reference loops
 ip1 link add wg1 type wireguard
 ip2 link add wg2 type wireguard
-- 
2.47.3



More information about the WireGuard mailing list