| 1 | |
| 2 | |
| 3 |
|
| 4 | import socket
|
| 5 | import struct
|
| 6 | import time
|
| 7 | from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx
|
| 8 | from lib.py import ksft_not_in, ksft_not_none
|
| 9 | from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily, RtnlRouteFamily
|
| 10 | from lib.py import defer
|
| 11 |
|
| 12 | IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
|
| 13 | IPV4_TEST_MULTICAST = b'\xef\x01\x01\x01'
|
| 14 | IPV6_TEST_MULTICAST = bytes.fromhex('ff020000000000000000000000000123')
|
| 15 |
|
| 16 |
|
| 17 | def _users_for(rtnl: RtnlAddrFamily, family: int, grp: bytes, ifindex: int):
|
| 18 |
|
| 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 |
|
| 31 | def 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 |
|
| 74 | def 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 |
|
| 108 | def 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 |
|
| 139 | def _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 |
|
| 147 | def _wait_route_ntf(rtnl, name, dst_len, dst=None, deadline=10):
|
| 148 |
|
| 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 |
|
| 162 | def _collect_route_ntfs(rtnl, name, want, deadline=10):
|
| 163 |
|
| 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 |
|
| 178 | def _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 |
|
| 183 | def ipv6_route_del_reason_expired() -> None:
|
| 184 |
|
| 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 |
|
| 201 | def _send_ra(sock, ifindex, lifetime, rio=None, pio=None):
|
| 202 |
|
| 203 |
|
| 204 | |
| 205 | |
| 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 | |
| 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 | |
| 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 |
|
| 220 | def _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 |
|
| 229 | def _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 |
|
| 251 | def 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 | |
| 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 | |
| 271 | |
| 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 |
|
| 297 | def 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 |
|
| 317 | def 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 |
|
| 324 | if __name__ == "__main__":
|
| 325 | main()
|