import ipaddress
import socket
import sys
from socket import AddressFamily # type hint
from typing import Iterator, List
import psutil
from ..logger import get_logger
from .mac import normalize_mac
# netifaces needs a new maintaine
# https://github.com/al45tair/netifaces/issues/78
# use psutil instead
logger = get_logger('network')
def _compatible_ipv6_address(address: str) -> str:
"""Remove the zone/index suffix from an IPv6 address (e.g. '%eth0') for older python versions."""
if sys.version_info < (3, 9):
if '%' in address:
return address.split('%', 1)[0]
return address
[docs]
def get_interfaces() -> List[str]:
"""Get all network interfaces names.
Returns:
List[str]: interface names
"""
return list(psutil.net_if_addrs().keys())
[docs]
def get_all_ips_from_interface(interface: str, family: AddressFamily = socket.AF_INET, prefix: str = '') -> List[str]:
"""Get the IP address from network interface name.
Args:
interface (str): specify network interface name
family (str, optional): address family. Defaults to AF_INET.
prefix (str, optional): filter ip address by prefix. Defaults to ''.
Raises:
ValueError: Can not get IP address
Returns:
List[str]: IP addresses from the interface
"""
addr_list = []
for if_name, addrs in psutil.net_if_addrs().items():
if interface and if_name != interface:
continue
for addr in addrs:
if addr.family != family:
continue
_ip = ipaddress.ip_address(_compatible_ipv6_address(addr.address))
if prefix and not addr.address.startswith(prefix):
continue
# Sort all available IP addresses.
# Put private ip4 or link-local ipv6 to the last
if _ip.is_loopback:
# Do not ignore loopback addresses
addr_list.append(addr.address)
elif _ip.is_link_local:
# '169.254.x.x', 'fe80::'
addr_list.append(addr.address)
else:
addr_list.insert(0, addr.address)
if not addr_list:
raise ValueError(f'Can not get IP address from interface {interface}')
return addr_list
[docs]
def get_ip4_from_interface(interface: str = '') -> str:
"""Get the IPv4 address from network interface name.
Args:
interface (str, optional): specify network interface name
Returns:
str: The most preferred IP address
"""
return get_all_ips_from_interface(interface, socket.AF_INET)[0]
[docs]
def get_ip6_from_interface(interface: str = '', prefix: str = '') -> str:
"""Get the IPv6 address from network interface name.
Args:
interface (str, optional): specify network interface name
prefix (str, optional): filter ip address by prefix. Defaults to ''.
Returns:
str: The most preferred IPv6 address
"""
return get_all_ips_from_interface(interface, socket.AF_INET6, prefix=prefix)[0]
[docs]
def get_local_ip4(to_addr: str = '') -> str:
"""Get the local IP (v4) that most likely to be able to connect to a remote IP or the Internet.
Depends on the routing table settings of this machine,
usually returns the highest priority IP in the routing table.
Args:
to_addr (str, optional): Try to get an IP that connect to this remote. Defaults to '8.8.8.8'(Internet).
Returns:
str: local IP address
"""
if not to_addr:
to_addr = '8.8.8.8'
s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s1.connect((to_addr, 80))
local_ip = s1.getsockname()[0]
s1.close()
assert isinstance(local_ip, str)
logger.debug(f'Using host ip: {local_ip}')
return local_ip
[docs]
def ip_in_network(ip: str, network: str) -> bool:
"""Check the IP (v4/v6) address is in the network with given mask.
Using ipaddress module: https://docs.python.org/3/library/ipaddress.html#module-ipaddress
Args:
ip (str): IP address, eg: 192.168.1.2
subnet (str): network, eg: 192.168.1.0/24 or 192.168.1.0/255.255.255.0
Returns:
bool: True if the IP address is in the network
"""
return ipaddress.ip_address(ip) in ipaddress.ip_network(network, strict=False)
[docs]
def guess_local_ip6(
to_addr: str,
interface: str = '',
) -> Iterator[str]:
"""Guess ipv6 address by given remote ipv6.
Args:
to_addr (str): target ip6 address that you want to establish a connection.
interface (str, optional): specify local net interface. Defaults to ''.
Yields:
Iterator[str]: possible IP addresses (eg: fe80::2%eth0) that may connect to the given to_addr.
"""
target = ipaddress.ip_address(_compatible_ipv6_address(to_addr))
for if_name, addrs in psutil.net_if_addrs().items():
if interface and if_name != interface:
continue
for addr in addrs:
if addr.family != socket.AF_INET6:
continue
_ip = ipaddress.ip_address(_compatible_ipv6_address(addr.address))
assert addr.netmask
_mask_len = bin(int(ipaddress.ip_address(addr.netmask))).count('1')
base_addr = _compatible_ipv6_address(addr.address)
_net = ipaddress.ip_network(f'{base_addr}/{_mask_len}', strict=False)
if target in _net:
yield addr.address
# If target is global address, do not check
if target.is_global and _ip.is_global:
yield addr.address
[docs]
def get_mac_by_interface(interface: str) -> str:
"""Get hardware mac address from network interface name.
Args:
interface (str): net interface name
Returns:
str: mac address (lower case)
"""
if_addrs = psutil.net_if_addrs()
mac_addr = ''
for addr in if_addrs[interface]:
if sys.platform == 'win32':
if addr.family == psutil.AF_LINK:
assert isinstance(addr.address, str)
mac_addr = addr.address
else:
if addr.family == socket.AF_PACKET:
assert isinstance(addr.address, str)
mac_addr = addr.address
if mac_addr:
return normalize_mac(mac_addr).lower()
raise ValueError(f'Failed to get addr info from {interface}')
[docs]
def get_interface_by_mac(mac_addr: str) -> str:
"""Get network interface name by given mac address.
Args:
mac_addr (str): hardware mac address.
Returns:
str: network interface name
"""
for interface, addrs in psutil.net_if_addrs().items():
for addr in addrs:
if sys.platform == 'win32':
if addr.family != psutil.AF_LINK:
continue
else:
if addr.family != socket.AF_PACKET:
continue
if normalize_mac(addr.address).lower() == mac_addr.lower():
assert isinstance(interface, str)
return interface
raise ValueError(f'Failed to get interface with mac {mac_addr}')