This commit is contained in:
1 parent
cc6748a622
commit
a4cffb67e4
10 files changed
+370
-139
No files matched your search
@@ -21,6 +21,7 @@ if str(LIB_DIR) not in sys.path:
|
||||
|
||||
import vpnrouter_ldap
|
||||
import vpnrouter_platforms
|
||||
from vpnrouter_net import ConfigError, detect_interfaces, interface_addresses, ip_output
|
||||
|
||||
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
|
||||
TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates')
|
||||
@@ -92,10 +93,6 @@ MODES = ('manual', 'interfaces', 'auto')
|
||||
PLATFORM_NONE = 'none'
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""A setting is present but malformed."""
|
||||
|
||||
|
||||
def warn(message):
|
||||
print(f'vpn-router: warning: {message}', file=sys.stderr)
|
||||
|
||||
@@ -327,55 +324,6 @@ def deferred(cfg):
|
||||
return (_get(cfg, 'general', 'platform') or PLATFORM_NONE) == PLATFORM_NONE
|
||||
|
||||
|
||||
def _ip_route(*args):
|
||||
result = subprocess.run(['ip', *args], capture_output=True, text=True)
|
||||
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_route('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_route('-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 detect_gateway(iface, address):
|
||||
"""Derive the next hop on the internal side.
|
||||
|
||||
@@ -383,12 +331,12 @@ def detect_gateway(iface, address):
|
||||
back to the first host address of the connected subnet, which is a
|
||||
convention rather than a fact - so say so.
|
||||
"""
|
||||
for line in _ip_route('-o', 'route', 'show', 'dev', iface).splitlines():
|
||||
for line in ip_output('-o', 'route', 'show', 'dev', iface).splitlines():
|
||||
fields = line.split()
|
||||
if 'via' in fields:
|
||||
return fields[fields.index('via') + 1]
|
||||
|
||||
for line in _ip_route('-o', '-4', 'addr', 'show', 'dev', iface).splitlines():
|
||||
for line in ip_output('-o', '-4', 'addr', 'show', 'dev', iface).splitlines():
|
||||
fields = line.split()
|
||||
if len(fields) >= 4 and fields[3].split('/')[0] == address:
|
||||
network = ipaddress.ip_interface(fields[3]).network
|
||||
@@ -399,34 +347,6 @@ def detect_gateway(iface, address):
|
||||
return ''
|
||||
|
||||
|
||||
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 resolve_interfaces(cfg):
|
||||
"""Return (ext_iface, int_iface, local_addrs, int_addr, int_gateway_ip).
|
||||
|
||||
|
||||
Reference in new issue
Block a user