110 lines
3.7 KiB
Python
110 lines
3.7 KiB
Python
"""Interfaces and addresses of the router, read from the system.
|
|
|
|
Only the Python standard library is used, so the web service can use this module too.
|
|
"""
|
|
|
|
import pathlib
|
|
import subprocess
|
|
|
|
|
|
class ConfigError(Exception):
|
|
"""A setting is present but malformed."""
|
|
|
|
|
|
def ip_output(*args):
|
|
"""The output of an ip command, or '' when it fails."""
|
|
try:
|
|
result = subprocess.run(['ip', *args], capture_output=True, text=True)
|
|
except OSError:
|
|
return ''
|
|
return result.stdout if result.returncode == 0 else ''
|
|
|
|
|
|
def detect_interfaces():
|
|
"""Work out the external and internal interface names.
|
|
|
|
Only reached in auto mode, where the operator has accepted that a wrong
|
|
guess is possible. The external interface is whichever one carries the
|
|
default route; the internal one is the only other addressed interface.
|
|
"""
|
|
tokens = ip_output('route', 'get', '1.1.1.1').split()
|
|
external = ''
|
|
for index, token in enumerate(tokens):
|
|
if token == 'dev' and index + 1 < len(tokens):
|
|
external = tokens[index + 1]
|
|
break
|
|
if not external:
|
|
raise ConfigError('auto mode: cannot determine the external interface')
|
|
|
|
candidates = [name for name in _addressed_interfaces() if name != external]
|
|
if len(candidates) != 1:
|
|
raise ConfigError(
|
|
f'auto mode: expected one internal interface, found {len(candidates)}; '
|
|
'use mode = interfaces and name them')
|
|
return external, candidates[0]
|
|
|
|
|
|
def _is_virtual(name):
|
|
"""True for an interface the kernel creates without a device behind it.
|
|
|
|
Loopback, bridges, veth pairs and tunnels live under /sys/devices/virtual,
|
|
which is where the Docker bridge is; a NIC of the virtual machine does not.
|
|
"""
|
|
link = pathlib.Path('/sys/class/net') / name
|
|
return '/devices/virtual/' in str(link.resolve())
|
|
|
|
|
|
def _addressed_interfaces():
|
|
"""Names of the NICs holding at least one IPv4 address; virtual interfaces are left out."""
|
|
names = []
|
|
for line in ip_output('-o', '-4', 'addr', 'show').splitlines():
|
|
fields = line.split()
|
|
if len(fields) >= 4 and fields[1] not in names and not _is_virtual(fields[1]):
|
|
names.append(fields[1])
|
|
return names
|
|
|
|
|
|
def interface_addresses(name, setting):
|
|
"""IPv4 addresses configured on an interface.
|
|
|
|
The interface name is configuration; its addresses are read from the
|
|
system. Looking up the addresses of a named interface is deterministic -
|
|
unlike working out which interface to use, which is a guess the package
|
|
deliberately does not make.
|
|
"""
|
|
if not name:
|
|
return []
|
|
if not (pathlib.Path('/sys/class/net') / name).exists():
|
|
raise ConfigError(f'{setting}: interface {name!r} does not exist')
|
|
|
|
result = subprocess.run(
|
|
['ip', '-o', '-4', 'addr', 'show', 'dev', name],
|
|
capture_output=True, text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
raise ConfigError(f'{setting}: cannot read addresses of {name!r}')
|
|
|
|
addresses = []
|
|
for line in result.stdout.splitlines():
|
|
fields = line.split()
|
|
if len(fields) >= 4:
|
|
addresses.append(fields[3].split('/')[0])
|
|
return addresses
|
|
|
|
|
|
def internal_address(mode, internal, int_addr):
|
|
"""The router's address on the internal network, as general.mode directs, or '' when it is not known yet.
|
|
|
|
manual takes local.int_addr; interfaces reads the address of interfaces.internal; auto also
|
|
works out which interface that is.
|
|
"""
|
|
if mode == 'manual':
|
|
return int_addr
|
|
try:
|
|
if mode == 'auto':
|
|
internal = detect_interfaces()[1]
|
|
addresses = interface_addresses(internal, 'interfaces.internal')
|
|
except ConfigError:
|
|
return ''
|
|
return addresses[0] if addresses else ''
|