Merge branch 'net-report-multicast-group-user-count'

Yuyang Huang says:

====================
net: report multicast group user count

RTM_GETMULTICAST reports IPv4 and IPv6 multicast group membership, but
does not include the per-group user count. Userspace therefore still has
to parse /proc/net/igmp and /proc/net/igmp6 to obtain the Users column.
In particular, this prevents iproute2 from moving "ip maddr show"
entirely from procfs to rtnetlink.

Add IFA_MC_USERS to carry the user count in RTM_GETMULTICAST dumps and
RTM_NEWMULTICAST / RTM_DELMULTICAST notifications for both address
families. Update the rt-addr YNL specification and extend the rtnetlink
selftest to verify that two joins increase the reported count by two.
====================

Link: https://patch.msgid.link/20260630110207.37841-1-sigefriedhyy@gmail.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Paolo Abeni
2026-07-03 08:50:45 +02:00
6 changed files with 99 additions and 11 deletions

View File

@@ -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:

View File

@@ -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,
};

View File

@@ -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)

View File

@@ -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);

View File

@@ -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)

View File

@@ -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__":