* [PATCH net-next 1/2] netlink: add an optional per-netns ROUTE multicast index
[not found] <20261104033526.2917590-1-work_patrick@qq.com>
@ 2026-11-04 3:35 ` patrick
2026-11-04 7:31 ` Paolo Abeni
2026-11-04 3:35 ` [PATCH net-next 2/2] selftests: net: cover ROUTE multicast namespace and switch semantics patrick
1 sibling, 1 reply; 3+ messages in thread
From: patrick @ 2026-11-04 3:35 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, horms, corbet, rdunlap, linux-doc,
skhan, linux-kselftest, linux-api, linux-kernel
NETLINK_ROUTE broadcasts visit subscribers in unrelated network namespaces
before checking whether they can receive a notification.
Maintain one ROUTE subscriber list per network namespace and one global
LISTEN_ALL_NSID list. Select those candidate lists when the new
net.core.netlink_route_mc_index sysctl is enabled. Exclude local sockets
from the second list to avoid duplicate delivery.
Preserve do_one_broadcast and the existing broadcast initialization,
locking, completion and error handling. Change only candidate selection
inside netlink_broadcast_filtered; do not force function inlining or add
separate broadcast setup, completion or indexed-walk helpers.
Use the existing table-grab protection for index maintenance at
subscription, option and removal transitions. Keep both indexes populated
while disabled so switching modes includes existing sockets immediately.
The sysctl defaults to zero, is visible only in init_net and affects all
network namespaces. Other Netlink protocols retain the original walk.
Disabling the switch retains index maintenance and memory costs.
AI assistance: An LLM assisted with implementation adaptation and
simplification.
Signed-off-by: patrick <work_patrick@qq.com>
---
Documentation/admin-guide/sysctl/net.rst | 27 ++++++
net/netlink/af_netlink.c | 109 ++++++++++++++++++++++-
net/netlink/af_netlink.h | 4 +
3 files changed, 137 insertions(+), 3 deletions(-)
diff --git a/Documentation/admin-guide/sysctl/net.rst b/Documentation/admin-guide/sysctl/net.rst
index fe43e8595..0cda347ed 100644
--- a/Documentation/admin-guide/sysctl/net.rst
+++ b/Documentation/admin-guide/sysctl/net.rst
@@ -280,6 +280,33 @@ checksums.
These messages are now emitted at KERN_DEBUG and can generally be enabled
and controlled by the dynamic_debug facility.
+netlink_route_mc_index
+---------------------
+
+Selects the multicast candidate lists used for NETLINK_ROUTE notifications.
+This setting is available only in the initial network namespace and applies
+to notifications originating in all network namespaces.
+
+Values:
+
+ - 0 - walk the original global subscription list (default).
+ - 1 - walk the sender's network namespace subscription list and the
+ global NETLINK_LISTEN_ALL_NSID subscription list, regardless of the
+ number of subscribers.
+
+Both auxiliary indexes are maintained in either mode.
+Existing sockets are therefore included immediately when the index is enabled.
+Each broadcast reads the setting once and completes using its selected path;
+changing the setting does not restart broadcasts already in progress.
+Group membership, namespace, permission and socket filter checks still apply
+in both modes. Other Netlink protocols continue to use their original lists.
+
+The indexed path can reduce notification costs when there are many ordinary
+subscribers in other network namespaces. It may add overhead when most
+subscribers are local or listen across network namespaces. Disabling the
+indexed path does not remove its memory or subscription maintenance costs.
+Without CONFIG_SYSCTL, the setting remains at its default value of 0.
+
netdev_budget
-------------
diff --git a/net/netlink/af_netlink.c b/net/netlink/af_netlink.c
index 1281c42a3..fa21290be 100644
--- a/net/netlink/af_netlink.c
+++ b/net/netlink/af_netlink.c
@@ -31,6 +31,7 @@
#include <linux/string.h>
#include <linux/stat.h>
#include <linux/socket.h>
+#include <linux/sysctl.h>
#include <linux/un.h>
#include <linux/fcntl.h>
#include <linux/termios.h>
@@ -93,6 +94,69 @@ EXPORT_SYMBOL_GPL(nl_table);
static DECLARE_WAIT_QUEUE_HEAD(nl_table_wait);
+/*
+ * Auxiliary NETLINK_ROUTE candidate indexes. Writers hold the table grabbed;
+ * broadcasts pin the same table with netlink_lock_table(). Socket/netns
+ * lifetime therefore follows the original mc_list, without another RCU domain.
+ */
+struct netlink_route_net {
+ struct hlist_head mc_list;
+};
+
+static unsigned int netlink_route_net_id;
+static HLIST_HEAD(netlink_route_all_nsid);
+
+/* Indexes stay populated in both modes; each broadcast samples this once. */
+static int netlink_route_mc_index __read_mostly;
+
+#ifdef CONFIG_SYSCTL
+static const struct ctl_table netlink_route_sysctls[] = {
+ {
+ .procname = "netlink_route_mc_index",
+ .data = &netlink_route_mc_index,
+ .maxlen = sizeof(netlink_route_mc_index),
+ .mode = 0644,
+ .proc_handler = proc_dointvec_minmax,
+ .extra1 = SYSCTL_ZERO,
+ .extra2 = SYSCTL_ONE,
+ },
+};
+
+static int __init netlink_route_sysctl_init(void)
+{
+ if (!register_net_sysctl(&init_net, "net/core", netlink_route_sysctls))
+ return -ENOMEM;
+ return 0;
+}
+fs_initcall(netlink_route_sysctl_init);
+#endif
+
+static void netlink_update_route_mc(struct sock *sk, unsigned int subscriptions)
+{
+ struct netlink_sock *nlk = nlk_sk(sk);
+ struct netlink_route_net *nn;
+
+ if (sk->sk_protocol != NETLINK_ROUTE)
+ return;
+
+ lockdep_assert_held(&nl_table_lock);
+ nn = net_generic(sock_net(sk), netlink_route_net_id);
+ if (subscriptions) {
+ if (hlist_unhashed(&nlk->mc_net_node))
+ hlist_add_head(&nlk->mc_net_node, &nn->mc_list);
+ } else if (!hlist_unhashed(&nlk->mc_net_node)) {
+ hlist_del_init(&nlk->mc_net_node);
+ }
+
+ if (subscriptions && nlk_test_bit(LISTEN_ALL_NSID, sk)) {
+ if (hlist_unhashed(&nlk->mc_all_nsid_node))
+ hlist_add_head(&nlk->mc_all_nsid_node,
+ &netlink_route_all_nsid);
+ } else if (!hlist_unhashed(&nlk->mc_all_nsid_node)) {
+ hlist_del_init(&nlk->mc_all_nsid_node);
+ }
+}
+
static struct lock_class_key nlk_cb_mutex_keys[MAX_LINKS];
static const char *const nlk_cb_mutex_key_strings[MAX_LINKS + 1] = {
@@ -598,6 +662,7 @@ static void netlink_remove(struct sock *sk)
__sock_put(sk);
netlink_table_grab();
+ netlink_update_route_mc(sk, 0);
if (nlk_sk(sk)->subscriptions) {
__sk_del_bind_node(sk);
netlink_update_listeners(sk);
@@ -628,6 +693,8 @@ static int __netlink_create(struct net *net, struct socket *sock,
sock_init_data(sock, sk);
nlk = nlk_sk(sk);
+ INIT_HLIST_NODE(&nlk->mc_net_node);
+ INIT_HLIST_NODE(&nlk->mc_all_nsid_node);
mutex_init(&nlk->nl_cb_mutex);
lockdep_set_class_and_name(&nlk->nl_cb_mutex,
nlk_cb_mutex_keys + protocol,
@@ -915,6 +982,7 @@ netlink_update_subscriptions(struct sock *sk, unsigned int subscriptions)
else if (!nlk->subscriptions && subscriptions)
sk_add_bind_node(sk, &nl_table[sk->sk_protocol].mc_list);
nlk->subscriptions = subscriptions;
+ netlink_update_route_mc(sk, subscriptions);
}
static int netlink_realloc_groups(struct sock *sk)
@@ -1555,8 +1623,25 @@ int netlink_broadcast_filtered(struct sock *ssk, struct sk_buff *skb,
netlink_lock_table();
- sk_for_each_bound(sk, &nl_table[ssk->sk_protocol].mc_list)
- do_one_broadcast(sk, &info);
+ if (ssk->sk_protocol == NETLINK_ROUTE &&
+ READ_ONCE(netlink_route_mc_index)) {
+ struct netlink_route_net *nn;
+ struct netlink_sock *nlk;
+
+ nn = net_generic(net, netlink_route_net_id);
+ hlist_for_each_entry(nlk, &nn->mc_list, mc_net_node)
+ do_one_broadcast(&nlk->sk, &info);
+
+ /* Local all-nsid listeners were visited above. */
+ hlist_for_each_entry(nlk, &netlink_route_all_nsid,
+ mc_all_nsid_node) {
+ if (!net_eq(sock_net(&nlk->sk), net))
+ do_one_broadcast(&nlk->sk, &info);
+ }
+ } else {
+ sk_for_each_bound(sk, &nl_table[ssk->sk_protocol].mc_list)
+ do_one_broadcast(sk, &info);
+ }
consume_skb(skb);
@@ -1722,6 +1807,13 @@ static int netlink_setsockopt(struct socket *sock, int level, int optname,
case NETLINK_LISTEN_ALL_NSID:
if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_BROADCAST))
return -EPERM;
+ if (sk->sk_protocol == NETLINK_ROUTE) {
+ netlink_table_grab();
+ assign_bit(NETLINK_F_LISTEN_ALL_NSID, &nlk->flags, val);
+ netlink_update_route_mc(sk, nlk->subscriptions);
+ netlink_table_ungrab();
+ return 0;
+ }
nr = NETLINK_F_LISTEN_ALL_NSID;
break;
case NETLINK_CAP_ACK:
@@ -2867,6 +2959,9 @@ static const struct net_proto_family netlink_family_ops = {
static int __net_init netlink_net_init(struct net *net)
{
+ struct netlink_route_net *nn = net_generic(net, netlink_route_net_id);
+
+ INIT_HLIST_HEAD(&nn->mc_list);
#ifdef CONFIG_PROC_FS
if (!proc_create_net("netlink", 0, net->proc_net, &netlink_seq_ops,
sizeof(struct nl_seq_iter)))
@@ -2877,6 +2972,11 @@ static int __net_init netlink_net_init(struct net *net)
static void __net_exit netlink_net_exit(struct net *net)
{
+ struct netlink_route_net *nn = net_generic(net, netlink_route_net_id);
+
+ netlink_table_grab();
+ WARN_ON_ONCE(!hlist_empty(&nn->mc_list));
+ netlink_table_ungrab();
#ifdef CONFIG_PROC_FS
remove_proc_entry("netlink", net->proc_net);
#endif
@@ -2905,6 +3005,8 @@ static void __init netlink_add_usersock_entry(void)
static struct pernet_operations __net_initdata netlink_net_ops = {
.init = netlink_net_init,
.exit = netlink_net_exit,
+ .id = &netlink_route_net_id,
+ .size = sizeof(struct netlink_route_net),
};
static inline u32 netlink_hash(const void *data, u32 len, u32 seed)
@@ -2980,7 +3082,8 @@ static int __init netlink_proto_init(void)
netlink_add_usersock_entry();
sock_register(&netlink_family_ops);
- register_pernet_subsys(&netlink_net_ops);
+ if (register_pernet_subsys(&netlink_net_ops))
+ goto panic;
register_pernet_subsys(&netlink_tap_net_ops);
/* The netlink device handler may be needed early. */
rtnetlink_init();
diff --git a/net/netlink/af_netlink.h b/net/netlink/af_netlink.h
index 778a38093..bc7b38d4f 100644
--- a/net/netlink/af_netlink.h
+++ b/net/netlink/af_netlink.h
@@ -47,6 +47,10 @@ struct netlink_sock {
unsigned long *groups);
struct module *module;
+ /* NETLINK_ROUTE auxiliary multicast indexes; nl_table_lock protected. */
+ struct hlist_node mc_net_node;
+ struct hlist_node mc_all_nsid_node;
+
struct rhash_head node;
struct rcu_head rcu;
};
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH net-next 2/2] selftests: net: cover ROUTE multicast namespace and switch semantics
[not found] <20261104033526.2917590-1-work_patrick@qq.com>
2026-11-04 3:35 ` [PATCH net-next 1/2] netlink: add an optional per-netns ROUTE multicast index patrick
@ 2026-11-04 3:35 ` patrick
1 sibling, 0 replies; 3+ messages in thread
From: patrick @ 2026-11-04 3:35 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, horms, corbet, rdunlap, linux-doc,
skhan, linux-kselftest, linux-api, linux-kernel
Exercise local and cross-namespace delivery, permissions, group filters,
subscription and option transitions, socket filters, overflow behaviour,
compatibility mode and unaffected Netlink protocols. Include group
memberships above 32. Validate live changes of the optional ROUTE index
while existing sockets remain active and while subscriptions change.
Use unique namespace names and clean up sockets and namespaces on exit.
Restore the sysctl after the switch test. Report missing privileges,
iproute2, Python setns support or the optional sysctl as kselftest SKIP.
The general semantic test also runs on kernels without the new switch.
No function tracing, FRR or network access is required.
AI assistance: An LLM assisted with implementation adaptation.
Signed-off-by: patrick <work_patrick@qq.com>
---
tools/testing/selftests/net/Makefile | 2 +
tools/testing/selftests/net/config | 1 +
.../selftests/net/netlink_mcast_semantics.py | 420 ++++++++++++++++++
.../selftests/net/netlink_mcast_switch.py | 217 +++++++++
4 files changed, 640 insertions(+)
create mode 100755 tools/testing/selftests/net/netlink_mcast_semantics.py
create mode 100755 tools/testing/selftests/net/netlink_mcast_switch.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 9180b9bec..4f7bb7edb 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -71,6 +71,8 @@ TEST_PROGS := \
netdev-l2addr.sh \
netdev_lock.py \
netdevice.sh \
+ netlink_mcast_semantics.py \
+ netlink_mcast_switch.py \
netns-name.sh \
netns-sysctl.sh \
nk_qlease.py \
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index d4416f8bd..3dbb0d3f8 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -97,6 +97,7 @@ CONFIG_NET_IPGRE_DEMUX=m
CONFIG_NET_IPIP=y
CONFIG_NET_IPVTI=y
CONFIG_NETKIT=y
+CONFIG_NETLINK_DIAG=y
CONFIG_NET_L3_MASTER_DEV=y
CONFIG_NET_NS=y
CONFIG_NET_PKTGEN=m
diff --git a/tools/testing/selftests/net/netlink_mcast_semantics.py b/tools/testing/selftests/net/netlink_mcast_semantics.py
new file mode 100755
index 000000000..2c5884d1e
--- /dev/null
+++ b/tools/testing/selftests/net/netlink_mcast_semantics.py
@@ -0,0 +1,420 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""NETLINK_ROUTE multicast semantic checks, independent of the lab."""
+import argparse
+import array
+import contextlib
+import ctypes
+import errno
+import json
+import os
+from pathlib import Path
+import select
+import socket
+import struct
+import subprocess as sp
+import time
+import tempfile
+import shutil
+import sys
+
+SOL_NETLINK = 270
+ROUTE6, ROUTE4, LINK, ADDR4, ADDR6, NEXTHOP = 11, 7, 1, 5, 9, 32
+ap = argparse.ArgumentParser(description=__doc__)
+ap.add_argument('--output', type=Path)
+args = ap.parse_args()
+if os.geteuid() != 0 or not shutil.which('ip') or not hasattr(os, 'setns'):
+ print('1..0 # SKIP requires root, iproute2 and Python os.setns')
+ sys.exit(4)
+out = args.output or Path(tempfile.mkdtemp(prefix='netlink-mcast-'))
+if args.output:
+ out.mkdir(exist_ok=False, parents=True)
+cases, sockets, namespaces = [], [], []
+prefix = f'nlmc-{os.getpid()}-'
+if not Path('/proc/sys/net/ipv4/nexthop_compat_mode').exists():
+ print('1..0 # SKIP requires IPv4/IPv6 nexthop support')
+ sys.exit(4)
+print('TAP version 13', flush=True)
+original = os.open('/proc/self/ns/net', os.O_RDONLY)
+# Probe namespace creation before beginning behavioural assertions.
+probe_lib = ctypes.CDLL(None, use_errno=True)
+if probe_lib.unshare(0x40000000):
+ probe_errno = ctypes.get_errno()
+ os.close(original)
+ if probe_errno in (errno.EPERM, errno.EINVAL, errno.ENOSYS):
+ print('1..0 # SKIP cannot create a network namespace')
+ sys.exit(4)
+ raise OSError(probe_errno, os.strerror(probe_errno))
+try:
+ for probe_protocol in (0, 4, 16):
+ socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, probe_protocol).close()
+except OSError as probe_error:
+ if probe_error.errno in (errno.EPROTONOSUPPORT, errno.EAFNOSUPPORT):
+ print('1..0 # SKIP missing required Netlink protocol')
+ os.setns(original, 0)
+ os.close(original)
+ sys.exit(4)
+ raise
+os.setns(original, 0)
+
+
+def cmd(*a):
+ return sp.check_output(a, text=True, stderr=sp.STDOUT, timeout=30).strip()
+
+def ns(name):
+ name = prefix+name
+ cmd('ip', 'netns', 'add', name)
+ namespaces.append(name)
+ cmd('ip', '-n', name, 'link', 'set', 'lo', 'up')
+ return name
+
+@contextlib.contextmanager
+def enter(name):
+ fd = os.open('/run/netns/'+name, os.O_RDONLY)
+ os.setns(fd, 0)
+ try:
+ yield
+ finally:
+ os.setns(original, 0)
+ os.close(fd)
+
+def sock(name, groups=(), allns=False, rcvbuf=4*1024*1024, protocol=0):
+ with enter(name):
+ s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, protocol)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, rcvbuf)
+ s.setsockopt(SOL_NETLINK, 3, 1) # NETLINK_PKTINFO
+ if allns:
+ s.setsockopt(SOL_NETLINK, 8, 1)
+ s.bind((0, 0))
+ for group in groups:
+ s.setsockopt(SOL_NETLINK, 1, group)
+ s.setblocking(False)
+ sockets.append(s)
+ return s
+
+def messages(s, wait=0.025, allow_enobufs=False):
+ result, errors = [], []
+ while select.select([s], [], [], wait)[0]:
+ try:
+ data, ancillary, flags, address = s.recvmsg(262144, 512)
+ except OSError as e:
+ if e.errno == errno.ENOBUFS and allow_enobufs:
+ errors.append(e.errno)
+ continue
+ raise
+ assert not flags & (socket.MSG_TRUNC | socket.MSG_CTRUNC)
+ tags = {kind: struct.unpack('i', value[:4])[0]
+ for level, kind, value in ancillary if level == SOL_NETLINK}
+ offset = 0
+ while offset < len(data):
+ length, kind, nlflags, seq, pid = struct.unpack_from('IHHII', data, offset)
+ assert 16 <= length <= len(data)-offset
+ result.append(dict(type=kind, flags=nlflags, seq=seq,
+ family=data[offset+16] if length > 16 else None,
+ tags=tags, data=data[offset+16:offset+length]))
+ offset += (length+3) & ~3
+ assert offset == len(data)
+ wait = 0
+ return result, errors
+
+def clean():
+ # One settle interval per preparation boundary; idle sockets do not each
+ # add another interval. Expected notification reads still wait explicitly.
+ time.sleep(0.025)
+ for s in sockets:
+ if s.fileno() >= 0:
+ messages(s,wait=0)
+
+def record(name, **details):
+ cases.append(dict(name=name, passed=True, **details))
+ (out/'cases.json').write_text(json.dumps(cases, indent=2))
+ print(f'ok {len(cases)} - {name}', flush=True)
+ print('# '+json.dumps(cases[-1]), flush=True)
+
+def route(name, family=6):
+ dest = 'fdab:123::1/128' if family == 6 else '198.18.0.1/32'
+ cmd('ip', '-n', name, '-'+str(family), 'route', 'replace', dest, 'dev', 'lo')
+
+def expect(s, count, kind=24, family=10, cross=None, group=ROUTE6):
+ found, errors = messages(s)
+ chosen = [m for m in found if m['type'] == kind and m['family'] == family]
+ assert len(chosen) == count, (count, [(m['type'], m['family'],m['tags']) for m in found])
+ for m in chosen:
+ assert m['tags'].get(3) == group, m
+ assert m['tags'].get(8) == cross, m
+ return len(chosen)
+
+def nlmsg(kind, flags, seq, body):
+ return struct.pack('IHHII', 16+len(body), kind, flags, seq, 0)+body
+
+def attr(kind, body):
+ value = struct.pack('HH', len(body)+4, kind)+body
+ return value+b'\0'*((-len(value)) % 4)
+
+def slow_queue(tx, no_enobufs, broadcast_error=False):
+ slow = sock(tx, [ROUTE6], rcvbuf=4096)
+ slow.setsockopt(SOL_NETLINK, 5, int(no_enobufs))
+ slow.setsockopt(SOL_NETLINK, 4, int(broadcast_error))
+ sender = sock(tx)
+ request = b''
+ for i in range(200):
+ body = struct.pack('BBBBBBBBI', 10, 128, 0, 0, 254, 186, 0, 1, 0)
+ body += attr(1, socket.inet_pton(socket.AF_INET6, 'fdab:456::'+format(i+1,'x')))
+ body += attr(4, struct.pack('I', 1))
+ request += nlmsg(24, 1|4|0x100|0x400, i+1, body)
+ sender.sendto(request, (0, 0))
+ acks, errors = messages(sender, 1)
+ assert len(acks) == 200 and all(m['type']==2 and struct.unpack_from('i',m['data'])[0]==0 for m in acks)
+ delivered, overflows = messages(slow, allow_enobufs=True)
+ assert 0 < len(delivered) < 200
+ assert bool(overflows) == (not no_enobufs), (len(delivered),overflows)
+ slow.close(); sender.close()
+ return dict(acks=len(acks), delivered=len(delivered), enobufs=len(overflows))
+
+try:
+ tx, rx, nomap = ns('tx'), ns('rx'), ns('nomap')
+ cmd('ip', 'netns', 'exec', rx, 'ip', 'netns', 'set', tx, '71')
+ local = sock(tx, [ROUTE6])
+ local_all = sock(tx, [ROUTE6], allns=True)
+ wrong = sock(tx, [LINK])
+ multi = sock(tx, [ROUTE6, LINK])
+ foreign = sock(rx, [ROUTE6])
+ cross = sock(rx, [ROUTE6], allns=True)
+ unmapped = sock(nomap, [ROUTE6], allns=True)
+ clean(); route(tx)
+ assert [expect(s,n,cross=x) for s,n,x in
+ [(local,1,None),(local_all,1,None),(wrong,0,None),(multi,1,None),
+ (foreign,0,None),(cross,1,71),(unmapped,0,None)]] == [1,1,0,1,0,1,0]
+ record('local-group-multiple-sockets-multiple-groups-and-cross-nsid',
+ local=3, cross=1, cross_nsid=71, foreign_ordinary=0, missing_map=0,
+ local_all_nsid_duplicates=0)
+
+ high = sock(tx, [ROUTE6, 33, 34])
+ mask = high.getsockopt(SOL_NETLINK, 9, 128)
+ assert struct.unpack_from('=I', mask, 4)[0] & 2
+ clean(); route(tx); expect(high, 1)
+ high.close()
+ record('membership-above-32-and-matching-route-group')
+
+ # Toggle an already subscribed foreign socket, then last-membership removal.
+ for enabled in [0,1,0,1]:
+ cross.setsockopt(SOL_NETLINK, 8, enabled)
+ clean(); route(tx)
+ expect(cross,enabled,cross=71)
+ record('listen-all-nsid-toggle-after-subscription', transitions=[0,1,0,1])
+ cross.setsockopt(SOL_NETLINK, 2, ROUTE6)
+ clean(); route(tx); expect(cross,0)
+ cross.setsockopt(SOL_NETLINK, 1, ROUTE6)
+ clean(); route(tx); expect(cross,1,cross=71)
+ record('cross-last-drop-and-rejoin')
+
+ local.setsockopt(SOL_NETLINK, 2, ROUTE6)
+ clean(); route(tx); expect(local,0)
+ local.setsockopt(SOL_NETLINK, 1, LINK)
+ local.setsockopt(SOL_NETLINK, 1, ROUTE6)
+ local.setsockopt(SOL_NETLINK, 2, LINK)
+ clean(); route(tx); expect(local,1)
+ local.bind((local.getsockname()[0],0))
+ clean(); route(tx); expect(local,0)
+ local.bind((local.getsockname()[0],1<<(ROUTE6-1)))
+ clean(); route(tx); expect(local,1)
+ record('first-last-membership-rebind-and-multi-group-updates')
+
+ # Receive-side classic BPF filter remains applied by do_one_broadcast().
+ class Filter(ctypes.Structure):
+ _fields_=[('code',ctypes.c_ushort),('jt',ctypes.c_ubyte),('jf',ctypes.c_ubyte),('k',ctypes.c_uint)]
+ class Program(ctypes.Structure):
+ _fields_=[('len',ctypes.c_ushort),('filter',ctypes.POINTER(Filter))]
+ insn=Filter(6,0,0,0); prog=Program(1,ctypes.pointer(insn))
+ libc=ctypes.CDLL(None,use_errno=True)
+ assert libc.setsockopt(local.fileno(),socket.SOL_SOCKET,26,ctypes.byref(prog),ctypes.sizeof(prog))==0
+ clean(); route(tx); expect(local,0)
+ local.setsockopt(socket.SOL_SOCKET,27,0)
+ clean(); route(tx); expect(local,1)
+ record('socket-bpf-receive-filter')
+
+ # file_ns_capable() checks the credentials captured when the socket opened.
+ parent, child = socket.socketpair()
+ pid=os.fork()
+ if pid==0:
+ try:
+ parent.close()
+ class CapHeader(ctypes.Structure):
+ _fields_=[('version',ctypes.c_uint),('pid',ctypes.c_int)]
+ class CapData(ctypes.Structure):
+ _fields_=[('effective',ctypes.c_uint),('permitted',ctypes.c_uint),('inheritable',ctypes.c_uint)]
+ header=CapHeader(0x20080522,0); caps=(CapData*2)()
+ assert libc.capget(ctypes.byref(header),caps)==0
+ for f in ['effective','permitted','inheritable']:
+ setattr(caps[0],f,getattr(caps[0],f)&~(1<<11))
+ assert libc.capset(ctypes.byref(header),caps)==0
+ with enter(rx):
+ denied=socket.socket(socket.AF_NETLINK,socket.SOCK_RAW,0)
+ denied.bind((0,1<<(ROUTE6-1)))
+ try:
+ denied.setsockopt(SOL_NETLINK,8,1)
+ except PermissionError:
+ pass
+ else:
+ raise AssertionError('unprivileged setsockopt unexpectedly succeeded')
+ child.sendmsg([b'fd'],[(socket.SOL_SOCKET,socket.SCM_RIGHTS,array.array('i',[denied.fileno()]))])
+ os._exit(0)
+ except BaseException:
+ import traceback
+ traceback.print_exc(); os._exit(1)
+ child.close()
+ data, anc, flags, addr=parent.recvmsg(16,socket.CMSG_SPACE(4))
+ assert os.waitpid(pid,0)[1]==0
+ fd=array.array('i'); fd.frombytes(anc[0][2][:4])
+ denied=socket.socket(fileno=fd[0]); denied.setblocking(False); sockets.append(denied)
+ denied.setsockopt(SOL_NETLINK,8,1)
+ clean(); route(tx); expect(denied,0); expect(cross,1,cross=71)
+ denied.close(); parent.close()
+ record('cross-namespace-capability-denied-and-set-option-denied')
+
+ ipv4=sock(tx,[ROUTE4]); clean(); route(tx,4)
+ expect(ipv4,1,family=2,group=ROUTE4); expect(local,0)
+ cmd('ip','-n',tx,'-4','route','del','198.18.0.1/32')
+ expect(ipv4,1,kind=25,family=2,group=ROUTE4)
+ clean(); cmd('ip','-n',tx,'-6','route','del','fdab:123::1/128')
+ expect(local,1,kind=25); expect(cross,1,kind=25,cross=71)
+ record('ipv4-ipv6-route-add-delete-and-family-group-filter')
+ link=sock(tx,[LINK]); a4=sock(tx,[ADDR4]); a6=sock(tx,[ADDR6]); nh=sock(tx,[NEXTHOP])
+ clean(); cmd('ip','-n',tx,'link','add','nl-dummy','type','dummy')
+ expect(link,1,kind=16,family=0,group=LINK)
+ cmd('ip','-n',tx,'link','set','nl-dummy','up'); clean()
+ cmd('ip','-n',tx,'addr','add','198.19.0.1/24','dev','nl-dummy')
+ expect(a4,1,kind=20,family=2,group=ADDR4)
+ cmd('ip','-n',tx,'-6','addr','add','fdab:999::1/64','dev','nl-dummy','nodad')
+ expect(a6,1,kind=20,family=10,group=ADDR6)
+ clean(); cmd('ip','-n',tx,'-6','nexthop','add','id','101','dev','lo')
+ expect(nh,1,kind=104,family=10,group=NEXTHOP)
+ cmd('ip','-n',tx,'nexthop','del','id','101')
+ expect(nh,1,kind=105,family=10,group=NEXTHOP)
+ clean(); cmd('ip','-n',tx,'addr','del','198.19.0.1/24','dev','nl-dummy')
+ expect(a4,1,kind=21,family=2,group=ADDR4)
+ clean(); cmd('ip','-n',tx,'-6','addr','del','fdab:999::1/64','dev','nl-dummy')
+ expect(a6,1,kind=21,family=10,group=ADDR6)
+ clean(); cmd('ip','-n',tx,'link','del','nl-dummy')
+ expect(link,1,kind=17,family=0,group=LINK)
+ record('link-address-ipv4-ipv6-and-nexthop-add-delete')
+
+ route(tx)
+ sender=sock(tx)
+ sender.sendto(nlmsg(26,1|0x300,100,bytes([10])+b'\0'*11),(0,0))
+ dump,errs=messages(sender,1)
+ assert dump and dump[-1]['type']==3 and all(m['seq']==100 for m in dump)
+ assert any(m['type']==24 for m in dump)
+ bad=struct.pack('BBBBBBBBI',10,129,0,0,254,186,0,1,0)
+ sender.sendto(nlmsg(24,1|4|0x400|0x100,101,bad),(0,0))
+ ack,errs=messages(sender,1)
+ assert len(ack)==1 and ack[0]['type']==2 and ack[0]['seq']==101
+ code=struct.unpack_from('i',ack[0]['data'])[0]
+ assert code==-errno.EINVAL,code
+ record('unicast-route-dump-and-invalid-request-error-ack',error=code)
+
+ # Change candidate population without changing the true recipients.
+ # Extra sockets use LINK and carry no IPv6-route data.
+ pool=[]
+ transitions=[]
+ for target in (0,48,64,80,128,64,48,0):
+ while len(pool)<target:
+ pool.append(sock(nomap,[LINK]))
+ while len(pool)>target:
+ pool.pop().close()
+ clean(); route(tx)
+ expect(local,1); expect(local_all,1); expect(cross,1,cross=71)
+ if target:
+ # Toggle cross-netns membership while true route recipients stay fixed.
+ for enabled in (1,0):
+ for extra in pool: extra.setsockopt(SOL_NETLINK,8,enabled)
+ clean(); route(tx)
+ expect(local,1); expect(local_all,1); expect(cross,1,cross=71)
+ transitions.append(target)
+ record('candidate-population-transitions-no-missing-or-duplicate',sizes=transitions)
+ # A mapped OVS-style listener must still receive LINK/ADDR on indexed path.
+ ovs=sock(rx,[1,5,9,12],allns=True)
+ field_pool=[sock(nomap,[LINK]) for _ in range(128)]
+ clean(); cmd('ip','-n',tx,'link','add','nlg-ovs','type','dummy')
+ expect(ovs,1,kind=16,family=0,group=LINK,cross=71)
+ cmd('ip','-n',tx,'link','set','nlg-ovs','up'); clean()
+ cmd('ip','-n',tx,'addr','add','198.20.1.1/32','dev','nlg-ovs')
+ expect(ovs,1,kind=20,family=2,group=ADDR4,cross=71)
+ clean(); cmd('ip','-n',tx,'-6','addr','add','fdaa:908::1/128','dev','nlg-ovs','nodad')
+ expect(ovs,1,kind=20,family=10,group=ADDR6,cross=71)
+ clean(); cmd('ip','-n',tx,'link','del','nlg-ovs'); clean()
+ for extra in field_pool: extra.close()
+ ovs.close()
+ record('mapped-ovs-style-link-address-notifications-with-128-foreign')
+
+ probe=sock(tx,allns=True)
+ # Unsubscribed option changes, duplicate joins, partial/last drops and rebind.
+ for enabled in (0,1,0,1): probe.setsockopt(SOL_NETLINK,8,enabled)
+ for group in (ROUTE6,ROUTE6,LINK): probe.setsockopt(SOL_NETLINK,1,group)
+ clean(); route(tx); expect(probe,1)
+ probe.setsockopt(SOL_NETLINK,2,LINK)
+ clean(); route(tx); expect(probe,1)
+ probe.setsockopt(SOL_NETLINK,2,ROUTE6)
+ clean(); route(tx); expect(probe,0)
+ for _ in range(2): probe.bind((probe.getsockname()[0],1<<(ROUTE6-1)))
+ clean(); route(tx); expect(probe,1)
+ probe.close()
+ record('duplicate-and-partial-membership-transitions')
+ for compat in (0,1):
+ cmd('ip','netns','exec',tx,'sysctl','-qw',f'net.ipv4.nexthop_compat_mode={compat}')
+ cmd('ip','-n',tx,'-6','nexthop','add','id','200','dev','lo')
+ cmd('ip','-n',tx,'-6','route','add','fdab:888::1/128','nhid','200')
+ clean()
+ cmd('ip','-n',tx,'-6','nexthop','replace','id','200','blackhole')
+ expect(local,compat); expect(cross,compat,cross=71)
+ clean(); cmd('ip','-n',tx,'nexthop','del','id','200')
+ expect(local,compat,kind=25); expect(cross,compat,kind=25,cross=71)
+ record('nexthop-reference-compat-'+str(compat),route_change_notifications=compat,route_delete_notifications=compat)
+
+ # Close regular listeners before deliberately flooding a small queue.
+ for s in sockets:
+ s.close()
+ record('slow-reader-default-enobufs',**slow_queue(tx,False))
+ record('slow-reader-no-enobufs-option',**slow_queue(tx,True))
+ record('slow-reader-broadcast-error-option-keeps-success-acks',**slow_queue(tx,False,True))
+
+ sink=sock(tx,protocol=2)
+ user_local=sock(tx,[1],protocol=2)
+ user_foreign=sock(rx,[1],protocol=2)
+ user_cross=sock(rx,[1],allns=True,protocol=2)
+ user_sender=sock(tx,protocol=2)
+ payload=nlmsg(42,0,900,b'\0usersock-regression')
+ assert user_sender.sendto(payload,(sink.getsockname()[0],1))==len(payload)
+ expect(user_local,1,kind=42,family=0,group=1)
+ expect(user_foreign,0,kind=42,family=0,group=1)
+ expect(user_cross,1,kind=42,family=0,group=1,cross=71)
+ unicast, errs = messages(sink)
+ assert len(unicast) == 1 and unicast[0]['type'] == 42
+ assert unicast[0]['data'] == payload[16:]
+ record('usersock-multicast-local-cross-and-unicast-unchanged')
+
+ # Generic-netlink control query and SOCK_DIAG dump use untouched protocols.
+ with enter(tx):
+ generic=socket.socket(socket.AF_NETLINK,socket.SOCK_RAW,16)
+ generic.bind((0,0)); generic.setblocking(False); sockets.append(generic)
+ generic.sendto(nlmsg(16,1|4,701,struct.pack('BBH',3,1,0)+attr(2,b'nlctrl\0')),(0,0))
+ g,errs=messages(generic,1)
+ assert any(m['type']==16 and m['seq']==701 for m in g)
+ diag=socket.socket(socket.AF_NETLINK,socket.SOCK_RAW,4)
+ diag.bind((0,0)); diag.setblocking(False); sockets.append(diag)
+ # netlink_diag_req is 20 bytes: family/protocol/pad, inode/show/cookie[2].
+ req=struct.pack('BBHIIII',socket.AF_NETLINK,0,0,0,1|8,0xffffffff,0xffffffff)
+ diag.sendto(nlmsg(20,1|0x300,702,req),(0,0))
+ d,errs=messages(diag,1)
+ assert d and d[-1]['type']==3 and any(m['type']==20 for m in d)
+ record('generic-netlink-and-sock-diag',generic_reply=True,diag_records=sum(m['type']==20 for m in d))
+ record('proc-netlink-visible',lines=len(Path('/proc/net/netlink').read_text().splitlines()))
+ print(f'1..{len(cases)}', flush=True)
+ (out/'complete.json').write_text(json.dumps(dict(success=True,kernel=os.uname().release,
+ boot_id=Path('/proc/sys/kernel/random/boot_id').read_text().strip(),cases=len(cases)),indent=2))
+finally:
+ for s in sockets:
+ s.close()
+ os.setns(original,0); os.close(original)
+ for name in reversed(namespaces):
+ sp.run(['ip','netns','del',name],check=False,stdout=sp.DEVNULL,stderr=sp.DEVNULL)
diff --git a/tools/testing/selftests/net/netlink_mcast_switch.py b/tools/testing/selftests/net/netlink_mcast_switch.py
new file mode 100755
index 000000000..5bf9f5f1d
--- /dev/null
+++ b/tools/testing/selftests/net/netlink_mcast_switch.py
@@ -0,0 +1,217 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""NETLINK_ROUTE live-switch semantics; no tracing dependency."""
+import contextlib
+import ctypes
+import errno
+import json
+import os
+from pathlib import Path
+import select
+import socket
+import struct
+import subprocess
+import threading
+import time
+import argparse
+import tempfile
+import shutil
+import sys
+
+p = argparse.ArgumentParser()
+p.add_argument('--output', type=Path)
+a = p.parse_args()
+if os.geteuid() != 0 or not shutil.which('ip') or not hasattr(os, 'setns'):
+ print('1..0 # SKIP requires root, iproute2 and Python os.setns')
+ sys.exit(4)
+control = Path('/proc/sys/net/core/netlink_route_mc_index')
+if not control.exists():
+ print('1..0 # SKIP kernel has no netlink_route_mc_index sysctl')
+ sys.exit(4)
+root = a.output or Path(tempfile.mkdtemp(prefix='netlink-switch-'))
+if a.output:
+ root.mkdir(exist_ok=False, parents=True)
+saved_mode = control.read_text()
+print('TAP version 13', flush=True)
+original = os.open("/proc/self/ns/net", os.O_RDONLY)
+# Probe namespace creation before beginning behavioural assertions.
+probe_lib = ctypes.CDLL(None, use_errno=True)
+if probe_lib.unshare(0x40000000):
+ probe_errno = ctypes.get_errno()
+ os.close(original)
+ if probe_errno in (errno.EPERM, errno.EINVAL, errno.ENOSYS):
+ print('1..0 # SKIP cannot create a network namespace')
+ sys.exit(4)
+ raise OSError(probe_errno, os.strerror(probe_errno))
+os.setns(original, 0)
+
+namespaces, sockets, checks = [], [], []
+sequence = 0
+
+
+def command(*args):
+ return subprocess.check_output(args, text=True, stderr=subprocess.STDOUT,
+ timeout=20).strip()
+
+
+def record(name, **details):
+ checks.append(dict(name=name, passed=True, **details))
+ print(f'ok {len(checks)} - {name}', flush=True)
+ print('# '+json.dumps(checks[-1]), flush=True)
+
+
+def namespace(suffix):
+ name = f"nlsw-{os.getpid()}-" + suffix
+ command("ip", "netns", "add", name)
+ namespaces.append(name)
+ command("ip", "-n", name, "link", "set", "lo", "up")
+ return name
+
+
+@contextlib.contextmanager
+def enter(name):
+ fd = os.open("/run/netns/" + name, os.O_RDONLY)
+ os.setns(fd, 0)
+ try:
+ yield
+ finally:
+ os.setns(original, 0)
+ os.close(fd)
+
+
+def subscriber(name, allns=False):
+ with enter(name):
+ sk = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, 0)
+ sk.setsockopt(270, 8, int(allns))
+ sk.bind((0, 1 << 10)) # RTNLGRP_IPV6_ROUTE = 11
+ sk.setblocking(False)
+ sockets.append(sk)
+ return sk
+
+
+def mode(value):
+ control.write_text(str(value) + "\n")
+ assert int(control.read_text()) == value
+
+
+def drain(sk):
+ found = []
+ while select.select([sk], [], [], 0)[0]:
+ data = sk.recv(262144)
+ offset = 0
+ while offset < len(data):
+ length, kind, flags, seq, pid = struct.unpack_from("IHHII", data, offset)
+ assert 16 <= length <= len(data) - offset
+ found.append((kind, data[offset + 16:offset + length]))
+ offset += (length + 3) & ~3
+ assert offset == len(data)
+ return found
+
+
+def notify(expected, selected=None):
+ global sequence
+ sequence += 1
+ for sk in sockets:
+ if sk.fileno() >= 0:
+ drain(sk)
+ command("ip", "-n", tx, "-6", "route", "add",
+ f"fdab:abcd::{sequence:x}/128", "dev", "lo")
+ for sk, count in expected.items():
+ messages = [m for m in drain(sk) if m[0] == 24 and m[1][0] == 10]
+ assert len(messages) == count, (sequence, sk.fileno(), count, len(messages))
+ return None
+
+
+try:
+ mode(0)
+ for bad in ("-1\n", "2\n", "garbage\n"):
+ try:
+ control.write_text(bad)
+ except OSError as error:
+ assert error.errno == errno.EINVAL, error
+ else:
+ raise AssertionError("invalid value accepted: " + bad)
+ assert control.read_text().strip() == "0"
+ record("disabled-setting-and-invalid-values-rejected")
+
+ tx, monitor, unmapped = [namespace(n) for n in ("tx", "monitor", "unmapped")]
+ with enter(tx):
+ assert not control.exists(), "global control exposed in child netns"
+ record("control-only-visible-in-initial-netns")
+ command("ip", "-n", monitor, "netns", "set", tx, "71")
+ local = subscriber(tx)
+ local_all = subscriber(tx, True)
+ ordinary = subscriber(monitor)
+ cross = subscriber(monitor, True)
+ no_map = subscriber(unmapped, True)
+ expected = {local: 1, local_all: 1, ordinary: 0, cross: 1, no_map: 0}
+
+ paths = []
+ for value in (0, 1, 0, 1):
+ mode(value)
+ paths.append(dict(mode=value, receive_check=notify(expected)))
+ record("live-switch-with-existing-local-and-cross-sockets", paths=paths)
+
+ mode(0)
+ late_local = subscriber(tx)
+ late_cross = subscriber(monitor, True)
+ local.setsockopt(270, 2, 11)
+ cross.setsockopt(270, 8, 0)
+ subscriber(tx).close()
+ subscriber(monitor, True).close()
+ mode(1)
+ changed = {**expected, local: 0, cross: 0, late_local: 1, late_cross: 1}
+ notify(changed)
+ mode(0)
+ local.setsockopt(270, 1, 11)
+ cross.setsockopt(270, 8, 1)
+ mode(1)
+ expected.update({late_local: 1, late_cross: 1})
+ notify(expected)
+ record("indexes-maintained-while-off-for-join-drop-option-and-close")
+
+ mode(0)
+ pool = [subscriber(unmapped, True) for _ in range(80)]
+ mode(1)
+ notify({**expected, **{sk: 0 for sk in pool}})
+ for sk in pool:
+ sk.close()
+ record("enabled-forces-index-despite-large-cross-list")
+
+ stop, started = threading.Event(), threading.Event()
+ updates, errors = [0], []
+
+ def toggle():
+ try:
+ while not stop.is_set():
+ mode(updates[0] % 2)
+ updates[0] += 1
+ started.set()
+ time.sleep(0.001)
+ except BaseException as error:
+ errors.append(repr(error))
+
+ worker = threading.Thread(target=toggle)
+ worker.start()
+ assert started.wait(5)
+ try:
+ for _ in range(40):
+ notify(expected)
+ finally:
+ stop.set()
+ worker.join(5)
+ assert not worker.is_alive() and not errors, errors
+ assert updates[0] >= 2
+ record("concurrent-switch-no-missing-or-duplicate", notifications=40,
+ switch_writes=updates[0])
+ print(f'1..{len(checks)}', flush=True)
+ (root / "switch-test.json").write_text(json.dumps(checks, indent=2) + "\n")
+finally:
+ if control.exists():
+ control.write_text(saved_mode)
+ for sk in sockets:
+ sk.close()
+ os.setns(original, 0)
+ os.close(original)
+ for name in reversed(namespaces):
+ subprocess.run(["ip", "netns", "del", name], check=True)
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH net-next 1/2] netlink: add an optional per-netns ROUTE multicast index
2026-11-04 3:35 ` [PATCH net-next 1/2] netlink: add an optional per-netns ROUTE multicast index patrick
@ 2026-11-04 7:31 ` Paolo Abeni
0 siblings, 0 replies; 3+ messages in thread
From: Paolo Abeni @ 2026-11-04 7:31 UTC (permalink / raw)
To: patrick, netdev
Cc: davem, edumazet, kuba, horms, corbet, rdunlap, linux-doc, skhan,
linux-kselftest, linux-api, linux-kernel
On 11/4/26 04:35, patrick wrote:
> diff --git a/Documentation/admin-guide/sysctl/net.rst b/Documentation/admin-guide/sysctl/net.rst
> index fe43e8595..0cda347ed 100644
> --- a/Documentation/admin-guide/sysctl/net.rst
> +++ b/Documentation/admin-guide/sysctl/net.rst
> @@ -280,6 +280,33 @@ checksums.
> These messages are now emitted at KERN_DEBUG and can generally be enabled
> and controlled by the dynamic_debug facility.
>
> +netlink_route_mc_index
> +---------------------
The above breaks kdoc
/P
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-04 7:31 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
[not found] <20261104033526.2917590-1-work_patrick@qq.com>
2026-11-04 3:35 ` [PATCH net-next 1/2] netlink: add an optional per-netns ROUTE multicast index patrick
2026-11-04 7:31 ` Paolo Abeni
2026-11-04 3:35 ` [PATCH net-next 2/2] selftests: net: cover ROUTE multicast namespace and switch semantics patrick
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®