xref: /linux/tools/testing/selftests/net/rtnetlink.py (revision 91ec2035134982b98fab0609a9fd8480e8217dc1)
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 main() -> None:
318    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify,
319              ipv6_route_del_reason_expired,
320              ipv6_route_del_reason_ra_withdrawn,
321              ipv6_route_del_reason_absent])
322    ksft_exit()
323
324if __name__ == "__main__":
325    main()
326