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