xref: /linux/tools/testing/selftests/net/rtnetlink.py (revision 0e87a4a83e1306b36f1ebd3cc10eaf31476dfe19)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4import socket
5import struct
6import time
7from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx
8from lib.py import ksft_in, ksft_not_in, ksft_not_none
9from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily, RtnlRouteFamily
10from lib.py import defer
11
12IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
13IPV4_TEST_MULTICAST = b'\xef\x01\x01\x01'
14IPV6_TEST_MULTICAST = bytes.fromhex('ff020000000000000000000000000123')
15ETH_ALL_HOSTS_MULTICAST = bytes.fromhex('01005e000001')
16ETH_TEST_MULTICAST_STR = '01:00:5e:01:01:01'
17ETH_TEST_MULTICAST = bytes.fromhex(ETH_TEST_MULTICAST_STR.replace(':', ''))
18ETH_PEER_MULTICAST_STR = '01:00:5e:02:02:02'
19ETH_PEER_MULTICAST = bytes.fromhex(ETH_PEER_MULTICAST_STR.replace(':', ''))
20
21
22def _users_for(rtnl: RtnlAddrFamily, family: int, grp: bytes, ifindex: int):
23    """Return mc-users for grp on ifindex, or 0 if absent."""
24
25    addrs = rtnl.getmulticast({"ifa-family": family}, dump=True)
26    matches = [addr for addr in addrs
27               if addr['multicast'] == grp and addr['ifa-index'] == ifindex]
28    if not matches:
29        return 0
30    if 'mc-users' not in matches[0]:
31        return None
32
33    return matches[0]['mc-users']
34
35
36def dump_mcaddr_check() -> None:
37    """
38    Verify IPv4 multicast addresses and their user counts in RTM_GETMULTICAST.
39    """
40
41    with NetNS() as ns:
42        with NetNSEnter(str(ns)):
43            ip("link set lo up")
44            rtnl = RtnlAddrFamily()
45            lo_idx = socket.if_nametoindex('lo')
46            addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
47
48            all_host_multicasts = [
49                addr for addr in addresses
50                if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST
51            ]
52
53            ksft_ge(len(all_host_multicasts), 1,
54                    "No interface found with the IPv4 all-hosts multicast address")
55
56            mreq = IPV4_TEST_MULTICAST + socket.inet_aton('127.0.0.1')
57            before = _users_for(rtnl, socket.AF_INET, IPV4_TEST_MULTICAST, lo_idx)
58            if before is None:
59                raise KsftSkipEx("kernel does not expose IFA_MC_USERS")
60
61            s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
62            s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
63            try:
64                s1.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
65                s2.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
66
67                after_join = _users_for(rtnl, socket.AF_INET,
68                                        IPV4_TEST_MULTICAST, lo_idx)
69                if after_join is None:
70                    raise KsftSkipEx("kernel does not expose IFA_MC_USERS")
71                ksft_eq(after_join - before, 2,
72                        f"users delta != 2 after two joins "
73                        f"(before={before}, after={after_join})")
74            finally:
75                s1.close()
76                s2.close()
77
78
79def dump_mcaddr6_check() -> None:
80    """
81    Verify IPv6 multicast addresses and their user counts in RTM_GETMULTICAST.
82    """
83
84    with NetNS() as ns:
85        with NetNSEnter(str(ns)):
86            ip("link set lo up")
87            rtnl = RtnlAddrFamily()
88            lo_idx = socket.if_nametoindex('lo')
89            before = _users_for(rtnl, socket.AF_INET6,
90                                IPV6_TEST_MULTICAST, lo_idx)
91            if before is None:
92                raise KsftSkipEx("kernel does not expose IFA_MC_USERS for IPv6")
93
94            mreq = IPV6_TEST_MULTICAST + struct.pack('=I', lo_idx)
95            s1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
96            s2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
97            try:
98                s1.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
99                s2.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
100
101                after_join = _users_for(rtnl, socket.AF_INET6,
102                                        IPV6_TEST_MULTICAST, lo_idx)
103                if after_join is None:
104                    raise KsftSkipEx("kernel does not expose IFA_MC_USERS for IPv6")
105                ksft_eq(after_join - before, 2,
106                        f"IPv6 users delta != 2 after two joins "
107                        f"(before={before}, after={after_join})")
108            finally:
109                s1.close()
110                s2.close()
111
112
113def dump_mcaddr_l2_check() -> None:
114    """
115    Verify link-layer multicast addresses in an AF_PACKET RTM_GETMULTICAST
116    dump: the ifa-index filter, mc-users, the global flag and
117    target-netnsid.
118    """
119
120    with NetNS() as ns, NetNSEnter(str(ns)):
121        for ifname in ("dummy1", "dummy2"):
122            ip(f"link add name {ifname} type dummy")
123            ip(f"link set {ifname} up")
124        dev_idx = socket.if_nametoindex("dummy1")
125        ip(f"maddr add {ETH_TEST_MULTICAST_STR} dev dummy1")
126
127        rtnl = RtnlAddrFamily()
128        defer(rtnl.close)
129        addresses = rtnl.getmulticast(
130            {"ifa-family": socket.AF_PACKET, "ifa-index": dev_idx},
131            dump=True)
132
133        # dummy2 has entries as well, only dummy1 may be listed
134        ksft_eq({addr['ifa-index'] for addr in addresses}, {dev_idx},
135                "AF_PACKET multicast dump ignored ifa-index filter")
136
137        entries = {addr['multicast']: addr for addr in addresses}
138
139        # Bringing an Ethernet device up joins 224.0.0.1, which maps
140        # to 01:00:5e:00:00:01 in the device multicast list.
141        all_hosts = entries.get(ETH_ALL_HOSTS_MULTICAST)
142        ksft_not_none(all_hosts,
143                      "dummy1 does not have the all-hosts link-layer address")
144        if all_hosts is not None:
145            ksft_not_in('global', all_hosts['flags'],
146                        "protocol entry is global")
147
148        static = entries.get(ETH_TEST_MULTICAST)
149        ksft_not_none(static, "dummy1 does not have the SIOCADDMULTI address")
150        if static is not None:
151            ksft_eq(static['mc-users'], 1,
152                    "unexpected mc-users for the SIOCADDMULTI address")
153            ksft_in('global', static['flags'],
154                    "SIOCADDMULTI entry is not global")
155
156        # target-netnsid dumps another netns, ifa-index is relative to it
157        with NetNS() as peer:
158            ip(f"netns set {peer} 5")
159            ip("link add name dummy3 type dummy", ns=peer)
160            ip("link set dummy3 up", ns=peer)
161            ip(f"maddr add {ETH_PEER_MULTICAST_STR} dev dummy3", ns=peer)
162            peer_idx = ip("link show dummy3", json=True, ns=peer)[0]['ifindex']
163
164            addresses = rtnl.getmulticast(
165                {"ifa-family": socket.AF_PACKET, "target-netnsid": 5,
166                 "ifa-index": peer_idx}, dump=True)
167            ksft_eq({(addr['ifa-index'], addr['target-netnsid'])
168                     for addr in addresses}, {(peer_idx, 5)},
169                    "target-netnsid did not dump the peer netns")
170            # dummy1 in this netns can have the same ifindex as dummy3
171            ksft_in(ETH_PEER_MULTICAST,
172                    {addr['multicast'] for addr in addresses},
173                    "target-netnsid did not dump the peer device")
174
175
176def ipv4_devconf_notify() -> None:
177    """
178    Configure an interface and set ipv4-devconf values through netlink
179    to verify that the appropriate netlink notifications are being sent.
180    """
181
182    with NetNS() as ns:
183        with NetNSEnter(str(ns)):
184            ifname = "dummy1"
185            ip(f"link add name {ifname} type dummy", ns=str(ns))
186
187            with bkg("ip monitor", ns=str(ns)) as cmd_obj:
188                time.sleep(1)
189                try:
190                    ip(f"link set dev {ifname} inet forwarding on")
191                    ip(f"link set dev {ifname} inet proxy_arp on")
192                    ip(f"link set dev {ifname} inet rp_filter 1")
193                    ip(f"link set dev {ifname} inet ignore_routes_with_linkdown on")
194                except CmdExitFailure:
195                    raise KsftSkipEx("iproute2 does not support IPv4 devconf attributes")
196                time.sleep(1)
197
198    ksft_true(f"inet {ifname} ignore_routes_with_linkdown on" in cmd_obj.stdout,
199              f"No 'ignore_routes_with_linkdown on' notificiation found for interface {ifname}")
200    ksft_true(f"inet {ifname} rp_filter strict" in cmd_obj.stdout,
201              f"No 'rp_filter strict' notificiation found for interface {ifname}")
202    ksft_true(f"inet {ifname} proxy_neigh on" in cmd_obj.stdout,
203              f"No 'proxy_neigh on' notificiation found for interface {ifname}")
204    ksft_true(f"inet {ifname} forwarding on" in cmd_obj.stdout,
205              f"No 'forwarding on' notificiation found for interface {ifname}")
206
207def _rtnl_route_subscribe(ns):
208    with NetNSEnter(str(ns)):
209        rtnl = RtnlRouteFamily()
210    defer(rtnl.close)
211    rtnl.ntf_subscribe("rtnlgrp-ipv6-route")
212    return rtnl
213
214
215def _wait_route_ntf(rtnl, name, dst_len, dst=None, deadline=10):
216    """Return the attrs of the first matching notification, None on timeout."""
217
218    for msg in rtnl.poll_ntf(duration=deadline):
219        if msg['name'] != name:
220            continue
221        attrs = msg['msg']
222        if attrs['rtm-dst-len'] != dst_len:
223            continue
224        if dst is not None and attrs.get('dst') != dst:
225            continue
226        return attrs
227    return None
228
229
230def _collect_route_ntfs(rtnl, name, want, deadline=10):
231    """Gather attrs of matching notifications, keyed by (dst_len, dst)."""
232
233    seen = {}
234    for msg in rtnl.poll_ntf(duration=deadline):
235        if msg['name'] != name:
236            continue
237        attrs = msg['msg']
238        key = (attrs['rtm-dst-len'], attrs.get('dst'))
239        if key in want:
240            seen[key] = attrs
241            if len(seen) == len(want):
242                break
243    return seen
244
245
246def _write_ipv6_sysctl(name, value):
247    with open(f"/proc/sys/net/ipv6/{name}", "w") as f:
248        f.write(f"{value}\n")
249
250
251def ipv6_route_del_reason_expired() -> None:
252    """An expired route reports RTA_DEL_REASON == expired."""
253
254    with NetNS() as ns:
255        rtnl = _rtnl_route_subscribe(ns)
256        with NetNSEnter(str(ns)):
257            _write_ipv6_sysctl("route/gc_interval", 2)
258        ip("link add name dummy1 type dummy", ns=str(ns))
259        ip("link set dev dummy1 up", ns=str(ns))
260        ip("-6 route add 2001:db8:2::/64 dev dummy1 expires 2", ns=str(ns))
261
262        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:2::',
263                                deadline=15)
264        ksft_not_none(attrs, "no RTM_DELROUTE for the expired route")
265        if attrs is not None:
266            ksft_eq(attrs.get('del-reason'), 'expired')
267
268
269def _send_ra(sock, ifindex, lifetime, rio=None, pio=None):
270    """The kernel fills in the ICMPv6 checksum on raw ICMPv6 sockets."""
271
272    # type, code, cksum, hop limit, flags, router lifetime,
273    # reachable time, retrans timer
274    ra = struct.pack('!BBHBBHII', 134, 0, 0, 64, 0, lifetime, 0, 0)
275    if rio is not None:
276        prefix, plen, rio_lifetime = rio
277        # RFC 4191 route information option, /64 prefix (8 bytes)
278        ra += struct.pack('!BBBBI', 24, 2, plen, 0, rio_lifetime)
279        ra += socket.inet_pton(socket.AF_INET6, prefix)[:8]
280    if pio is not None:
281        prefix, plen, valid_lft = pio
282        # RFC 4861 prefix information option, on-link only (L set, A clear)
283        ra += struct.pack('!BBBBIII', 3, 4, plen, 0x80, valid_lft, 0, 0)
284        ra += socket.inet_pton(socket.AF_INET6, prefix)
285    sock.sendto(ra, ('ff02::1', 0, 0, ifindex))
286
287
288def _ra_router_sock(ns_r, ifname):
289    with NetNSEnter(str(ns_r)):
290        sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW,
291                             socket.IPPROTO_ICMPV6)
292        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
293        defer(sock.close)
294        return sock, socket.if_nametoindex(ifname)
295
296
297def _ra_advertise_routes(rtnl, sock, ifindex, want, **ra_opts):
298    """
299    Sending fails with EADDRNOTAVAIL while the router's link-local
300    address is still tentative. addrconf_dad_start() only queues
301    addrconf_dad_work(), and IFA_F_TENTATIVE is cleared when that work
302    item runs, so retry until it does.
303    """
304
305    seen = {}
306    for _ in range(10):
307        try:
308            _send_ra(sock, ifindex, **ra_opts)
309        except OSError:
310            time.sleep(0.2)
311            continue
312        seen.update(_collect_route_ntfs(rtnl, 'newroute-ntf',
313                                        want - set(seen.keys()), deadline=2))
314        if len(seen) == len(want):
315            break
316    return seen
317
318
319def ipv6_route_del_reason_ra_withdrawn() -> None:
320    """
321    Routes withdrawn by a zero-lifetime RA (router lifetime, RFC 4861
322    PIO, RFC 4191 RIO) report RTA_DEL_REASON == ra-withdrawn.
323    """
324
325    # (rtm-dst-len, dst); the default route carries no RTA_DST
326    routes = {(0, None), (64, '2001:db8:6::'), (64, '2001:db8:5::')}
327
328    with NetNS() as ns_h, NetNS() as ns_r:
329        ip(f"link add veth0 netns {ns_h} type veth peer name veth1 netns {ns_r}")
330        with NetNSEnter(str(ns_h)):
331            _write_ipv6_sysctl("conf/veth0/accept_ra", 2)
332            _write_ipv6_sysctl("conf/veth0/forwarding", 0)
333            try:
334                _write_ipv6_sysctl("conf/veth0/accept_ra_rt_info_max_plen", 64)
335            except FileNotFoundError:
336                raise KsftSkipEx("no CONFIG_IPV6_ROUTE_INFO")
337        with NetNSEnter(str(ns_r)):
338            # skip the DAD probe so the router's link-local source only
339            # has to wait for addrconf_dad_work() to clear IFA_F_TENTATIVE
340            _write_ipv6_sysctl("conf/veth1/accept_dad", 0)
341        ip("link set dev veth0 up", ns=str(ns_h))
342        ip("link set dev veth1 up", ns=str(ns_r))
343
344        rtnl = _rtnl_route_subscribe(ns_h)
345        sock, ifindex = _ra_router_sock(ns_r, "veth1")
346
347        seen = _ra_advertise_routes(rtnl, sock, ifindex, routes,
348                                    lifetime=1800,
349                                    rio=('2001:db8:5::', 64, 600),
350                                    pio=('2001:db8:6::', 64, 600))
351        ksft_eq(set(seen), routes, "not all RA routes were installed")
352        if set(seen) != routes:
353            return
354
355        _send_ra(sock, ifindex, 0, rio=('2001:db8:5::', 64, 0),
356                 pio=('2001:db8:6::', 64, 0))
357        seen = _collect_route_ntfs(rtnl, 'delroute-ntf', routes)
358        for key in routes:
359            attrs = seen.get(key)
360            ksft_not_none(attrs, f"no RTM_DELROUTE for {key}")
361            if attrs is not None:
362                ksft_eq(attrs.get('del-reason'), 'ra-withdrawn')
363
364
365def ipv6_route_del_reason_absent() -> None:
366    """
367    A deletion path that records no cause (here a userspace request)
368    must not carry RTA_DEL_REASON at all.
369    """
370
371    with NetNS() as ns:
372        rtnl = _rtnl_route_subscribe(ns)
373        ip("link add name dummy1 type dummy", ns=str(ns))
374        ip("link set dev dummy1 up", ns=str(ns))
375        ip("-6 route add 2001:db8:1::/64 dev dummy1", ns=str(ns))
376        ip("-6 route del 2001:db8:1::/64 dev dummy1", ns=str(ns))
377
378        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:1::')
379        ksft_not_none(attrs, "no RTM_DELROUTE for 2001:db8:1::/64")
380        if attrs is not None:
381            ksft_not_in('del-reason', attrs,
382                        "user deletion must not carry del-reason")
383
384
385def _insert_and_get_addrs_ipv4(test_addrs: list[str], scopes: list[str]) -> list[str]:
386    with NetNS() as ns, NetNSEnter(str(ns)):
387        dev_name = "dummy_dev"
388
389        ip(f"link add name {dev_name} type dummy", ns=str(ns))
390        for test_addr, scope in zip(test_addrs, scopes):
391            ip(f"address add {test_addr}/24 dev {dev_name} scope {scope}", ns=str(ns))
392
393        rtnl = RtnlAddrFamily()
394        addrs = rtnl.getaddr({"ifa-family": socket.AF_INET}, dump=True)
395        return [addr["address"] for addr in addrs]
396
397
398def ipv4_verify_same_scope_addr_order() -> None:
399    """
400    After inserting multiple same scope IPv4 addresses, their order
401    must be the same as the insertion order. The only aspect affecting
402    this are primary addresses, which precede secondary ones.
403    """
404
405    primary_first = ["192.0.2.1", "203.0.113.1", "192.0.2.2", "203.0.113.2"]
406    scopes = ["global"] * 4
407    expected_result = primary_first
408    resulting_list = _insert_and_get_addrs_ipv4(primary_first, scopes)
409    ksft_eq(resulting_list, expected_result, "Unexpected IPv4 address order")
410
411    subnet_first = ["192.0.2.1", "192.0.2.2", "203.0.113.1", "203.0.113.2"]
412    # Scope and expected result stay the same.
413    resulting_list = _insert_and_get_addrs_ipv4(subnet_first, scopes)
414    ksft_eq(resulting_list, expected_result, "Unexpected IPv4 address order")
415
416
417def ipv4_verify_inter_scope_addr_order() -> None:
418    """
419    When IPv4 addresses from different scopes are inserted,
420    primary link local addresses must precede global ones.
421
422    Address ordering across different scopes has also
423    been attempted to be patched, bringing in a new risk of a user-space
424    regression, similar to the same scope equivalent. This will further
425    consolidate the implementation differences of both protocols.
426    """
427
428    test_addrs = ["192.0.2.1", "203.0.113.1", "192.0.2.2", "203.0.113.2"]
429
430    link_first = ["link", "global", "link", "global"]
431    expected_result = test_addrs
432    resulting_list = _insert_and_get_addrs_ipv4(test_addrs, link_first)
433    ksft_eq(resulting_list, expected_result, "Unexpected IPv4 address order across scopes")
434
435    global_first = ["global", "link", "global", "link"]
436    expected_result = ["203.0.113.1", "192.0.2.1", "192.0.2.2", "203.0.113.2"]
437    resulting_list = _insert_and_get_addrs_ipv4(test_addrs, global_first)
438    ksft_eq(resulting_list, expected_result, "Unexpected IPv4 address order across scopes")
439
440
441def _insert_and_get_addrs_ipv6(test_addrs: list[str]) -> list[str]:
442    with NetNS() as ns, NetNSEnter(str(ns)):
443        dev_name = "dummy_dev"
444
445        ip(f"link add name {dev_name} type dummy", ns=str(ns))
446        for test_addr in test_addrs:
447            ip(f"address add {test_addr}/64 dev {dev_name}", ns=str(ns))
448
449        rtnl = RtnlAddrFamily()
450        addrs = rtnl.getaddr({"ifa-family": socket.AF_INET6}, dump=True)
451        return [addr["address"] for addr in addrs]
452
453
454def ipv6_verify_same_scope_addr_order() -> None:
455    """
456    After inserting multiple same scope IPv6 addresses, their order
457    must be the _reverse_ of the insertion order.
458
459    While this behaviour is different from how IPv4 acts,
460    updating the IPv6 implementation to act the same way
461    has proved to cause user-space application regressions
462    (particularly in NetworkManager). This behaviour is being
463    tested to consolidate it as being expected and correct.
464    """
465
466    addr_list = ["2001:db8::1", "2001:db8::2", "2001:db8::3"]
467    expected_result = addr_list[::-1]
468    resulting_list = _insert_and_get_addrs_ipv6(addr_list)
469    ksft_eq(resulting_list, expected_result, "Unexpected IPv6 address order")
470
471
472def ipv6_verify_inter_scope_addr_order() -> None:
473    """
474    Inserted IPv6 addresses from different scopes must have
475    global primary address precede link local ones. This again
476    is the _reverse_ of how IPv4 addresses are ordered.
477
478    To prevent potential user-space regressions with IPv6
479    addresses, the inter-scope insertion order is also being tested.
480    """
481
482    global_first = ["2001:db8::1", "fe80::1", "2001:db8::2", "fe80::2"]
483    # Not only must the global addresses be first, but their insertion order must be reversed.
484    expected_result = ["2001:db8::2", "2001:db8::1", "fe80::2", "fe80::1"]
485    resulting_list = _insert_and_get_addrs_ipv6(global_first)
486    ksft_eq(resulting_list, expected_result, "Unexpected IPv6 address order across scopes")
487
488    link_first = ["fe80::1", "2001:db8::1", "fe80::2", "2001:db8::2"]
489    # Expected result stays the same.
490    resulting_list = _insert_and_get_addrs_ipv6(link_first)
491    ksft_eq(resulting_list, expected_result, "Unexpected IPv6 address order across scopes")
492
493
494def main() -> None:
495    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, dump_mcaddr_l2_check,
496              ipv4_devconf_notify,
497              ipv6_route_del_reason_expired,
498              ipv6_route_del_reason_ra_withdrawn,
499              ipv6_route_del_reason_absent,
500              ipv4_verify_same_scope_addr_order, ipv4_verify_inter_scope_addr_order,
501              ipv6_verify_same_scope_addr_order, ipv6_verify_inter_scope_addr_order])
502    ksft_exit()
503
504if __name__ == "__main__":
505    main()
506