diff options
Diffstat (limited to '')
-rw-r--r-- | staslib/iputil.py | 305 |
1 files changed, 222 insertions, 83 deletions
diff --git a/staslib/iputil.py b/staslib/iputil.py index 9199a49..96c5d56 100644 --- a/staslib/iputil.py +++ b/staslib/iputil.py @@ -8,45 +8,126 @@ '''A collection of IP address and network interface utilities''' +import struct import socket -import logging import ipaddress -from staslib import conf +RTM_BASE = 16 +RTM_GETLINK = 18 RTM_NEWADDR = 20 RTM_GETADDR = 22 NLM_F_REQUEST = 0x01 NLM_F_ROOT = 0x100 NLMSG_DONE = 3 -IFLA_ADDRESS = 1 -NLMSGHDR_SZ = 16 +NLMSG_HDRLEN = 16 IFADDRMSG_SZ = 8 +IFINFOMSG_SZ = 16 +ARPHRD_ETHER = 1 +ARPHRD_LOOPBACK = 772 +NLMSG_LENGTH = lambda msg_len: msg_len + NLMSG_HDRLEN # pylint: disable=unnecessary-lambda-assignment + RTATTR_SZ = 4 +RTA_ALIGN = lambda length: ((length + 3) & ~3) # pylint: disable=unnecessary-lambda-assignment +IFLA_ADDRESS = 1 +IFLA_IFNAME = 3 + + +def _nlmsghdr(nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid, msg_len: int): + '''Implement this C struct: + struct nlmsghdr { + __u32 nlmsg_len; /* Length of message including header */ + __u16 nlmsg_type; /* Message content */ + __u16 nlmsg_flags; /* Additional flags */ + __u32 nlmsg_seq; /* Sequence number */ + __u32 nlmsg_pid; /* Sending process port ID */ + }; + ''' + return struct.pack('<LHHLL', NLMSG_LENGTH(msg_len), nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid) + + +def _ifaddrmsg(family=0, prefixlen=0, flags=0, scope=0, index=0): + '''Implement this C struct: + struct ifaddrmsg { + __u8 ifa_family; + __u8 ifa_prefixlen; /* The prefix length */ + __u8 ifa_flags; /* Flags */ + __u8 ifa_scope; /* Address scope */ + __u32 ifa_index; /* Link index */ + }; + ''' + return struct.pack('<BBBBL', family, prefixlen, flags, scope, index) + + +def _ifinfomsg(family=0, dev_type=0, index=0, flags=0, change=0): + '''Implement this C struct: + struct ifinfomsg { + unsigned char ifi_family; /* AF_UNSPEC */ + unsigned char __ifi_pad; + unsigned short ifi_type; /* Device type: ARPHRD_* */ + int ifi_index; /* Interface index */ + unsigned int ifi_flags; /* Device flags: IFF_* */ + unsigned int ifi_change; /* change mask: IFF_* */ + }; + ''' + return struct.pack('<BBHiII', family, 0, dev_type, index, flags, change) + + +def _nlmsg(nlmsg_type, nlmsg_flags, msg: bytes): + '''Build a Netlink message''' + return _nlmsghdr(nlmsg_type, nlmsg_flags, 0, 0, len(msg)) + msg + + +# Netlink request (Get address command) +GETADDRCMD = _nlmsg(RTM_GETADDR, NLM_F_REQUEST | NLM_F_ROOT, _ifaddrmsg()) # Netlink request (Get address command) -GETADDRCMD = ( - # BEGIN: struct nlmsghdr - b'\0' * 4 # nlmsg_len (placeholder - actual length calculated below) - + (RTM_GETADDR).to_bytes(2, byteorder='little', signed=False) # nlmsg_type - + (NLM_F_REQUEST | NLM_F_ROOT).to_bytes(2, byteorder='little', signed=False) # nlmsg_flags - + b'\0' * 2 # nlmsg_seq - + b'\0' * 2 # nlmsg_pid - # END: struct nlmsghdr - + b'\0' * 8 # struct ifaddrmsg -) -GETADDRCMD = len(GETADDRCMD).to_bytes(4, byteorder='little') + GETADDRCMD[4:] # nlmsg_len +GETLINKCMD = _nlmsg(RTM_GETLINK, NLM_F_REQUEST | NLM_F_ROOT, _ifinfomsg(family=socket.AF_UNSPEC, change=0xFFFFFFFF)) # ****************************************************************************** -def get_ipaddress_obj(ipaddr): - '''@brief Return a IPv4Address or IPv6Address depending on whether @ipaddr - is a valid IPv4 or IPv6 address. Return None otherwise.''' - try: - ip = ipaddress.ip_address(ipaddr) - except ValueError: - return None +def _data_matches_mac(data, mac): + return mac.lower() == ':'.join([f'{x:02x}' for x in data[0:6]]) - return ip + +def mac2iface(mac: str): # pylint: disable=too-many-locals + '''@brief Find the interface that has @mac as its assigned MAC address. + @param mac: The MAC address to match + ''' + with socket.socket(family=socket.AF_NETLINK, type=socket.SOCK_RAW, proto=socket.NETLINK_ROUTE) as sock: + sock.sendall(GETLINKCMD) + nlmsg = sock.recv(8192) + nlmsg_idx = 0 + while True: # pylint: disable=too-many-nested-blocks + if nlmsg_idx >= len(nlmsg): + nlmsg += sock.recv(8192) + + nlmsghdr = nlmsg[nlmsg_idx : nlmsg_idx + NLMSG_HDRLEN] + nlmsg_len, nlmsg_type, _, _, _ = struct.unpack('<LHHLL', nlmsghdr) + + if nlmsg_type == NLMSG_DONE: + break + + if nlmsg_type == RTM_BASE: + msg_indx = nlmsg_idx + NLMSG_HDRLEN + msg = nlmsg[msg_indx : msg_indx + IFINFOMSG_SZ] # ifinfomsg + _, _, ifi_type, ifi_index, _, _ = struct.unpack('<BBHiII', msg) + + if ifi_type in (ARPHRD_LOOPBACK, ARPHRD_ETHER): + rtattr_indx = msg_indx + IFINFOMSG_SZ + while rtattr_indx < (nlmsg_idx + nlmsg_len): + rtattr = nlmsg[rtattr_indx : rtattr_indx + RTATTR_SZ] + rta_len, rta_type = struct.unpack('<HH', rtattr) + if rta_type == IFLA_ADDRESS: + data = nlmsg[rtattr_indx + RTATTR_SZ : rtattr_indx + rta_len] + if _data_matches_mac(data, mac): + return socket.if_indextoname(ifi_index) + + rta_len = RTA_ALIGN(rta_len) # Round up to multiple of 4 + rtattr_indx += rta_len # Move to next rtattr + + nlmsg_idx += nlmsg_len # Move to next Netlink message + + return '' # ****************************************************************************** @@ -71,8 +152,7 @@ def _data_matches_ip(data_family, data, ip): return other_ip == ip -# ****************************************************************************** -def iface_of(src_addr): +def _iface_of(src_addr): # pylint: disable=too-many-locals '''@brief Find the interface that has src_addr as one of its assigned IP addresses. @param src_addr: The IP address to match @type src_addr: Instance of ipaddress.IPv4Address or ipaddress.IPv6Address @@ -85,36 +165,133 @@ def iface_of(src_addr): if nlmsg_idx >= len(nlmsg): nlmsg += sock.recv(8192) - nlmsg_type = int.from_bytes(nlmsg[nlmsg_idx + 4 : nlmsg_idx + 6], byteorder='little', signed=False) + nlmsghdr = nlmsg[nlmsg_idx : nlmsg_idx + NLMSG_HDRLEN] + nlmsg_len, nlmsg_type, _, _, _ = struct.unpack('<LHHLL', nlmsghdr) + if nlmsg_type == NLMSG_DONE: break - if nlmsg_type != RTM_NEWADDR: - break + if nlmsg_type == RTM_NEWADDR: + msg_indx = nlmsg_idx + NLMSG_HDRLEN + msg = nlmsg[msg_indx : msg_indx + IFADDRMSG_SZ] # ifaddrmsg + ifa_family, _, _, _, ifa_index = struct.unpack('<BBBBL', msg) + + rtattr_indx = msg_indx + IFADDRMSG_SZ + while rtattr_indx < (nlmsg_idx + nlmsg_len): + rtattr = nlmsg[rtattr_indx : rtattr_indx + RTATTR_SZ] + rta_len, rta_type = struct.unpack('<HH', rtattr) + if rta_type == IFLA_ADDRESS: + data = nlmsg[rtattr_indx + RTATTR_SZ : rtattr_indx + rta_len] + if _data_matches_ip(ifa_family, data, src_addr): + return socket.if_indextoname(ifa_index) + + rta_len = RTA_ALIGN(rta_len) # Round up to multiple of 4 + rtattr_indx += rta_len # Move to next rtattr + + nlmsg_idx += nlmsg_len # Move to next Netlink message + + return '' + + +# ****************************************************************************** +def get_ipaddress_obj(ipaddr, ipv4_mapped_convert=False): + '''@brief Return a IPv4Address or IPv6Address depending on whether @ipaddr + is a valid IPv4 or IPv6 address. Return None otherwise. + + If ipv4_mapped_resolve is set to True, IPv6 addresses that are IPv4-Mapped, + will be converted to their IPv4 equivalent. + ''' + try: + ip = ipaddress.ip_address(ipaddr) + except ValueError: + return None + + if ipv4_mapped_convert: + ipv4_mapped = getattr(ip, 'ipv4_mapped', None) + if ipv4_mapped is not None: + ip = ipv4_mapped + + return ip + - nlmsg_len = int.from_bytes(nlmsg[nlmsg_idx : nlmsg_idx + 4], byteorder='little', signed=False) - if nlmsg_len % 4: # Is msg length not a multiple of 4? +# ****************************************************************************** +def net_if_addrs(): # pylint: disable=too-many-locals + '''@brief Return a dictionary listing every IP addresses for each interface. + The first IP address of a list is the primary address used as the default + source address. + @example: { + 'wlp0s20f3': { + 4: ['10.0.0.28'], + 6: [ + 'fd5e:9a9e:c5bd:0:5509:890c:1848:3843', + 'fd5e:9a9e:c5bd:0:1fd5:e527:8df7:7912', + '2605:59c8:6128:fb00:c083:1b8:c467:81d2', + '2605:59c8:6128:fb00:e99d:1a02:38e0:ad52', + 'fe80::d71b:e807:d5ee:7614' + ], + }, + 'lo': { + 4: ['127.0.0.1'], + 6: ['::1'], + }, + 'docker0': { + 4: ['172.17.0.1'], + 6: [] + }, + } + ''' + interfaces = {} + with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW) as sock: + sock.sendall(GETADDRCMD) + nlmsg = sock.recv(8192) + nlmsg_idx = 0 + while True: # pylint: disable=too-many-nested-blocks + if nlmsg_idx >= len(nlmsg): + nlmsg += sock.recv(8192) + + nlmsghdr = nlmsg[nlmsg_idx : nlmsg_idx + NLMSG_HDRLEN] + nlmsg_len, nlmsg_type, _, _, _ = struct.unpack('<LHHLL', nlmsghdr) + + if nlmsg_type == NLMSG_DONE: break - ifaddrmsg_indx = nlmsg_idx + NLMSGHDR_SZ - ifa_family = nlmsg[ifaddrmsg_indx] - ifa_index = int.from_bytes(nlmsg[ifaddrmsg_indx + 4 : ifaddrmsg_indx + 8], byteorder='little', signed=False) + if nlmsg_type == RTM_NEWADDR: + msg_indx = nlmsg_idx + NLMSG_HDRLEN + msg = nlmsg[msg_indx : msg_indx + IFADDRMSG_SZ] # ifaddrmsg + ifa_family, _, _, _, ifa_index = struct.unpack('<BBBBL', msg) + + if ifa_family in (socket.AF_INET, socket.AF_INET6): + interfaces.setdefault(ifa_index, {4: [], 6: []}) + + rtattr_indx = msg_indx + IFADDRMSG_SZ + while rtattr_indx < (nlmsg_idx + nlmsg_len): + rtattr = nlmsg[rtattr_indx : rtattr_indx + RTATTR_SZ] + rta_len, rta_type = struct.unpack('<HH', rtattr) - rtattr_indx = ifaddrmsg_indx + IFADDRMSG_SZ - while rtattr_indx < (nlmsg_idx + nlmsg_len): - rta_len = int.from_bytes(nlmsg[rtattr_indx : rtattr_indx + 2], byteorder='little', signed=False) - rta_type = int.from_bytes(nlmsg[rtattr_indx + 2 : rtattr_indx + 4], byteorder='little', signed=False) - if rta_type == IFLA_ADDRESS: - data = nlmsg[rtattr_indx + RTATTR_SZ : rtattr_indx + rta_len] - if _data_matches_ip(ifa_family, data, src_addr): - return socket.if_indextoname(ifa_index) + if rta_type == IFLA_IFNAME: + data = nlmsg[rtattr_indx + RTATTR_SZ : rtattr_indx + rta_len] + ifname = data.rstrip(b'\0').decode() + interfaces[ifa_index]['name'] = ifname - rta_len = (rta_len + 3) & ~3 # Round up to multiple of 4 - rtattr_indx += rta_len # Move to next rtattr + elif rta_type == IFLA_ADDRESS: + data = nlmsg[rtattr_indx + RTATTR_SZ : rtattr_indx + rta_len] + ip = get_ipaddress_obj(data) + if ip: + family = 4 if ifa_family == socket.AF_INET else 6 + interfaces[ifa_index][family].append(ip) + + rta_len = RTA_ALIGN(rta_len) # Round up to multiple of 4 + rtattr_indx += rta_len # Move to next rtattr nlmsg_idx += nlmsg_len # Move to next Netlink message - return '' + if_addrs = {} + for value in interfaces.values(): + name = value.pop('name', None) + if name is not None: + if_addrs[name] = value + + return if_addrs # ****************************************************************************** @@ -128,42 +305,4 @@ def get_interface(src_addr): src_addr = src_addr.split('%')[0] # remove scope-id (if any) src_addr = get_ipaddress_obj(src_addr) - return '' if src_addr is None else iface_of(src_addr) - - -# ****************************************************************************** -def remove_invalid_addresses(controllers: list): - '''@brief Remove controllers with invalid addresses from the list of controllers. - @param controllers: List of TIDs - ''' - service_conf = conf.SvcConf() - valid_controllers = list() - for controller in controllers: - if controller.transport in ('tcp', 'rdma'): - # Let's make sure that traddr is - # syntactically a valid IPv4 or IPv6 address. - ip = get_ipaddress_obj(controller.traddr) - if ip is None: - logging.warning('%s IP address is not valid', controller) - continue - - # Let's make sure the address family is enabled. - if ip.version not in service_conf.ip_family: - logging.debug( - '%s ignored because IPv%s is disabled in %s', - controller, - ip.version, - service_conf.conf_file, - ) - continue - - valid_controllers.append(controller) - - elif controller.transport in ('fc', 'loop'): - # At some point, need to validate FC addresses as well... - valid_controllers.append(controller) - - else: - logging.warning('Invalid transport %s', controller.transport) - - return valid_controllers + return '' if src_addr is None else _iface_of(src_addr) |