Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

wireguard: allowedips: add WGALLOWEDIP_F_REMOVE_ME flag

The current netlink API for WireGuard does not directly support removal
of allowed ips from a peer. A user can remove an allowed ip from a peer
in one of two ways:

1. By using the WGPEER_F_REPLACE_ALLOWEDIPS flag and providing a new
list of allowed ips which omits the allowed ip that is to be removed.
2. By reassigning an allowed ip to a "dummy" peer then removing that
peer with WGPEER_F_REMOVE_ME.

With the first approach, the driver completely rebuilds the allowed ip
list for a peer. If my current configuration is such that a peer has
allowed ips 192.168.0.2 and 192.168.0.3 and I want to remove 192.168.0.2
the actual transition looks like this.

[192.168.0.2, 192.168.0.3] <-- Initial state
[] <-- Step 1: Allowed ips removed for peer
[192.168.0.3] <-- Step 2: Allowed ips added back for peer

This is true even if the allowed ip list is small and the update does
not need to be batched into multiple WG_CMD_SET_DEVICE requests, as the
removal and subsequent addition of ips is non-atomic within a single
request. Consequently, wg_allowedips_lookup_dst and
wg_allowedips_lookup_src may return NULL while reconfiguring a peer even
for packets bound for ips a user did not intend to remove leading to
unintended interruptions in connectivity. This presents in userspace as
failed calls to sendto and sendmsg for UDP sockets. In my case, I ran
netperf while repeatedly reconfiguring the allowed ips for a peer with
wg.

/usr/local/bin/netperf -H 10.102.73.72 -l 10m -t UDP_STREAM -- -R 1 -m 1024
send_data: data send error: No route to host (errno 113)
netperf: send_omni: send_data failed: No route to host

While this may not be of particular concern for environments where peers
and allowed ips are mostly static, systems like Cilium manage peers and
allowed ips in a dynamic environment where peers (i.e. Kubernetes nodes)
and allowed ips (i.e. pods running on those nodes) can frequently
change making WGPEER_F_REPLACE_ALLOWEDIPS problematic.

The second approach avoids any possible connectivity interruptions
but is hacky and less direct, requiring the creation of a temporary
peer just to dispose of an allowed ip.

Introduce a new flag called WGALLOWEDIP_F_REMOVE_ME which in the same
way that WGPEER_F_REMOVE_ME allows a user to remove a single peer from
a WireGuard device's configuration allows a user to remove an ip from a
peer's set of allowed ips. This enables incremental updates to a
device's configuration without any connectivity blips or messy
workarounds.

A corresponding patch for wg extends the existing `wg set` interface to
leverage this feature.

$ wg set wg0 peer <PUBKEY> allowed-ips +192.168.88.0/24,-192.168.0.1/32

When '+' or '-' is prepended to any ip in the list, wg clears
WGPEER_F_REPLACE_ALLOWEDIPS and sets the WGALLOWEDIP_F_REMOVE_ME flag on
any ip prefixed with '-'.

Signed-off-by: Jordan Rife <jordan@jrife.io>
[Jason: minor style nits, fixes to selftest, bump of wireguard-tools version]
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Link: https://patch.msgid.link/20250521212707.1767879-5-Jason@zx2c4.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>

authored by

Jordan Rife and committed by
Paolo Abeni
ba3d7b93 c8529020

+187 -44
+71 -31
drivers/net/wireguard/allowedips.c
··· 249 249 return 0; 250 250 } 251 251 252 + static void remove_node(struct allowedips_node *node, struct mutex *lock) 253 + { 254 + struct allowedips_node *child, **parent_bit, *parent; 255 + bool free_parent; 256 + 257 + list_del_init(&node->peer_list); 258 + RCU_INIT_POINTER(node->peer, NULL); 259 + if (node->bit[0] && node->bit[1]) 260 + return; 261 + child = rcu_dereference_protected(node->bit[!rcu_access_pointer(node->bit[0])], 262 + lockdep_is_held(lock)); 263 + if (child) 264 + child->parent_bit_packed = node->parent_bit_packed; 265 + parent_bit = (struct allowedips_node **)(node->parent_bit_packed & ~3UL); 266 + *parent_bit = child; 267 + parent = (void *)parent_bit - 268 + offsetof(struct allowedips_node, bit[node->parent_bit_packed & 1]); 269 + free_parent = !rcu_access_pointer(node->bit[0]) && !rcu_access_pointer(node->bit[1]) && 270 + (node->parent_bit_packed & 3) <= 1 && !rcu_access_pointer(parent->peer); 271 + if (free_parent) 272 + child = rcu_dereference_protected(parent->bit[!(node->parent_bit_packed & 1)], 273 + lockdep_is_held(lock)); 274 + call_rcu(&node->rcu, node_free_rcu); 275 + if (!free_parent) 276 + return; 277 + if (child) 278 + child->parent_bit_packed = parent->parent_bit_packed; 279 + *(struct allowedips_node **)(parent->parent_bit_packed & ~3UL) = child; 280 + call_rcu(&parent->rcu, node_free_rcu); 281 + } 282 + 283 + static int remove(struct allowedips_node __rcu **trie, u8 bits, const u8 *key, 284 + u8 cidr, struct wg_peer *peer, struct mutex *lock) 285 + { 286 + struct allowedips_node *node; 287 + 288 + if (unlikely(cidr > bits)) 289 + return -EINVAL; 290 + if (!rcu_access_pointer(*trie) || !node_placement(*trie, key, cidr, bits, &node, lock) || 291 + peer != rcu_access_pointer(node->peer)) 292 + return 0; 293 + 294 + remove_node(node, lock); 295 + return 0; 296 + } 297 + 252 298 void wg_allowedips_init(struct allowedips *table) 253 299 { 254 300 table->root4 = table->root6 = NULL; ··· 346 300 return add(&table->root6, 128, key, cidr, peer, lock); 347 301 } 348 302 303 + int wg_allowedips_remove_v4(struct allowedips *table, const struct in_addr *ip, 304 + u8 cidr, struct wg_peer *peer, struct mutex *lock) 305 + { 306 + /* Aligned so it can be passed to fls */ 307 + u8 key[4] __aligned(__alignof(u32)); 308 + 309 + ++table->seq; 310 + swap_endian(key, (const u8 *)ip, 32); 311 + return remove(&table->root4, 32, key, cidr, peer, lock); 312 + } 313 + 314 + int wg_allowedips_remove_v6(struct allowedips *table, const struct in6_addr *ip, 315 + u8 cidr, struct wg_peer *peer, struct mutex *lock) 316 + { 317 + /* Aligned so it can be passed to fls64 */ 318 + u8 key[16] __aligned(__alignof(u64)); 319 + 320 + ++table->seq; 321 + swap_endian(key, (const u8 *)ip, 128); 322 + return remove(&table->root6, 128, key, cidr, peer, lock); 323 + } 324 + 349 325 void wg_allowedips_remove_by_peer(struct allowedips *table, 350 326 struct wg_peer *peer, struct mutex *lock) 351 327 { 352 - struct allowedips_node *node, *child, **parent_bit, *parent, *tmp; 353 - bool free_parent; 328 + struct allowedips_node *node, *tmp; 354 329 355 330 if (list_empty(&peer->allowedips_list)) 356 331 return; 357 332 ++table->seq; 358 - list_for_each_entry_safe(node, tmp, &peer->allowedips_list, peer_list) { 359 - list_del_init(&node->peer_list); 360 - RCU_INIT_POINTER(node->peer, NULL); 361 - if (node->bit[0] && node->bit[1]) 362 - continue; 363 - child = rcu_dereference_protected(node->bit[!rcu_access_pointer(node->bit[0])], 364 - lockdep_is_held(lock)); 365 - if (child) 366 - child->parent_bit_packed = node->parent_bit_packed; 367 - parent_bit = (struct allowedips_node **)(node->parent_bit_packed & ~3UL); 368 - *parent_bit = child; 369 - parent = (void *)parent_bit - 370 - offsetof(struct allowedips_node, bit[node->parent_bit_packed & 1]); 371 - free_parent = !rcu_access_pointer(node->bit[0]) && 372 - !rcu_access_pointer(node->bit[1]) && 373 - (node->parent_bit_packed & 3) <= 1 && 374 - !rcu_access_pointer(parent->peer); 375 - if (free_parent) 376 - child = rcu_dereference_protected( 377 - parent->bit[!(node->parent_bit_packed & 1)], 378 - lockdep_is_held(lock)); 379 - call_rcu(&node->rcu, node_free_rcu); 380 - if (!free_parent) 381 - continue; 382 - if (child) 383 - child->parent_bit_packed = parent->parent_bit_packed; 384 - *(struct allowedips_node **)(parent->parent_bit_packed & ~3UL) = child; 385 - call_rcu(&parent->rcu, node_free_rcu); 386 - } 333 + list_for_each_entry_safe(node, tmp, &peer->allowedips_list, peer_list) 334 + remove_node(node, lock); 387 335 } 388 336 389 337 int wg_allowedips_read_node(struct allowedips_node *node, u8 ip[16], u8 *cidr)
+4
drivers/net/wireguard/allowedips.h
··· 38 38 u8 cidr, struct wg_peer *peer, struct mutex *lock); 39 39 int wg_allowedips_insert_v6(struct allowedips *table, const struct in6_addr *ip, 40 40 u8 cidr, struct wg_peer *peer, struct mutex *lock); 41 + int wg_allowedips_remove_v4(struct allowedips *table, const struct in_addr *ip, 42 + u8 cidr, struct wg_peer *peer, struct mutex *lock); 43 + int wg_allowedips_remove_v6(struct allowedips *table, const struct in6_addr *ip, 44 + u8 cidr, struct wg_peer *peer, struct mutex *lock); 41 45 void wg_allowedips_remove_by_peer(struct allowedips *table, 42 46 struct wg_peer *peer, struct mutex *lock); 43 47 /* The ip input pointer should be __aligned(__alignof(u64))) */
+25 -12
drivers/net/wireguard/netlink.c
··· 46 46 static const struct nla_policy allowedip_policy[WGALLOWEDIP_A_MAX + 1] = { 47 47 [WGALLOWEDIP_A_FAMILY] = { .type = NLA_U16 }, 48 48 [WGALLOWEDIP_A_IPADDR] = NLA_POLICY_MIN_LEN(sizeof(struct in_addr)), 49 - [WGALLOWEDIP_A_CIDR_MASK] = { .type = NLA_U8 } 49 + [WGALLOWEDIP_A_CIDR_MASK] = { .type = NLA_U8 }, 50 + [WGALLOWEDIP_A_FLAGS] = NLA_POLICY_MASK(NLA_U32, __WGALLOWEDIP_F_ALL), 50 51 }; 51 52 52 53 static struct wg_device *lookup_interface(struct nlattr **attrs, ··· 330 329 static int set_allowedip(struct wg_peer *peer, struct nlattr **attrs) 331 330 { 332 331 int ret = -EINVAL; 332 + u32 flags = 0; 333 333 u16 family; 334 334 u8 cidr; 335 335 ··· 339 337 return ret; 340 338 family = nla_get_u16(attrs[WGALLOWEDIP_A_FAMILY]); 341 339 cidr = nla_get_u8(attrs[WGALLOWEDIP_A_CIDR_MASK]); 340 + if (attrs[WGALLOWEDIP_A_FLAGS]) 341 + flags = nla_get_u32(attrs[WGALLOWEDIP_A_FLAGS]); 342 342 343 343 if (family == AF_INET && cidr <= 32 && 344 - nla_len(attrs[WGALLOWEDIP_A_IPADDR]) == sizeof(struct in_addr)) 345 - ret = wg_allowedips_insert_v4( 346 - &peer->device->peer_allowedips, 347 - nla_data(attrs[WGALLOWEDIP_A_IPADDR]), cidr, peer, 348 - &peer->device->device_update_lock); 349 - else if (family == AF_INET6 && cidr <= 128 && 350 - nla_len(attrs[WGALLOWEDIP_A_IPADDR]) == sizeof(struct in6_addr)) 351 - ret = wg_allowedips_insert_v6( 352 - &peer->device->peer_allowedips, 353 - nla_data(attrs[WGALLOWEDIP_A_IPADDR]), cidr, peer, 354 - &peer->device->device_update_lock); 344 + nla_len(attrs[WGALLOWEDIP_A_IPADDR]) == sizeof(struct in_addr)) { 345 + if (flags & WGALLOWEDIP_F_REMOVE_ME) 346 + ret = wg_allowedips_remove_v4(&peer->device->peer_allowedips, 347 + nla_data(attrs[WGALLOWEDIP_A_IPADDR]), cidr, 348 + peer, &peer->device->device_update_lock); 349 + else 350 + ret = wg_allowedips_insert_v4(&peer->device->peer_allowedips, 351 + nla_data(attrs[WGALLOWEDIP_A_IPADDR]), cidr, 352 + peer, &peer->device->device_update_lock); 353 + } else if (family == AF_INET6 && cidr <= 128 && 354 + nla_len(attrs[WGALLOWEDIP_A_IPADDR]) == sizeof(struct in6_addr)) { 355 + if (flags & WGALLOWEDIP_F_REMOVE_ME) 356 + ret = wg_allowedips_remove_v6(&peer->device->peer_allowedips, 357 + nla_data(attrs[WGALLOWEDIP_A_IPADDR]), cidr, 358 + peer, &peer->device->device_update_lock); 359 + else 360 + ret = wg_allowedips_insert_v6(&peer->device->peer_allowedips, 361 + nla_data(attrs[WGALLOWEDIP_A_IPADDR]), cidr, 362 + peer, &peer->device->device_update_lock); 363 + } 355 364 356 365 return ret; 357 366 }
+48
drivers/net/wireguard/selftest/allowedips.c
··· 460 460 wg_allowedips_insert_v##version(&t, ip##version(ipa, ipb, ipc, ipd), \ 461 461 cidr, mem, &mutex) 462 462 463 + #define remove(version, mem, ipa, ipb, ipc, ipd, cidr) \ 464 + wg_allowedips_remove_v##version(&t, ip##version(ipa, ipb, ipc, ipd), \ 465 + cidr, mem, &mutex) 466 + 463 467 #define maybe_fail() do { \ 464 468 ++i; \ 465 469 if (!_s) { \ ··· 588 584 test_negative(4, a, 128, 0, 0, 0); 589 585 test_negative(4, a, 192, 0, 0, 0); 590 586 test_negative(4, a, 255, 0, 0, 0); 587 + 588 + insert(4, a, 1, 0, 0, 0, 32); 589 + insert(4, a, 192, 0, 0, 0, 24); 590 + insert(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128); 591 + insert(6, a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98); 592 + test(4, a, 1, 0, 0, 0); 593 + test(4, a, 192, 0, 0, 1); 594 + test(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef); 595 + test(6, a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010); 596 + /* Must be an exact match to remove */ 597 + remove(4, a, 192, 0, 0, 0, 32); 598 + test(4, a, 192, 0, 0, 1); 599 + /* NULL peer should have no effect and return 0 */ 600 + test_boolean(!remove(4, NULL, 192, 0, 0, 0, 24)); 601 + test(4, a, 192, 0, 0, 1); 602 + /* different peer should have no effect and return 0 */ 603 + test_boolean(!remove(4, b, 192, 0, 0, 0, 24)); 604 + test(4, a, 192, 0, 0, 1); 605 + /* invalid CIDR should have no effect and return -EINVAL */ 606 + test_boolean(remove(4, b, 192, 0, 0, 0, 33) == -EINVAL); 607 + test(4, a, 192, 0, 0, 1); 608 + remove(4, a, 192, 0, 0, 0, 24); 609 + test_negative(4, a, 192, 0, 0, 1); 610 + remove(4, a, 1, 0, 0, 0, 32); 611 + test_negative(4, a, 1, 0, 0, 0); 612 + /* Must be an exact match to remove */ 613 + remove(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 96); 614 + test(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef); 615 + /* NULL peer should have no effect and return 0 */ 616 + test_boolean(!remove(6, NULL, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128)); 617 + test(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef); 618 + /* different peer should have no effect and return 0 */ 619 + test_boolean(!remove(6, b, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128)); 620 + test(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef); 621 + /* invalid CIDR should have no effect and return -EINVAL */ 622 + test_boolean(remove(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 129) == -EINVAL); 623 + test(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef); 624 + remove(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128); 625 + test_negative(6, a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef); 626 + /* Must match the peer to remove */ 627 + remove(6, b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98); 628 + test(6, a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010); 629 + remove(6, a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98); 630 + test_negative(6, a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010); 591 631 592 632 wg_allowedips_free(&t, &mutex); 593 633 wg_allowedips_init(&t);
+9
include/uapi/linux/wireguard.h
··· 101 101 * WGALLOWEDIP_A_FAMILY: NLA_U16 102 102 * WGALLOWEDIP_A_IPADDR: struct in_addr or struct in6_addr 103 103 * WGALLOWEDIP_A_CIDR_MASK: NLA_U8 104 + * WGALLOWEDIP_A_FLAGS: NLA_U32, WGALLOWEDIP_F_REMOVE_ME if 105 + * the specified IP should be removed; 106 + * otherwise, this IP will be added if 107 + * it is not already present. 104 108 * 0: NLA_NESTED 105 109 * ... 106 110 * 0: NLA_NESTED ··· 188 184 }; 189 185 #define WGPEER_A_MAX (__WGPEER_A_LAST - 1) 190 186 187 + enum wgallowedip_flag { 188 + WGALLOWEDIP_F_REMOVE_ME = 1U << 0, 189 + __WGALLOWEDIP_F_ALL = WGALLOWEDIP_F_REMOVE_ME 190 + }; 191 191 enum wgallowedip_attribute { 192 192 WGALLOWEDIP_A_UNSPEC, 193 193 WGALLOWEDIP_A_FAMILY, 194 194 WGALLOWEDIP_A_IPADDR, 195 195 WGALLOWEDIP_A_CIDR_MASK, 196 + WGALLOWEDIP_A_FLAGS, 196 197 __WGALLOWEDIP_A_LAST 197 198 }; 198 199 #define WGALLOWEDIP_A_MAX (__WGALLOWEDIP_A_LAST - 1)
+29
tools/testing/selftests/wireguard/netns.sh
··· 611 611 } < <(n0 wg show wg0 allowed-ips) 612 612 ip0 link del wg0 613 613 614 + allowedips=( ) 615 + for i in {1..197}; do 616 + allowedips+=( 192.168.0.$i ) 617 + allowedips+=( abcd::$i ) 618 + done 619 + saved_ifs="$IFS" 620 + IFS=, 621 + allowedips="${allowedips[*]}" 622 + IFS="$saved_ifs" 623 + ip0 link add wg0 type wireguard 624 + n0 wg set wg0 peer "$pub1" allowed-ips "$allowedips" 625 + n0 wg set wg0 peer "$pub1" allowed-ips -192.168.0.1/32,-192.168.0.20/32,-192.168.0.100/32,-abcd::1/128,-abcd::20/128,-abcd::100/128 626 + { 627 + read -r pub allowedips 628 + [[ $pub == "$pub1" ]] 629 + i=0 630 + for ip in $allowedips; do 631 + [[ $ip != "192.168.0.1" ]] 632 + [[ $ip != "192.168.0.20" ]] 633 + [[ $ip != "192.168.0.100" ]] 634 + [[ $ip != "abcd::1" ]] 635 + [[ $ip != "abcd::20" ]] 636 + [[ $ip != "abcd::100" ]] 637 + ((++i)) 638 + done 639 + ((i == 388)) 640 + } < <(n0 wg show wg0 allowed-ips) 641 + ip0 link del wg0 642 + 614 643 ! n0 wg show doesnotexist || false 615 644 616 645 ip0 link add wg0 type wireguard
+1 -1
tools/testing/selftests/wireguard/qemu/Makefile
··· 43 43 $(eval $(call tar_download,IPTABLES,iptables,1.8.7,.tar.bz2,https://www.netfilter.org/projects/iptables/files/,c109c96bb04998cd44156622d36f8e04b140701ec60531a10668cfdff5e8d8f0)) 44 44 $(eval $(call tar_download,NMAP,nmap,7.92,.tgz,https://nmap.org/dist/,064183ea642dc4c12b1ab3b5358ce1cef7d2e7e11ffa2849f16d339f5b717117)) 45 45 $(eval $(call tar_download,IPUTILS,iputils,s20190709,.tar.gz,https://github.com/iputils/iputils/archive/s20190709.tar.gz/#,a15720dd741d7538dd2645f9f516d193636ae4300ff7dbc8bfca757bf166490a)) 46 - $(eval $(call tar_download,WIREGUARD_TOOLS,wireguard-tools,1.0.20210914,.tar.xz,https://git.zx2c4.com/wireguard-tools/snapshot/,97ff31489217bb265b7ae850d3d0f335ab07d2652ba1feec88b734bc96bd05ac)) 46 + $(eval $(call tar_download,WIREGUARD_TOOLS,wireguard-tools,1.0.20250521,.tar.xz,https://git.zx2c4.com/wireguard-tools/snapshot/,b6f2628b85b1b23cc06517ec9c74f82d52c4cdbd020f3dd2f00c972a1782950e)) 47 47 48 48 export CFLAGS := -O3 -pipe 49 49 ifeq ($(HOST_ARCH),$(ARCH))