From 7cb8198761e627ff3a3b4770c8f147e75c4e649d Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Tue, 30 Jun 2026 20:02:05 +0900 Subject: [PATCH 1/3] net: ipv4: report multicast group user count RTM_GETMULTICAST has been part of the rtnetlink ABI for a long time and already reports IPv4 multicast group membership through IFA_MULTICAST and IFA_CACHEINFO. It does not report how many consumers hold each membership, so userspace still has to parse /proc/net/igmp to get the Users column. Add IFA_MC_USERS as a u32 attribute carrying ip_mc_list::users in RTM_GETMULTICAST replies and entry-lifecycle notifications. This gives iproute2 enough information to migrate the IPv4 part of "ip maddr show" from procfs parsing to rtnetlink. Signed-off-by: Yuyang Huang Reviewed-by: Vadim Fedorenko Reviewed-by: Ido Schimmel Link: https://patch.msgid.link/20260630110207.37841-2-sigefriedhyy@gmail.com Signed-off-by: Paolo Abeni --- Documentation/netlink/specs/rt-addr.yaml | 4 ++++ include/uapi/linux/if_addr.h | 1 + net/ipv4/igmp.c | 2 ++ 3 files changed, 7 insertions(+) diff --git a/Documentation/netlink/specs/rt-addr.yaml b/Documentation/netlink/specs/rt-addr.yaml index 163a106c41bb..0ecbd24c890c 100644 --- a/Documentation/netlink/specs/rt-addr.yaml +++ b/Documentation/netlink/specs/rt-addr.yaml @@ -123,6 +123,9 @@ attribute-sets: - name: proto type: u8 + - + name: mc-users + type: u32 operations: @@ -176,6 +179,7 @@ operations: value: 58 attributes: &mcaddr-attrs - multicast + - mc-users - cacheinfo dump: request: diff --git a/include/uapi/linux/if_addr.h b/include/uapi/linux/if_addr.h index aa7958b4e41d..7fb630b7fe31 100644 --- a/include/uapi/linux/if_addr.h +++ b/include/uapi/linux/if_addr.h @@ -36,6 +36,7 @@ enum { IFA_RT_PRIORITY, /* u32, priority/metric for prefix route */ IFA_TARGET_NETNSID, IFA_PROTO, /* u8, address protocol */ + IFA_MC_USERS, /* u32, multicast group users */ __IFA_MAX, }; diff --git a/net/ipv4/igmp.c b/net/ipv4/igmp.c index b6337a47c141..116ce7cec80e 100644 --- a/net/ipv4/igmp.c +++ b/net/ipv4/igmp.c @@ -1473,6 +1473,7 @@ int inet_fill_ifmcaddr(struct sk_buff *skb, struct net_device *dev, ci.ifa_valid = INFINITY_LIFE_TIME; if (nla_put_in_addr(skb, IFA_MULTICAST, im->multiaddr) < 0 || + nla_put_u32(skb, IFA_MC_USERS, READ_ONCE(im->users)) < 0 || nla_put(skb, IFA_CACHEINFO, sizeof(ci), &ci) < 0) { nlmsg_cancel(skb, nlh); return -EMSGSIZE; @@ -1494,6 +1495,7 @@ static void inet_ifmcaddr_notify(struct net_device *dev, skb = nlmsg_new(NLMSG_ALIGN(sizeof(struct ifaddrmsg)) + nla_total_size(sizeof(__be32)) + + nla_total_size(sizeof(u32)) + nla_total_size(sizeof(struct ifa_cacheinfo)), GFP_KERNEL); if (!skb) From e1d0f3f0839103bc5387d2fa78eb1fd9af2d1fe1 Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Tue, 30 Jun 2026 20:02:06 +0900 Subject: [PATCH 2/3] net: ipv6: report multicast group user count The previous patch added IFA_MC_USERS and emits it for IPv4 multicast groups. Add the same snapshot attribute to IPv6 RTM_GETMULTICAST replies and entry-lifecycle notifications, carrying ifmcaddr6::mca_users. This makes the multicast rtnetlink ABI symmetric across IPv4 and IPv6 and gives userspace the same user count that /proc/net/igmp6 exposes. Signed-off-by: Yuyang Huang Reviewed-by: Vadim Fedorenko Reviewed-by: Ido Schimmel Link: https://patch.msgid.link/20260630110207.37841-3-sigefriedhyy@gmail.com Signed-off-by: Paolo Abeni --- net/ipv6/addrconf.c | 1 + net/ipv6/mcast.c | 1 + 2 files changed, 2 insertions(+) diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c index cbe681de3818..f1fe9ede1edb 100644 --- a/net/ipv6/addrconf.c +++ b/net/ipv6/addrconf.c @@ -5264,6 +5264,7 @@ int inet6_fill_ifmcaddr(struct sk_buff *skb, put_ifaddrmsg(nlh, 128, IFA_F_PERMANENT, scope, ifindex); if (nla_put_in6_addr(skb, IFA_MULTICAST, &ifmca->mca_addr) < 0 || + nla_put_u32(skb, IFA_MC_USERS, READ_ONCE(ifmca->mca_users)) < 0 || put_cacheinfo(skb, ifmca->mca_cstamp, READ_ONCE(ifmca->mca_tstamp), INFINITY_LIFE_TIME, INFINITY_LIFE_TIME) < 0) { nlmsg_cancel(skb, nlh); diff --git a/net/ipv6/mcast.c b/net/ipv6/mcast.c index 04b811b3be97..774f4c72a6fa 100644 --- a/net/ipv6/mcast.c +++ b/net/ipv6/mcast.c @@ -908,6 +908,7 @@ static void inet6_ifmcaddr_notify(struct net_device *dev, skb = nlmsg_new(NLMSG_ALIGN(sizeof(struct ifaddrmsg)) + nla_total_size(sizeof(struct in6_addr)) + + nla_total_size(sizeof(u32)) + nla_total_size(sizeof(struct ifa_cacheinfo)), GFP_KERNEL); if (!skb) From 3373cb099e6692b219328960e1d1ea70b978f20b Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Tue, 30 Jun 2026 20:02:07 +0900 Subject: [PATCH 3/3] selftests: net: check multicast group user count Extend the RTM_GETMULTICAST dump test to verify IFA_MC_USERS for both IPv4 and IPv6 multicast groups. Run each protocol test in a fresh network namespace to avoid changing host-network state or racing with unrelated multicast users. Join a fixed multicast group twice using separate sockets and check that the reported user count increases by two. Signed-off-by: Yuyang Huang Reviewed-by: Vadim Fedorenko Link: https://patch.msgid.link/20260630110207.37841-4-sigefriedhyy@gmail.com Signed-off-by: Paolo Abeni --- tools/testing/selftests/net/rtnetlink.py | 101 ++++++++++++++++++++--- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py index 3622413d793d..0c67c7c00d84 100755 --- a/tools/testing/selftests/net/rtnetlink.py +++ b/tools/testing/selftests/net/rtnetlink.py @@ -2,27 +2,106 @@ # SPDX-License-Identifier: GPL-2.0 import socket +import struct import time -from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_ge, ksft_true, KsftSkipEx +from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01' +IPV4_TEST_MULTICAST = b'\xef\x01\x01\x01' +IPV6_TEST_MULTICAST = bytes.fromhex('ff020000000000000000000000000123') + + +def _users_for(rtnl: RtnlAddrFamily, family: int, grp: bytes, ifindex: int): + """Return mc-users for grp on ifindex, or 0 if absent.""" + + addrs = rtnl.getmulticast({"ifa-family": family}, dump=True) + matches = [addr for addr in addrs + if addr['multicast'] == grp and addr['ifa-index'] == ifindex] + if not matches: + return 0 + if 'mc-users' not in matches[0]: + return None + + return matches[0]['mc-users'] + def dump_mcaddr_check() -> None: """ - Verify that at least one interface has the IPv4 all-hosts multicast address. - At least the loopback interface should have this address. + Verify IPv4 multicast addresses and their user counts in RTM_GETMULTICAST. """ - rtnl = RtnlAddrFamily() - addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True) + with NetNS() as ns: + with NetNSEnter(str(ns)): + ip("link set lo up") + rtnl = RtnlAddrFamily() + lo_idx = socket.if_nametoindex('lo') + addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True) - all_host_multicasts = [ - addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST - ] + all_host_multicasts = [ + addr for addr in addresses + if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST + ] + + ksft_ge(len(all_host_multicasts), 1, + "No interface found with the IPv4 all-hosts multicast address") + + mreq = IPV4_TEST_MULTICAST + socket.inet_aton('127.0.0.1') + before = _users_for(rtnl, socket.AF_INET, IPV4_TEST_MULTICAST, lo_idx) + if before is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS") + + s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s1.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + s2.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + + after_join = _users_for(rtnl, socket.AF_INET, + IPV4_TEST_MULTICAST, lo_idx) + if after_join is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS") + ksft_eq(after_join - before, 2, + f"users delta != 2 after two joins " + f"(before={before}, after={after_join})") + finally: + s1.close() + s2.close() + + +def dump_mcaddr6_check() -> None: + """ + Verify IPv6 multicast addresses and their user counts in RTM_GETMULTICAST. + """ + + with NetNS() as ns: + with NetNSEnter(str(ns)): + ip("link set lo up") + rtnl = RtnlAddrFamily() + lo_idx = socket.if_nametoindex('lo') + before = _users_for(rtnl, socket.AF_INET6, + IPV6_TEST_MULTICAST, lo_idx) + if before is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS for IPv6") + + mreq = IPV6_TEST_MULTICAST + struct.pack('=I', lo_idx) + s1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + try: + s1.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + s2.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + + after_join = _users_for(rtnl, socket.AF_INET6, + IPV6_TEST_MULTICAST, lo_idx) + if after_join is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS for IPv6") + ksft_eq(after_join - before, 2, + f"IPv6 users delta != 2 after two joins " + f"(before={before}, after={after_join})") + finally: + s1.close() + s2.close() - ksft_ge(len(all_host_multicasts), 1, - "No interface found with the IPv4 all-hosts multicast address") def ipv4_devconf_notify() -> None: """ @@ -56,7 +135,7 @@ def ipv4_devconf_notify() -> None: f"No 'forwarding on' notificiation found for interface {ifname}") def main() -> None: - ksft_run([dump_mcaddr_check, ipv4_devconf_notify]) + ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify]) ksft_exit() if __name__ == "__main__":