1# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause 2# 3# pylint: disable=missing-class-docstring, missing-function-docstring 4# pylint: disable=too-many-branches, too-many-locals, too-many-instance-attributes 5# pylint: disable=too-many-lines 6 7""" 8YAML Netlink Library 9 10An implementation of the genetlink and raw netlink protocols. 11""" 12 13from collections import namedtuple 14from enum import Enum 15import functools 16import os 17import random 18import socket 19import struct 20from struct import Struct 21import sys 22import ipaddress 23import uuid 24import queue 25import selectors 26import time 27 28from .nlspec import SpecFamily 29 30# 31# Generic Netlink code which should really be in some library, but I can't quickly find one. 32# 33 34 35class YnlException(Exception): 36 pass 37 38 39# pylint: disable=too-few-public-methods 40class Netlink: 41 # Netlink socket 42 SOL_NETLINK = 270 43 44 NETLINK_ADD_MEMBERSHIP = 1 45 NETLINK_LISTEN_ALL_NSID = 8 46 NETLINK_CAP_ACK = 10 47 NETLINK_EXT_ACK = 11 48 NETLINK_GET_STRICT_CHK = 12 49 50 # Netlink message 51 NLMSG_ERROR = 2 52 NLMSG_DONE = 3 53 54 NLM_F_REQUEST = 1 55 NLM_F_ACK = 4 56 NLM_F_ROOT = 0x100 57 NLM_F_MATCH = 0x200 58 59 NLM_F_REPLACE = 0x100 60 NLM_F_EXCL = 0x200 61 NLM_F_CREATE = 0x400 62 NLM_F_APPEND = 0x800 63 64 NLM_F_CAPPED = 0x100 65 NLM_F_ACK_TLVS = 0x200 66 67 NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH 68 69 NLA_F_NESTED = 0x8000 70 NLA_F_NET_BYTEORDER = 0x4000 71 72 NLA_TYPE_MASK = NLA_F_NESTED | NLA_F_NET_BYTEORDER 73 74 # Genetlink defines 75 NETLINK_GENERIC = 16 76 77 GENL_ID_CTRL = 0x10 78 79 # nlctrl 80 CTRL_CMD_GETFAMILY = 3 81 CTRL_CMD_GETPOLICY = 10 82 83 CTRL_ATTR_FAMILY_ID = 1 84 CTRL_ATTR_FAMILY_NAME = 2 85 CTRL_ATTR_MAXATTR = 5 86 CTRL_ATTR_MCAST_GROUPS = 7 87 CTRL_ATTR_POLICY = 8 88 CTRL_ATTR_OP_POLICY = 9 89 CTRL_ATTR_OP = 10 90 91 CTRL_ATTR_MCAST_GRP_NAME = 1 92 CTRL_ATTR_MCAST_GRP_ID = 2 93 94 CTRL_ATTR_POLICY_DO = 1 95 CTRL_ATTR_POLICY_DUMP = 2 96 97 # Extack types 98 NLMSGERR_ATTR_MSG = 1 99 NLMSGERR_ATTR_OFFS = 2 100 NLMSGERR_ATTR_COOKIE = 3 101 NLMSGERR_ATTR_POLICY = 4 102 NLMSGERR_ATTR_MISS_TYPE = 5 103 NLMSGERR_ATTR_MISS_NEST = 6 104 105 # Policy types 106 NL_POLICY_TYPE_ATTR_TYPE = 1 107 NL_POLICY_TYPE_ATTR_MIN_VALUE_S = 2 108 NL_POLICY_TYPE_ATTR_MAX_VALUE_S = 3 109 NL_POLICY_TYPE_ATTR_MIN_VALUE_U = 4 110 NL_POLICY_TYPE_ATTR_MAX_VALUE_U = 5 111 NL_POLICY_TYPE_ATTR_MIN_LENGTH = 6 112 NL_POLICY_TYPE_ATTR_MAX_LENGTH = 7 113 NL_POLICY_TYPE_ATTR_POLICY_IDX = 8 114 NL_POLICY_TYPE_ATTR_POLICY_MAXTYPE = 9 115 NL_POLICY_TYPE_ATTR_BITFIELD32_MASK = 10 116 NL_POLICY_TYPE_ATTR_PAD = 11 117 NL_POLICY_TYPE_ATTR_MASK = 12 118 NL_POLICY_TYPE_ATTR_BYTE_ORDER = 13 119 120 AttrType = Enum('AttrType', ['flag', 'u8', 'u16', 'u32', 'u64', 121 's8', 's16', 's32', 's64', 122 'binary', 'string', 'nul-string', 123 'nested', 'nested-array', 124 'bitfield32', 'sint', 'uint']) 125 126 ByteOrder = Enum('ByteOrder', ['big-endian'], start=1) 127 128class NlError(Exception): 129 def __init__(self, nl_msg): 130 self.nl_msg = nl_msg 131 self.error = -nl_msg.error 132 133 def __str__(self): 134 msg = "Netlink error: " 135 136 extack = self.nl_msg.extack.copy() if self.nl_msg.extack else {} 137 if 'msg' in extack: 138 msg += extack['msg'] + ': ' 139 del extack['msg'] 140 msg += os.strerror(self.error) 141 if extack: 142 msg += ' ' + str(extack) 143 return msg 144 145 146class ConfigError(Exception): 147 pass 148 149 150class NlPolicy: 151 """Kernel policy for one mode (do or dump) of one operation. 152 153 Returned by YnlFamily.get_policy(). Attributes of the policy 154 are accessible as attributes of the object. Nested policies 155 can be accessed indexing the object like a dictionary:: 156 157 pol = ynl.get_policy('page-pool-stats-get', 'do') 158 pol['info'].type # 'nested' 159 pol['info']['id'].type # 'uint' 160 pol['info']['id'].min_value # 1 161 162 Each policy entry always has a 'type' attribute (e.g. u32, string, 163 nested). Optional attributes depending on the 'type': min-value, 164 max-value, min-length, max-length, mask, byte-order. 165 166 Policies can form infinite nesting loops. These loops are trimmed 167 when policy is converted to a dict with pol.to_dict(). 168 """ 169 def __init__(self, ynl, policy_idx, policy_table, attr_set, props=None): 170 self._policy_idx = policy_idx 171 self._policy_table = policy_table 172 self._ynl = ynl 173 self._props = props or {} 174 self._entries = {} 175 self._cache = {} 176 if policy_idx is not None and policy_idx in policy_table: 177 for attr_id, decoded in policy_table[policy_idx].items(): 178 if attr_set and attr_id in attr_set.attrs_by_val: 179 spec = attr_set.attrs_by_val[attr_id] 180 name = spec['name'] 181 else: 182 spec = None 183 name = f'attr-{attr_id}' 184 self._entries[name] = (spec, decoded) 185 186 def __getitem__(self, name): 187 """Descend into a nested policy by attribute name.""" 188 if name not in self._cache: 189 spec, decoded = self._entries[name] 190 props = dict(decoded) 191 child_idx = None 192 child_set = None 193 if 'policy-idx' in props: 194 child_idx = props.pop('policy-idx') 195 if spec and 'nested-attributes' in spec.yaml: 196 child_set = self._ynl.attr_sets[spec.yaml['nested-attributes']] 197 self._cache[name] = NlPolicy(self._ynl, child_idx, 198 self._policy_table, 199 child_set, props) 200 return self._cache[name] 201 202 def __getattr__(self, name): 203 """Access this policy entry's own properties (type, min-value, etc.). 204 205 Underscores in the name are converted to dashes, so that 206 pol.min_value looks up "min-value". 207 """ 208 key = name.replace('_', '-') 209 try: 210 # Hack for level-0 which we still want to have .type but we don't 211 # want type to pointlessly show up in the dict / JSON form. 212 if not self._props and name == "type": 213 return "nested" 214 return self._props[key] 215 except KeyError: 216 raise AttributeError(name) 217 218 def get(self, name, default=None): 219 """Look up a child policy entry by attribute name, with a default.""" 220 try: 221 return self[name] 222 except KeyError: 223 return default 224 225 def __contains__(self, name): 226 return name in self._entries 227 228 def __len__(self): 229 return len(self._entries) 230 231 def __iter__(self): 232 return iter(self._entries) 233 234 def keys(self): 235 """Return attribute names accepted by this policy.""" 236 return self._entries.keys() 237 238 def to_dict(self, seen=None): 239 """Convert to a plain dict, suitable for JSON serialization. 240 241 Nested NlPolicy objects are expanded recursively. Cyclic 242 references are trimmed (resolved to just {"type": "nested"}). 243 """ 244 if seen is None: 245 seen = set() 246 result = dict(self._props) 247 if self._policy_idx is not None: 248 if self._policy_idx not in seen: 249 seen = seen | {self._policy_idx} 250 children = {} 251 for name in self: 252 children[name] = self[name].to_dict(seen) 253 if self._props: 254 result['policy'] = children 255 else: 256 result = children 257 return result 258 259 def __repr__(self): 260 return repr(self.to_dict()) 261 262 263class NlAttr: 264 ScalarFormat = namedtuple('ScalarFormat', ['native', 'big', 'little']) 265 type_formats = { 266 'u8' : ScalarFormat(Struct('B'), Struct("B"), Struct("B")), 267 's8' : ScalarFormat(Struct('b'), Struct("b"), Struct("b")), 268 'u16': ScalarFormat(Struct('H'), Struct(">H"), Struct("<H")), 269 's16': ScalarFormat(Struct('h'), Struct(">h"), Struct("<h")), 270 'u32': ScalarFormat(Struct('I'), Struct(">I"), Struct("<I")), 271 's32': ScalarFormat(Struct('i'), Struct(">i"), Struct("<i")), 272 'u64': ScalarFormat(Struct('Q'), Struct(">Q"), Struct("<Q")), 273 's64': ScalarFormat(Struct('q'), Struct(">q"), Struct("<q")) 274 } 275 276 def __init__(self, raw, offset): 277 self._len, self._type = struct.unpack("HH", raw[offset : offset + 4]) 278 self.type = self._type & ~Netlink.NLA_TYPE_MASK 279 self.is_nest = self._type & Netlink.NLA_F_NESTED 280 self.payload_len = self._len 281 self.full_len = (self.payload_len + 3) & ~3 282 self.raw = raw[offset + 4 : offset + self.payload_len] 283 284 @classmethod 285 def get_format(cls, attr_type, byte_order=None): 286 format_ = cls.type_formats[attr_type] 287 if byte_order: 288 return format_.big if byte_order == "big-endian" \ 289 else format_.little 290 return format_.native 291 292 def as_scalar(self, attr_type, byte_order=None): 293 format_ = self.get_format(attr_type, byte_order) 294 return format_.unpack(self.raw)[0] 295 296 def as_auto_scalar(self, attr_type, byte_order=None): 297 if len(self.raw) != 4 and len(self.raw) != 8: 298 raise YnlException(f"Auto-scalar len payload be 4 or 8 bytes, got {len(self.raw)}") 299 real_type = attr_type[0] + str(len(self.raw) * 8) 300 format_ = self.get_format(real_type, byte_order) 301 return format_.unpack(self.raw)[0] 302 303 def as_strz(self): 304 return self.raw.decode('ascii')[:-1] 305 306 def as_bin(self): 307 return self.raw 308 309 def as_c_array(self, c_type): 310 format_ = self.get_format(c_type) 311 return [ x[0] for x in format_.iter_unpack(self.raw) ] 312 313 def __repr__(self): 314 return f"[type:{self.type} len:{self._len}] {self.raw}" 315 316 317class NlAttrs: 318 def __init__(self, msg, offset=0): 319 self.attrs = [] 320 321 while offset < len(msg): 322 attr = NlAttr(msg, offset) 323 offset += attr.full_len 324 self.attrs.append(attr) 325 326 def __iter__(self): 327 yield from self.attrs 328 329 def __repr__(self): 330 msg = '' 331 for a in self.attrs: 332 if msg: 333 msg += '\n' 334 msg += repr(a) 335 return msg 336 337 338class NlMsg: 339 def __init__(self, msg, offset, attr_space=None): 340 self.hdr = msg[offset : offset + 16] 341 342 self.nl_len, self.nl_type, self.nl_flags, self.nl_seq, self.nl_portid = \ 343 struct.unpack("IHHII", self.hdr) 344 345 self.raw = msg[offset + 16 : offset + self.nl_len] 346 347 self.error = 0 348 self.done = 0 349 350 extack_off = None 351 if self.nl_type == Netlink.NLMSG_ERROR: 352 self.error = struct.unpack("i", self.raw[0:4])[0] 353 self.done = 1 354 extack_off = 20 355 elif self.nl_type == Netlink.NLMSG_DONE: 356 self.error = struct.unpack("i", self.raw[0:4])[0] 357 self.done = 1 358 extack_off = 4 359 360 self.extack = None 361 if self.nl_flags & Netlink.NLM_F_ACK_TLVS and extack_off: 362 self.extack = {} 363 extack_attrs = NlAttrs(self.raw[extack_off:]) 364 for extack in extack_attrs: 365 if extack.type == Netlink.NLMSGERR_ATTR_MSG: 366 self.extack['msg'] = extack.as_strz() 367 elif extack.type == Netlink.NLMSGERR_ATTR_MISS_TYPE: 368 self.extack['miss-type'] = extack.as_scalar('u32') 369 elif extack.type == Netlink.NLMSGERR_ATTR_MISS_NEST: 370 self.extack['miss-nest'] = extack.as_scalar('u32') 371 elif extack.type == Netlink.NLMSGERR_ATTR_OFFS: 372 self.extack['bad-attr-offs'] = extack.as_scalar('u32') 373 elif extack.type == Netlink.NLMSGERR_ATTR_POLICY: 374 self.extack['policy'] = _genl_decode_policy(extack.raw) 375 else: 376 if 'unknown' not in self.extack: 377 self.extack['unknown'] = [] 378 self.extack['unknown'].append(extack) 379 380 if attr_space: 381 self.annotate_extack(attr_space) 382 383 def annotate_extack(self, attr_space): 384 """ Make extack more human friendly with attribute information """ 385 386 # We don't have the ability to parse nests yet, so only do global 387 if 'miss-type' in self.extack and 'miss-nest' not in self.extack: 388 miss_type = self.extack['miss-type'] 389 if miss_type in attr_space.attrs_by_val: 390 spec = attr_space.attrs_by_val[miss_type] 391 self.extack['miss-type'] = spec['name'] 392 if 'doc' in spec: 393 self.extack['miss-type-doc'] = spec['doc'] 394 395 def cmd(self): 396 return self.nl_type 397 398 def __repr__(self): 399 msg = (f"nl_len = {self.nl_len} ({len(self.raw)}) " 400 f"nl_flags = 0x{self.nl_flags:x} nl_type = {self.nl_type}") 401 if self.error: 402 msg += '\n\terror: ' + str(self.error) 403 if self.extack: 404 msg += '\n\textack: ' + repr(self.extack) 405 return msg 406 407 408# pylint: disable=too-few-public-methods 409class NlMsgs: 410 def __init__(self, data): 411 self.msgs = [] 412 413 offset = 0 414 while offset < len(data): 415 msg = NlMsg(data, offset) 416 offset += msg.nl_len 417 self.msgs.append(msg) 418 419 def __iter__(self): 420 yield from self.msgs 421 422 423def _genl_msg(nl_type, nl_flags, genl_cmd, genl_version, seq=None): 424 # we prepend length in _genl_msg_finalize() 425 if seq is None: 426 seq = random.randint(1, 1024) 427 nlmsg = struct.pack("HHII", nl_type, nl_flags, seq, 0) 428 genlmsg = struct.pack("BBH", genl_cmd, genl_version, 0) 429 return nlmsg + genlmsg 430 431 432def _genl_msg_finalize(msg): 433 return struct.pack("I", len(msg) + 4) + msg 434 435 436def _genl_decode_policy(raw): 437 policy = {} 438 for attr in NlAttrs(raw): 439 if attr.type == Netlink.NL_POLICY_TYPE_ATTR_TYPE: 440 type_ = attr.as_scalar('u32') 441 policy['type'] = Netlink.AttrType(type_).name 442 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_S: 443 policy['min-value'] = attr.as_scalar('s64') 444 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_S: 445 policy['max-value'] = attr.as_scalar('s64') 446 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_U: 447 policy['min-value'] = attr.as_scalar('u64') 448 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_U: 449 policy['max-value'] = attr.as_scalar('u64') 450 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_LENGTH: 451 policy['min-length'] = attr.as_scalar('u32') 452 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_LENGTH: 453 policy['max-length'] = attr.as_scalar('u32') 454 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_POLICY_IDX: 455 policy['policy-idx'] = attr.as_scalar('u32') 456 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_BITFIELD32_MASK: 457 policy['bitfield32-mask'] = attr.as_scalar('u32') 458 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MASK: 459 policy['mask'] = attr.as_scalar('u64') 460 elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_BYTE_ORDER: 461 byte_order = attr.as_scalar('u32') 462 policy['byte-order'] = Netlink.ByteOrder(byte_order).name 463 return policy 464 465 466# pylint: disable=too-many-nested-blocks 467def _genl_load_families(): 468 genl_family_name_to_id = {} 469 470 with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, Netlink.NETLINK_GENERIC) as sock: 471 sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1) 472 473 msg = _genl_msg(Netlink.GENL_ID_CTRL, 474 Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK | Netlink.NLM_F_DUMP, 475 Netlink.CTRL_CMD_GETFAMILY, 1) 476 msg = _genl_msg_finalize(msg) 477 478 sock.send(msg, 0) 479 480 while True: 481 reply = sock.recv(128 * 1024) 482 nms = NlMsgs(reply) 483 for nl_msg in nms: 484 if nl_msg.error: 485 raise YnlException(f"Netlink error: {nl_msg.error}") 486 if nl_msg.done: 487 return genl_family_name_to_id 488 489 gm = GenlMsg(nl_msg) 490 fam = {} 491 for attr in NlAttrs(gm.raw): 492 if attr.type == Netlink.CTRL_ATTR_FAMILY_ID: 493 fam['id'] = attr.as_scalar('u16') 494 elif attr.type == Netlink.CTRL_ATTR_FAMILY_NAME: 495 fam['name'] = attr.as_strz() 496 elif attr.type == Netlink.CTRL_ATTR_MAXATTR: 497 fam['maxattr'] = attr.as_scalar('u32') 498 elif attr.type == Netlink.CTRL_ATTR_MCAST_GROUPS: 499 fam['mcast'] = {} 500 for entry in NlAttrs(attr.raw): 501 mcast_name = None 502 mcast_id = None 503 for entry_attr in NlAttrs(entry.raw): 504 if entry_attr.type == Netlink.CTRL_ATTR_MCAST_GRP_NAME: 505 mcast_name = entry_attr.as_strz() 506 elif entry_attr.type == Netlink.CTRL_ATTR_MCAST_GRP_ID: 507 mcast_id = entry_attr.as_scalar('u32') 508 if mcast_name and mcast_id is not None: 509 fam['mcast'][mcast_name] = mcast_id 510 if 'name' in fam and 'id' in fam: 511 genl_family_name_to_id[fam['name']] = fam 512 513 514# pylint: disable=too-many-nested-blocks 515def _genl_policy_dump(family_id, op): 516 op_policy = {} 517 policy_table = {} 518 519 with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, Netlink.NETLINK_GENERIC) as sock: 520 sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1) 521 522 msg = _genl_msg(Netlink.GENL_ID_CTRL, 523 Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK | Netlink.NLM_F_DUMP, 524 Netlink.CTRL_CMD_GETPOLICY, 1) 525 msg += struct.pack('HHHxx', 6, Netlink.CTRL_ATTR_FAMILY_ID, family_id) 526 msg += struct.pack('HHI', 8, Netlink.CTRL_ATTR_OP, op) 527 msg = _genl_msg_finalize(msg) 528 529 sock.send(msg, 0) 530 531 while True: 532 reply = sock.recv(128 * 1024) 533 nms = NlMsgs(reply) 534 for nl_msg in nms: 535 if nl_msg.error: 536 raise YnlException(f"Netlink error: {nl_msg.error}") 537 if nl_msg.done: 538 return op_policy, policy_table 539 540 gm = GenlMsg(nl_msg) 541 for attr in NlAttrs(gm.raw): 542 if attr.type == Netlink.CTRL_ATTR_OP_POLICY: 543 for op_attr in NlAttrs(attr.raw): 544 for method_attr in NlAttrs(op_attr.raw): 545 if method_attr.type == Netlink.CTRL_ATTR_POLICY_DO: 546 op_policy['do'] = method_attr.as_scalar('u32') 547 elif method_attr.type == Netlink.CTRL_ATTR_POLICY_DUMP: 548 op_policy['dump'] = method_attr.as_scalar('u32') 549 elif attr.type == Netlink.CTRL_ATTR_POLICY: 550 for pidx_attr in NlAttrs(attr.raw): 551 policy_idx = pidx_attr.type 552 for aid_attr in NlAttrs(pidx_attr.raw): 553 attr_id = aid_attr.type 554 decoded = _genl_decode_policy(aid_attr.raw) 555 if policy_idx not in policy_table: 556 policy_table[policy_idx] = {} 557 policy_table[policy_idx][attr_id] = decoded 558 559 560class GenlMsg: 561 def __init__(self, nl_msg): 562 self.nl = nl_msg 563 self.genl_cmd, self.genl_version, _ = struct.unpack_from("BBH", nl_msg.raw, 0) 564 self.raw = nl_msg.raw[4:] 565 self.raw_attrs = [] 566 567 def cmd(self): 568 return self.genl_cmd 569 570 def __repr__(self): 571 msg = repr(self.nl) 572 msg += f"\tgenl_cmd = {self.genl_cmd} genl_ver = {self.genl_version}\n" 573 for a in self.raw_attrs: 574 msg += '\t\t' + repr(a) + '\n' 575 return msg 576 577 578class NetlinkProtocol: 579 def __init__(self, family_name, proto_num): 580 self.family_name = family_name 581 self.proto_num = proto_num 582 583 def _message(self, nl_type, nl_flags, seq=None): 584 if seq is None: 585 seq = random.randint(1, 1024) 586 nlmsg = struct.pack("HHII", nl_type, nl_flags, seq, 0) 587 return nlmsg 588 589 def message(self, flags, command, _version, seq=None): 590 return self._message(command, flags, seq) 591 592 def _decode(self, nl_msg): 593 return nl_msg 594 595 def decode(self, ynl, nl_msg, op): 596 msg = self._decode(nl_msg) 597 if op is None: 598 op = ynl.rsp_by_value[msg.cmd()] 599 fixed_header_size = ynl.struct_size(op.fixed_header) 600 msg.raw_attrs = NlAttrs(msg.raw, fixed_header_size) 601 return msg 602 603 def get_mcast_id(self, mcast_name, mcast_groups): 604 if mcast_name not in mcast_groups: 605 raise YnlException(f'Multicast group "{mcast_name}" not present in the spec') 606 return mcast_groups[mcast_name].value 607 608 def msghdr_size(self): 609 return 16 610 611 612class GenlProtocol(NetlinkProtocol): 613 genl_family_name_to_id = {} 614 615 def __init__(self, family_name): 616 super().__init__(family_name, Netlink.NETLINK_GENERIC) 617 618 if not GenlProtocol.genl_family_name_to_id: 619 GenlProtocol.genl_family_name_to_id = _genl_load_families() 620 621 self.genl_family = GenlProtocol.genl_family_name_to_id[family_name] 622 self.family_id = GenlProtocol.genl_family_name_to_id[family_name]['id'] 623 624 def message(self, flags, command, version, seq=None): 625 nlmsg = self._message(self.family_id, flags, seq) 626 genlmsg = struct.pack("BBH", command, version, 0) 627 return nlmsg + genlmsg 628 629 def _decode(self, nl_msg): 630 return GenlMsg(nl_msg) 631 632 def get_mcast_id(self, mcast_name, mcast_groups): 633 if mcast_name not in self.genl_family['mcast']: 634 raise YnlException(f'Multicast group "{mcast_name}" not present in the family') 635 return self.genl_family['mcast'][mcast_name] 636 637 def msghdr_size(self): 638 return super().msghdr_size() + 4 639 640 641# pylint: disable=too-few-public-methods 642class SpaceAttrs: 643 SpecValuesPair = namedtuple('SpecValuesPair', ['spec', 'values']) 644 645 def __init__(self, attr_space, attrs, outer = None): 646 outer_scopes = outer.scopes if outer else [] 647 inner_scope = self.SpecValuesPair(attr_space, attrs) 648 self.scopes = [inner_scope] + outer_scopes 649 650 def lookup(self, name): 651 for scope in self.scopes: 652 if name in scope.spec: 653 if name in scope.values: 654 return scope.values[name] 655 spec_name = scope.spec.yaml['name'] 656 raise YnlException( 657 f"No value for '{name}' in attribute space '{spec_name}'") 658 raise YnlException(f"Attribute '{name}' not defined in any attribute-set") 659 660 661# 662# YNL implementation details. 663# 664 665 666class YnlFamily(SpecFamily): 667 """ 668 YNL family -- a Netlink interface built from a YAML spec. 669 670 The spec can be selected either by file path (def_path=) or, when it 671 ships in a well-known location, by family name (family="xyz"); exactly 672 one of the two must be given. For example: 673 674 from pyynl import YnlFamily 675 676 ynl = YnlFamily(family="netdev") 677 678 Primary use of the class is to execute Netlink commands: 679 680 ynl.<op_name>(attrs, ...) 681 682 By default this will execute the <op_name> as "do", pass dump=True 683 to perform a dump operation. 684 685 ynl.<op_name> is a shorthand / convenience wrapper for the following 686 methods which take the op_name as a string: 687 688 ynl.do(op_name, attrs, flags=None) -- execute a do operation 689 ynl.dump(op_name, attrs) -- execute a dump operation 690 ynl.do_multi(ops) -- batch multiple do operations 691 692 The flags argument in ynl.do() allows passing in extra NLM_F_* flags 693 which may be necessary for old families. 694 695 Notification API: 696 697 ynl.ntf_subscribe(mcast_name) -- join a multicast group 698 ynl.ntf_listen_all_nsid() -- listen on all netns 699 ynl.ntf_bind(addr=(0, 0)) -- bind socket for unicast notifications 700 ynl.check_ntf() -- drain pending notifications 701 ynl.poll_ntf(duration=None) -- yield notifications 702 703 Policy introspection allows querying validation criteria from the running 704 kernel. Allows checking whether kernel supports a given attribute or value. 705 706 ynl.get_policy(op_name, mode) -- query kernel policy for an op 707 """ 708 def __init__(self, def_path=None, schema=None, process_unknown=None, 709 recv_size=0, family=None): 710 super().__init__(def_path, schema, family=family) 711 712 self.include_raw = False 713 # Specs from /usr (selected by family=) have a higher chance of being 714 # stale, default to ignoring unknown attrs. In-tree users, and users 715 # who bundle the spec need to make a conscious decision. 716 if process_unknown is None: 717 process_unknown = family is not None 718 self.process_unknown = process_unknown 719 720 try: 721 if self.proto == "netlink-raw": 722 self.nlproto = NetlinkProtocol(self.yaml['name'], 723 self.yaml['protonum']) 724 else: 725 self.nlproto = GenlProtocol(self.yaml['name']) 726 except KeyError as err: 727 raise YnlException(f"Family '{self.yaml['name']}' not supported by the kernel") from err 728 729 self._recv_dbg = False 730 # Note that netlink will use conservative (min) message size for 731 # the first dump recv() on the socket, our setting will only matter 732 # from the second recv() on. 733 self._recv_size = recv_size if recv_size else 131072 734 # Netlink will always allocate at least PAGE_SIZE - sizeof(skb_shinfo) 735 # for a message, so smaller receive sizes will lead to truncation. 736 # Note that the min size for other families may be larger than 4k! 737 if self._recv_size < 4000: 738 raise ConfigError() 739 740 self.sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, self.nlproto.proto_num) 741 self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1) 742 self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_EXT_ACK, 1) 743 self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_GET_STRICT_CHK, 1) 744 745 self.async_msg_ids = set() 746 self.async_msg_queue = queue.Queue() 747 748 for msg in self.msgs.values(): 749 if msg.is_async: 750 self.async_msg_ids.add(msg.rsp_value) 751 752 for op_name, op in self.ops.items(): 753 bound_f = functools.partial(self._op, op_name) 754 setattr(self, op.ident_name, bound_f) 755 756 def close(self): 757 if self.sock is not None: 758 self.sock.close() 759 self.sock = None 760 761 def __enter__(self): 762 return self 763 764 def __exit__(self, exc_type, exc, tb): 765 self.close() 766 767 def ntf_subscribe(self, mcast_name): 768 mcast_id = self.nlproto.get_mcast_id(mcast_name, self.mcast_groups) 769 self.sock.bind((0, 0)) 770 self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_ADD_MEMBERSHIP, 771 mcast_id) 772 773 def ntf_listen_all_nsid(self): 774 """Enable NETLINK_LISTEN_ALL_NSID to receive notifications from all 775 namespaces that have an nsid mapped in the current one.""" 776 self.sock.setsockopt(Netlink.SOL_NETLINK, 777 Netlink.NETLINK_LISTEN_ALL_NSID, 1) 778 779 @staticmethod 780 def _decode_nsid(ancdata): 781 for cmsg_level, cmsg_type, cmsg_data in ancdata: 782 if (cmsg_level == Netlink.SOL_NETLINK and 783 cmsg_type == Netlink.NETLINK_LISTEN_ALL_NSID): 784 nsid = struct.unpack('i', cmsg_data)[0] 785 if nsid >= 0: 786 return nsid 787 return None 788 return None 789 790 def ntf_bind(self, addr=(0, 0)): 791 """Bind socket for receiving unicast notifications.""" 792 self.sock.bind(addr) 793 794 def set_recv_dbg(self, enabled): 795 self._recv_dbg = enabled 796 797 def _recv_dbg_print(self, reply, nl_msgs): 798 if not self._recv_dbg: 799 return 800 print("Recv: read", len(reply), "bytes,", 801 len(nl_msgs.msgs), "messages", file=sys.stderr) 802 for nl_msg in nl_msgs: 803 print(" ", nl_msg, file=sys.stderr) 804 805 def _encode_enum(self, attr_spec, value): 806 enum = self.consts[attr_spec['enum']] 807 if enum.type == 'flags' or attr_spec.get('enum-as-flags', False): 808 scalar = 0 809 if isinstance(value, str): 810 value = [value] 811 for single_value in value: 812 scalar += enum.entries[single_value].user_value(as_flags = True) 813 return scalar 814 return enum.entries[value].user_value() 815 816 def _get_scalar(self, attr_spec, value): 817 try: 818 return int(value) 819 except (ValueError, TypeError) as e: 820 if 'enum' in attr_spec: 821 return self._encode_enum(attr_spec, value) 822 if attr_spec.display_hint: 823 return self._from_string(value, attr_spec) 824 raise e 825 826 # pylint: disable=too-many-statements 827 def _add_attr(self, space, name, value, search_attrs): 828 try: 829 attr = self.attr_sets[space][name] 830 except KeyError as err: 831 raise YnlException(f"Space '{space}' has no attribute '{name}'") from err 832 nl_type = attr.value 833 834 if attr.is_multi and isinstance(value, list): 835 attr_payload = b'' 836 for subvalue in value: 837 attr_payload += self._add_attr(space, name, subvalue, search_attrs) 838 return attr_payload 839 840 if attr["type"] == 'nest': 841 nl_type |= Netlink.NLA_F_NESTED 842 sub_space = attr['nested-attributes'] 843 attr_payload = self._add_nest_attrs(value, sub_space, search_attrs) 844 elif attr['type'] == 'indexed-array' and attr['sub-type'] == 'nest': 845 nl_type |= Netlink.NLA_F_NESTED 846 sub_space = attr['nested-attributes'] 847 attr_payload = self._encode_indexed_array(value, sub_space, 848 search_attrs) 849 elif attr["type"] == 'flag': 850 if not value: 851 # If value is absent or false then skip attribute creation. 852 return b'' 853 attr_payload = b'' 854 elif attr["type"] == 'string': 855 attr_payload = str(value).encode('ascii') + b'\x00' 856 elif attr["type"] == 'binary': 857 if value is None: 858 attr_payload = b'' 859 elif isinstance(value, bytes): 860 attr_payload = value 861 elif isinstance(value, str): 862 if attr.display_hint: 863 attr_payload = self._from_string(value, attr) 864 else: 865 attr_payload = bytes.fromhex(value) 866 elif isinstance(value, dict) and attr.struct_name: 867 attr_payload = self._encode_struct(attr.struct_name, value) 868 elif isinstance(value, list) and attr.sub_type in NlAttr.type_formats: 869 format_ = NlAttr.get_format(attr.sub_type) 870 attr_payload = b''.join([format_.pack(x) for x in value]) 871 else: 872 raise YnlException(f'Unknown type for binary attribute, value: {value}') 873 elif attr['type'] in NlAttr.type_formats or attr.is_auto_scalar: 874 scalar = self._get_scalar(attr, value) 875 if attr.is_auto_scalar: 876 attr_type = attr["type"][0] + ('32' if scalar.bit_length() <= 32 else '64') 877 else: 878 attr_type = attr["type"] 879 format_ = NlAttr.get_format(attr_type, attr.byte_order) 880 attr_payload = format_.pack(scalar) 881 elif attr['type'] in "bitfield32": 882 scalar_value = self._get_scalar(attr, value["value"]) 883 scalar_selector = self._get_scalar(attr, value["selector"]) 884 attr_payload = struct.pack("II", scalar_value, scalar_selector) 885 elif attr['type'] == 'sub-message': 886 msg_format, _ = self._resolve_selector(attr, search_attrs) 887 attr_payload = b'' 888 if msg_format.fixed_header: 889 attr_payload += self._encode_struct(msg_format.fixed_header, value) 890 if msg_format.attr_set: 891 if msg_format.attr_set in self.attr_sets: 892 nl_type |= Netlink.NLA_F_NESTED 893 sub_attrs = SpaceAttrs(msg_format.attr_set, value, search_attrs) 894 for subname, subvalue in value.items(): 895 attr_payload += self._add_attr(msg_format.attr_set, 896 subname, subvalue, sub_attrs) 897 else: 898 raise YnlException(f"Unknown attribute-set '{msg_format.attr_set}'") 899 else: 900 raise YnlException(f'Unknown type at {space} {name} {value} {attr["type"]}') 901 902 return self._add_attr_raw(nl_type, attr_payload) 903 904 def _add_attr_raw(self, nl_type, attr_payload): 905 pad = b'\x00' * ((4 - len(attr_payload) % 4) % 4) 906 return struct.pack('HH', len(attr_payload) + 4, nl_type) + attr_payload + pad 907 908 def _add_nest_attrs(self, value, sub_space, search_attrs): 909 sub_attrs = SpaceAttrs(self.attr_sets[sub_space], value, search_attrs) 910 attr_payload = b'' 911 for subname, subvalue in value.items(): 912 attr_payload += self._add_attr(sub_space, subname, subvalue, 913 sub_attrs) 914 return attr_payload 915 916 def _encode_indexed_array(self, vals, sub_space, search_attrs): 917 attr_payload = b'' 918 for i, val in enumerate(vals): 919 idx = i | Netlink.NLA_F_NESTED 920 val_payload = self._add_nest_attrs(val, sub_space, search_attrs) 921 attr_payload += self._add_attr_raw(idx, val_payload) 922 return attr_payload 923 924 def _get_enum_or_unknown(self, enum, raw): 925 try: 926 name = enum.entries_by_val[raw].name 927 except KeyError as error: 928 if self.process_unknown: 929 name = f"Unknown({raw})" 930 else: 931 raise error 932 return name 933 934 def _decode_enum(self, raw, attr_spec): 935 enum = self.consts[attr_spec['enum']] 936 if enum.type == 'flags' or attr_spec.get('enum-as-flags', False): 937 i = 0 938 value = set() 939 while raw: 940 if raw & 1: 941 value.add(self._get_enum_or_unknown(enum, i)) 942 raw >>= 1 943 i += 1 944 else: 945 value = self._get_enum_or_unknown(enum, raw) 946 return value 947 948 def _decode_binary(self, attr, attr_spec): 949 if attr_spec.struct_name: 950 decoded = self._decode_struct(attr.raw, attr_spec.struct_name) 951 elif attr_spec.sub_type: 952 decoded = attr.as_c_array(attr_spec.sub_type) 953 if 'enum' in attr_spec: 954 decoded = [ self._decode_enum(x, attr_spec) for x in decoded ] 955 elif attr_spec.display_hint: 956 decoded = [ self._formatted_string(x, attr_spec.display_hint) 957 for x in decoded ] 958 else: 959 decoded = attr.as_bin() 960 if attr_spec.display_hint: 961 decoded = self._formatted_string(decoded, attr_spec.display_hint) 962 return decoded 963 964 def _decode_array_attr(self, attr, attr_spec): 965 decoded = [] 966 offset = 0 967 while offset < len(attr.raw): 968 item = NlAttr(attr.raw, offset) 969 offset += item.full_len 970 971 if attr_spec["sub-type"] == 'nest': 972 subattrs = self._decode(NlAttrs(item.raw), attr_spec['nested-attributes']) 973 decoded.append({ item.type: subattrs }) 974 elif attr_spec["sub-type"] == 'binary': 975 subattr = item.as_bin() 976 if attr_spec.display_hint: 977 subattr = self._formatted_string(subattr, attr_spec.display_hint) 978 decoded.append(subattr) 979 elif attr_spec["sub-type"] in NlAttr.type_formats: 980 subattr = item.as_scalar(attr_spec['sub-type'], attr_spec.byte_order) 981 if 'enum' in attr_spec: 982 subattr = self._decode_enum(subattr, attr_spec) 983 elif attr_spec.display_hint: 984 subattr = self._formatted_string(subattr, attr_spec.display_hint) 985 decoded.append(subattr) 986 else: 987 raise YnlException(f'Unknown {attr_spec["sub-type"]} with name {attr_spec["name"]}') 988 return decoded 989 990 def _decode_nest_type_value(self, attr, attr_spec): 991 decoded = {} 992 value = attr 993 for name in attr_spec['type-value']: 994 value = NlAttr(value.raw, 0) 995 decoded[name] = value.type 996 subattrs = self._decode(NlAttrs(value.raw), attr_spec['nested-attributes']) 997 decoded.update(subattrs) 998 return decoded 999 1000 def _decode_unknown(self, attr): 1001 if attr.is_nest: 1002 return self._decode(NlAttrs(attr.raw), None) 1003 return attr.as_bin() 1004 1005 def _rsp_add(self, rsp, name, is_multi, decoded): 1006 if is_multi is None: 1007 if name in rsp and not isinstance(rsp[name], list): 1008 rsp[name] = [rsp[name]] 1009 is_multi = True 1010 else: 1011 is_multi = False 1012 1013 if not is_multi: 1014 rsp[name] = decoded 1015 elif name in rsp: 1016 rsp[name].append(decoded) 1017 else: 1018 rsp[name] = [decoded] 1019 1020 def _resolve_selector(self, attr_spec, search_attrs): 1021 sub_msg = attr_spec.sub_message 1022 if sub_msg not in self.sub_msgs: 1023 raise YnlException(f"No sub-message spec named {sub_msg} for {attr_spec.name}") 1024 sub_msg_spec = self.sub_msgs[sub_msg] 1025 1026 selector = attr_spec.selector 1027 value = search_attrs.lookup(selector) 1028 if value not in sub_msg_spec.formats: 1029 raise YnlException(f"No message format for '{value}' in sub-message spec '{sub_msg}'") 1030 1031 spec = sub_msg_spec.formats[value] 1032 return spec, value 1033 1034 def _decode_sub_msg(self, attr, attr_spec, search_attrs): 1035 msg_format, _ = self._resolve_selector(attr_spec, search_attrs) 1036 decoded = {} 1037 offset = 0 1038 if msg_format.fixed_header: 1039 decoded.update(self._decode_struct(attr.raw, msg_format.fixed_header)) 1040 offset = self.struct_size(msg_format.fixed_header) 1041 if msg_format.attr_set: 1042 if msg_format.attr_set in self.attr_sets: 1043 subdict = self._decode(NlAttrs(attr.raw, offset), msg_format.attr_set) 1044 decoded.update(subdict) 1045 else: 1046 raise YnlException(f"Unknown attribute-set '{msg_format.attr_set}' " 1047 f"when decoding '{attr_spec.name}'") 1048 return decoded 1049 1050 # pylint: disable=too-many-statements 1051 def _decode(self, attrs, space, outer_attrs = None): 1052 rsp = {} 1053 search_attrs = {} 1054 if space: 1055 attr_space = self.attr_sets[space] 1056 search_attrs = SpaceAttrs(attr_space, rsp, outer_attrs) 1057 1058 for attr in attrs: 1059 try: 1060 attr_spec = attr_space.attrs_by_val[attr.type] 1061 except (KeyError, UnboundLocalError) as err: 1062 if not self.process_unknown: 1063 raise YnlException(f"Space '{space}' has no attribute " 1064 f"with value '{attr.type}'") from err 1065 attr_name = f"UnknownAttr({attr.type})" 1066 self._rsp_add(rsp, attr_name, None, self._decode_unknown(attr)) 1067 continue 1068 1069 try: 1070 if attr_spec["type"] == 'pad': 1071 continue 1072 elif attr_spec["type"] == 'nest': 1073 subdict = self._decode(NlAttrs(attr.raw), 1074 attr_spec['nested-attributes'], 1075 search_attrs) 1076 decoded = subdict 1077 elif attr_spec["type"] == 'string': 1078 decoded = attr.as_strz() 1079 elif attr_spec["type"] == 'binary': 1080 decoded = self._decode_binary(attr, attr_spec) 1081 elif attr_spec["type"] == 'flag': 1082 decoded = True 1083 elif attr_spec.is_auto_scalar: 1084 decoded = attr.as_auto_scalar(attr_spec['type'], attr_spec.byte_order) 1085 if 'enum' in attr_spec: 1086 decoded = self._decode_enum(decoded, attr_spec) 1087 elif attr_spec["type"] in NlAttr.type_formats: 1088 decoded = attr.as_scalar(attr_spec['type'], attr_spec.byte_order) 1089 if 'enum' in attr_spec: 1090 decoded = self._decode_enum(decoded, attr_spec) 1091 elif attr_spec.display_hint: 1092 decoded = self._formatted_string(decoded, attr_spec.display_hint) 1093 elif attr_spec["type"] == 'indexed-array': 1094 decoded = self._decode_array_attr(attr, attr_spec) 1095 elif attr_spec["type"] == 'bitfield32': 1096 value, selector = struct.unpack("II", attr.raw) 1097 if 'enum' in attr_spec: 1098 value = self._decode_enum(value, attr_spec) 1099 selector = self._decode_enum(selector, attr_spec) 1100 decoded = {"value": value, "selector": selector} 1101 elif attr_spec["type"] == 'sub-message': 1102 decoded = self._decode_sub_msg(attr, attr_spec, search_attrs) 1103 elif attr_spec["type"] == 'nest-type-value': 1104 decoded = self._decode_nest_type_value(attr, attr_spec) 1105 else: 1106 if not self.process_unknown: 1107 raise YnlException(f'Unknown {attr_spec["type"]} ' 1108 f'with name {attr_spec["name"]}') 1109 decoded = self._decode_unknown(attr) 1110 1111 self._rsp_add(rsp, attr_spec["name"], attr_spec.is_multi, decoded) 1112 except: 1113 print(f"Error decoding '{attr_spec.name}' from '{space}'") 1114 raise 1115 1116 return rsp 1117 1118 # pylint: disable=too-many-arguments, too-many-positional-arguments 1119 def _decode_extack_path(self, attrs, attr_set, offset, target, search_attrs): 1120 for attr in attrs: 1121 try: 1122 attr_spec = attr_set.attrs_by_val[attr.type] 1123 except KeyError as err: 1124 raise YnlException( 1125 f"Space '{attr_set.name}' has no attribute with value '{attr.type}'") from err 1126 if offset > target: 1127 break 1128 if offset == target: 1129 return '.' + attr_spec.name 1130 1131 if offset + attr.full_len <= target: 1132 offset += attr.full_len 1133 continue 1134 1135 pathname = attr_spec.name 1136 if attr_spec['type'] == 'nest': 1137 sub_attrs = self.attr_sets[attr_spec['nested-attributes']] 1138 search_attrs = SpaceAttrs(sub_attrs, search_attrs.lookup(attr_spec['name'])) 1139 elif attr_spec['type'] == 'sub-message': 1140 msg_format, value = self._resolve_selector(attr_spec, search_attrs) 1141 if msg_format is None: 1142 raise YnlException(f"Can't resolve sub-message of " 1143 f"{attr_spec['name']} for extack") 1144 sub_attrs = self.attr_sets[msg_format.attr_set] 1145 pathname += f"({value})" 1146 else: 1147 raise YnlException(f"Can't dive into {attr.type} ({attr_spec['name']}) for extack") 1148 offset += 4 1149 subpath = self._decode_extack_path(NlAttrs(attr.raw), sub_attrs, 1150 offset, target, search_attrs) 1151 if subpath is None: 1152 return None 1153 return '.' + pathname + subpath 1154 1155 return None 1156 1157 def _decode_extack(self, request, op, extack, vals): 1158 if 'bad-attr-offs' not in extack: 1159 return 1160 1161 msg = self.nlproto.decode(self, NlMsg(request, 0, op.attr_set), op) 1162 offset = self.nlproto.msghdr_size() + self.struct_size(op.fixed_header) 1163 search_attrs = SpaceAttrs(op.attr_set, vals) 1164 path = self._decode_extack_path(msg.raw_attrs, op.attr_set, offset, 1165 extack['bad-attr-offs'], search_attrs) 1166 if path: 1167 del extack['bad-attr-offs'] 1168 extack['bad-attr'] = path 1169 1170 def struct_size(self, name): 1171 if name: 1172 members = self.consts[name].members 1173 size = 0 1174 for m in members: 1175 if m.type in ['pad', 'binary']: 1176 if m.struct: 1177 size += self.struct_size(m.struct) 1178 else: 1179 size += m.len 1180 else: 1181 format_ = NlAttr.get_format(m.type, m.byte_order) 1182 size += format_.size 1183 return size 1184 return 0 1185 1186 def _decode_struct(self, data, name): 1187 members = self.consts[name].members 1188 attrs = {} 1189 offset = 0 1190 for m in members: 1191 value = None 1192 if m.type == 'pad': 1193 offset += m.len 1194 elif m.type == 'binary': 1195 if m.struct: 1196 len_ = self.struct_size(m.struct) 1197 value = self._decode_struct(data[offset : offset + len_], 1198 m.struct) 1199 offset += len_ 1200 else: 1201 value = data[offset : offset + m.len] 1202 offset += m.len 1203 else: 1204 format_ = NlAttr.get_format(m.type, m.byte_order) 1205 [ value ] = format_.unpack_from(data, offset) 1206 offset += format_.size 1207 if value is not None: 1208 if m.enum: 1209 value = self._decode_enum(value, m) 1210 elif m.display_hint: 1211 value = self._formatted_string(value, m.display_hint) 1212 attrs[m.name] = value 1213 return attrs 1214 1215 def _encode_struct(self, name, vals): 1216 members = self.consts[name].members 1217 attr_payload = b'' 1218 for m in members: 1219 value = vals.pop(m.name) if m.name in vals else None 1220 if m.type == 'pad': 1221 attr_payload += bytearray(m.len) 1222 elif m.type == 'binary': 1223 if m.struct: 1224 if value is None: 1225 value = {} 1226 attr_payload += self._encode_struct(m.struct, value) 1227 else: 1228 if value is None: 1229 attr_payload += bytearray(m.len) 1230 else: 1231 attr_payload += bytes.fromhex(value) 1232 else: 1233 if value is None: 1234 value = 0 1235 format_ = NlAttr.get_format(m.type, m.byte_order) 1236 attr_payload += format_.pack(value) 1237 return attr_payload 1238 1239 def _formatted_string(self, raw, display_hint): 1240 if display_hint == 'mac': 1241 formatted = ':'.join(f'{b:02x}' for b in raw) 1242 elif display_hint == 'hex': 1243 if isinstance(raw, int): 1244 formatted = hex(raw) 1245 else: 1246 formatted = bytes.hex(raw, ' ') 1247 elif display_hint in [ 'ipv4', 'ipv6', 'ipv4-or-v6' ]: 1248 formatted = format(ipaddress.ip_address(raw)) 1249 elif display_hint == 'uuid': 1250 formatted = str(uuid.UUID(bytes=raw)) 1251 else: 1252 formatted = raw 1253 return formatted 1254 1255 def _from_string(self, string, attr_spec): 1256 if attr_spec.display_hint in ['ipv4', 'ipv6', 'ipv4-or-v6']: 1257 ip = ipaddress.ip_address(string) 1258 if attr_spec['type'] == 'binary': 1259 raw = ip.packed 1260 else: 1261 raw = int(ip) 1262 elif attr_spec.display_hint == 'hex': 1263 if attr_spec['type'] == 'binary': 1264 raw = bytes.fromhex(string) 1265 else: 1266 raw = int(string, 16) 1267 elif attr_spec.display_hint == 'mac': 1268 # Parse MAC address in format "00:11:22:33:44:55" or "001122334455" 1269 if ':' in string: 1270 mac_bytes = [int(x, 16) for x in string.split(':')] 1271 else: 1272 if len(string) % 2 != 0: 1273 raise YnlException(f"Invalid MAC address format: {string}") 1274 mac_bytes = [int(string[i:i+2], 16) for i in range(0, len(string), 2)] 1275 raw = bytes(mac_bytes) 1276 else: 1277 raise YnlException(f"Display hint '{attr_spec.display_hint}' not implemented" 1278 f" when parsing '{attr_spec['name']}'") 1279 return raw 1280 1281 def handle_ntf(self, decoded, nsid=None): 1282 msg = {} 1283 if self.include_raw: 1284 msg['raw'] = decoded 1285 op = self.rsp_by_value[decoded.cmd()] 1286 attrs = self._decode(decoded.raw_attrs, op.attr_set.name) 1287 if op.fixed_header: 1288 attrs.update(self._decode_struct(decoded.raw, op.fixed_header)) 1289 1290 msg['name'] = op['name'] 1291 msg['msg'] = attrs 1292 if nsid is not None: 1293 msg['nsid'] = nsid 1294 self.async_msg_queue.put(msg) 1295 1296 def _recvmsg(self, flags=0): 1297 reply, ancdata, _, _ = self.sock.recvmsg(self._recv_size, 4096, flags) 1298 return reply, ancdata 1299 1300 def check_ntf(self): 1301 while True: 1302 try: 1303 reply, ancdata = self._recvmsg(socket.MSG_DONTWAIT) 1304 except BlockingIOError: 1305 return 1306 1307 nsid = self._decode_nsid(ancdata) 1308 nms = NlMsgs(reply) 1309 self._recv_dbg_print(reply, nms) 1310 for nl_msg in nms: 1311 if nl_msg.error: 1312 print("Netlink error in ntf!?", os.strerror(-nl_msg.error)) 1313 print(nl_msg) 1314 continue 1315 if nl_msg.done: 1316 print("Netlink done while checking for ntf!?") 1317 continue 1318 1319 decoded = self.nlproto.decode(self, nl_msg, None) 1320 if decoded.cmd() not in self.async_msg_ids: 1321 print("Unexpected msg id while checking for ntf", decoded) 1322 continue 1323 1324 self.handle_ntf(decoded, nsid) 1325 1326 def poll_ntf(self, duration=None): 1327 start_time = time.time() 1328 selector = selectors.DefaultSelector() 1329 selector.register(self.sock, selectors.EVENT_READ) 1330 1331 while True: 1332 try: 1333 yield self.async_msg_queue.get_nowait() 1334 except queue.Empty: 1335 if duration is not None: 1336 timeout = start_time + duration - time.time() 1337 if timeout <= 0: 1338 return 1339 else: 1340 timeout = None 1341 events = selector.select(timeout) 1342 if events: 1343 self.check_ntf() 1344 1345 def operation_do_attributes(self, name): 1346 """ 1347 For a given operation name, find and return a supported 1348 set of attributes (as a dict). 1349 """ 1350 op = self.find_operation(name) 1351 if not op: 1352 return None 1353 1354 return op['do']['request']['attributes'].copy() 1355 1356 def _encode_message(self, op, vals, flags, req_seq): 1357 nl_flags = Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK 1358 for flag in flags or []: 1359 nl_flags |= flag 1360 1361 msg = self.nlproto.message(nl_flags, op.req_value, 1, req_seq) 1362 if op.fixed_header: 1363 msg += self._encode_struct(op.fixed_header, vals) 1364 search_attrs = SpaceAttrs(op.attr_set, vals) 1365 for name, value in vals.items(): 1366 msg += self._add_attr(op.attr_set.name, name, value, search_attrs) 1367 msg = _genl_msg_finalize(msg) 1368 return msg 1369 1370 # pylint: disable=too-many-statements 1371 def _ops(self, ops): 1372 reqs_by_seq = {} 1373 req_seq = random.randint(1024, 65535) 1374 payload = b'' 1375 for (method, vals, flags) in ops: 1376 op = self.ops[method] 1377 msg = self._encode_message(op, vals, flags, req_seq) 1378 reqs_by_seq[req_seq] = (op, vals, msg, flags) 1379 payload += msg 1380 req_seq += 1 1381 1382 self.sock.send(payload, 0) 1383 1384 done = False 1385 rsp = [] 1386 op_rsp = [] 1387 while not done: 1388 reply, ancdata = self._recvmsg() 1389 nsid = self._decode_nsid(ancdata) 1390 nms = NlMsgs(reply) 1391 self._recv_dbg_print(reply, nms) 1392 for nl_msg in nms: 1393 if nl_msg.nl_seq in reqs_by_seq: 1394 (op, vals, req_msg, req_flags) = reqs_by_seq[nl_msg.nl_seq] 1395 if nl_msg.extack: 1396 nl_msg.annotate_extack(op.attr_set) 1397 self._decode_extack(req_msg, op, nl_msg.extack, vals) 1398 else: 1399 op = None 1400 req_flags = [] 1401 1402 if nl_msg.error: 1403 raise NlError(nl_msg) 1404 if nl_msg.done: 1405 if nl_msg.extack: 1406 print("Netlink warning:") 1407 print(nl_msg) 1408 1409 if Netlink.NLM_F_DUMP in req_flags: 1410 rsp.append(op_rsp) 1411 elif not op_rsp: 1412 rsp.append(None) 1413 elif len(op_rsp) == 1: 1414 rsp.append(op_rsp[0]) 1415 else: 1416 rsp.append(op_rsp) 1417 op_rsp = [] 1418 1419 del reqs_by_seq[nl_msg.nl_seq] 1420 done = len(reqs_by_seq) == 0 1421 break 1422 1423 decoded = self.nlproto.decode(self, nl_msg, op) 1424 1425 # Check if this is a reply to our request 1426 if nl_msg.nl_seq not in reqs_by_seq or decoded.cmd() != op.rsp_value: 1427 if decoded.cmd() in self.async_msg_ids: 1428 self.handle_ntf(decoded, nsid) 1429 continue 1430 print('Unexpected message: ' + repr(decoded)) 1431 continue 1432 1433 rsp_msg = self._decode(decoded.raw_attrs, op.attr_set.name) 1434 if op.fixed_header: 1435 rsp_msg.update(self._decode_struct(decoded.raw, op.fixed_header)) 1436 op_rsp.append(rsp_msg) 1437 1438 return rsp 1439 1440 def _op(self, method, vals, flags=None, dump=False): 1441 req_flags = flags or [] 1442 if dump: 1443 req_flags.append(Netlink.NLM_F_DUMP) 1444 1445 ops = [(method, vals, req_flags)] 1446 return self._ops(ops)[0] 1447 1448 def do(self, method, vals, flags=None): 1449 return self._op(method, vals, flags) 1450 1451 def dump(self, method, vals): 1452 return self._op(method, vals, dump=True) 1453 1454 def do_multi(self, ops): 1455 return self._ops(ops) 1456 1457 def get_policy(self, op_name, mode): 1458 """Query running kernel for the Netlink policy of an operation. 1459 1460 Allows checking whether kernel supports a given attribute or value. 1461 This method consults the running kernel, not the YAML spec. 1462 1463 Args: 1464 op_name: operation name as it appears in the YAML spec 1465 mode: 'do' or 'dump' 1466 1467 Returns: 1468 NlPolicy acting as a read-only dict mapping attribute names 1469 to their policy properties (type, min/max, nested, etc.), 1470 or None if the operation has no policy for the given mode. 1471 Empty policy usually implies that the operation rejects 1472 all attributes. 1473 """ 1474 op = self.ops[op_name] 1475 op_policy, policy_table = _genl_policy_dump(self.nlproto.family_id, 1476 op.req_value) 1477 if mode not in op_policy: 1478 return None 1479 policy_idx = op_policy[mode] 1480 return NlPolicy(self, policy_idx, policy_table, op.attr_set) 1481