Commit 99e28608 authored by Xavier Thompson's avatar Xavier Thompson

slapformat: WIP: Add tap

parent f933b3a6
......@@ -99,6 +99,7 @@ class Options(object):
create_tap: bool = True
tap_base_name: str = 'slaptap'
tap_ipv6: bool = True
tap_ipv4_prefix: int = 32
tap_gateway_interface: str = None
create_tun: bool = False
......@@ -292,6 +293,7 @@ class Interface(object):
ipv6_interface : str
ipv4_network : ipaddress.IPv4Network
ipv6_network : ipaddress.IPv6Network
tap_ipv4_network : ipaddress.IPv4Network
conf : FormatConfig
def __init__(self, conf):
......@@ -300,6 +302,7 @@ class Interface(object):
self.ipv6_interface = conf.ipv6_interface or conf.interface_name
self.ipv4_network = self.getIPv4Network(conf.ipv4_local_network)
self.ipv6_network = self.getIPv6Network()
self.tap_ipv4_network = self.getTapIPv4Network(conf)
def getIPv4Network(self, cidr):
if cidr:
......@@ -310,6 +313,45 @@ class Interface(object):
network = self.ipv4_network
return ipaddress.IPv4Interface((network[index + 2], network.prefixlen))
def getTapIPv4Network(self, conf):
tap_gateway_interface = conf.tap_gateway_interface
if conf.create_tap and tap_gateway_interface:
try:
addresses = netifaces.ifaddresses(tap_gateway_interface)[AF_INET]
except ValueError:
self.conf.abort(
"Interface %s (intended as tap gateway interface) does not exist",
tap_gateway_interface
)
except KeyError:
self.conf.abort(
"%s must have at least one IPv4 address assigned",
tap_gateway_interface
)
result = None
for a in addresses:
address = a['addr'].split('%')[0]
netmask = a['netmask'].split('/')[-1]
ip = ipaddress.IPv4Interface('%s/%s' % (address, netmask))
network = ip.network
if not result or network.prefixlen < result.prefixlen:
result = network
return result
def getTapIPv4Gateway(self, _index):
network = self.tap_ipv4_network
if network:
return ipaddress.IPv4Interface((network[1], network.prefixlen))
def getTapIPv4Range(self, index):
network = self.tap_ipv4_network
if network:
prefixlen = conf.tap_ipv4_prefix
i = index + 2 if prefixlen == 32 else index + 1
k = 1 if prefixlen < 31 else 0
addr = network[(i << (32 - prefixlen)) + k]
return ipaddress.IPv4Interface((addr, prefixlen))
def getIPv6Network(self):
try:
addresses = netifaces.ifaddresses(self.ipv6_interface)[AF_INET6]
......@@ -329,6 +371,22 @@ class Interface(object):
result = network
return result
def getIPv6LinkLocalAddress(self, interface):
try:
addresses = netifaces.ifaddresses(interface)[AF_INET6]
except KeyError:
addresses = ()
for a in addresses:
address = a['addr'].split('%')[0]
netmask = a['netmask'].split('/')[-1]
ip = ipaddress.IPv6Interface('%s/%s' % (address, netmask))
if ip.is_link_local:
return ip
self.conf.abort(
"No link-local IPv6 address found on %s",
interface
)
def getComputerIPv6Addr(self):
network = self.ipv6_network
return ipaddress.IPv6Interface((network[1], network.prefixlen))
......@@ -340,12 +398,21 @@ class Interface(object):
def getPartitionIPv6Range(self, index):
network = self.ipv6_network
prefixlen = network.prefixlen + 16
if prefixlen > 128:
if prefixlen > 128: # XXX move this check elsewhere
self.conf.abort("IPv6 network %s is too small for IPv6 ranges", network)
bits = 128 - network.prefixlen
addr = network[(1 << (bits - 2)) + (index << (128 - prefixlen))]
return ipaddress.IPv6Network((addr, prefixlen))
def getTapIPv6Range(self, index):
network = self.ipv6_network
prefixlen = network.prefixlen + 16
if prefixlen > 128: # XXX move this check elsewhere
self.conf.abort("IPv6 network %s is too small for IPv6 ranges", network)
bits = 128 - network.prefixlen
addr = network[(2 << (bits - 2)) + (index << (128 - prefixlen)) + 1]
return ipaddress.IPv6Interface((addr, prefixlen))
def enableIPv6NonLocalBind(self):
call(['sysctl', 'net.ipv6.ip_nonlocal_bind=1'])
network = str(self.ipv6_network)
......@@ -407,8 +474,8 @@ class Partition(object):
ipv4_list: list[ipaddress.IPv4Interface]
ipv6_list: list[ipaddress.IPv6Interface]
ipv6_range: ipaddress.IPv6Network = None
tap : Tap
tun : Tun
tap : Tap = None
tun : Tun = None
def __init__(self, index, computer, definition=None):
i = str(index)
......@@ -435,7 +502,12 @@ class Partition(object):
self.ipv6_range = ipaddress.IPv6Network(ipv6_range, strict=False)
elif conf.ipv6_range:
self.ipv6_range = interface.getPartitionIPv6Range(index)
# Tap & Tun
# Tap
tap = options['network_interface']
if tap or conf.create_tap:
tap = tap or conf.tap_base_name + i
self.tap = Tap(tap, index, computer, definition)
# Tun
# XXX
def format(self, interface):
......@@ -445,6 +517,8 @@ class Partition(object):
interface.addAddress(ip, self.reference)
for ip in self.ipv6_list:
interface.addAddress(ip, self.reference)
if self.tap:
self.tap.format(interface, self.user)
def checkFormat(self, interface):
for ip in self.ipv4_list:
......@@ -510,6 +584,120 @@ class User(object):
return False
class Tap(object):
name : str
ipv4_gateway : ipaddress.IPv4Interface = None
ipv4_address : ipaddress.IPv4Interface = None
ipv6_gateway : ipaddress.IPv6Interface = None
ipv6_address : ipaddress.IPv6Interface = None
def __init__(self, name, index, computer, definition):
conf = computer.conf
interface = computer.interface
options = definition[name]
self.name = name
# Tap IPv4: tap-to-host gateway and host-to-tap address
ipv4_gateway = options['ipv4_gateway']
ipv4_address = options['ipv4_address']
if bool(ipv4_gateway) != bool(ipv4_address):
conf.wrong(
"Options %r and %r for tap %s must either both be provided or neither",
'ipv4_gateway', 'ipv4_address', name
)
elif ipv4_address:
self.ipv4_gateway = ipaddress.IPv4Interface(ipv4_gateway)
self.ipv4_address = ipaddress.IPv4Interface(ipv4_address)
elif interface.tap_ipv4_network:
self.ipv4_gateway = interface.getTapIPv4Gateway(index)
self.ipv4_address = interface.getTapIPv4Range(index)
# Tap IPv6: host-to-tap gateway and tap-to-host address
ipv6_gateway = options['ipv6_gateway']
link_local = None # link-local address resolved later
if ipv6_gateway:
self.ipv6_gateway = ipaddress.IPv4Interface(ipv6_gateway)
ipv6_address = options['ipv6_address']
if ipv6_address:
self.ipv6_address = ipaddress.IPv6Interface(ipv6_address)
else:
self.ipv6_address = link_local
elif conf.tap_ipv6:
self.ipv6_gateway = interface.getTapIPv6Range(index)
self.ipv6_address = link_local
def format(self, interface, user):
tap = self.name
conf = interface.conf
# Create/ensure TAP interface
self.ensureTap(user, conf)
# Find link local address if needed
if self.ipv6_gateway and not self.ipv6_address:
self.ipv6_address = interface.getIPv6LinkLocalAddress(tap)
# Create/ensure IPv4 routes
self.ensureIPv4Routes(conf)
# Create/ensure IPv6 routes
self.ensureIPv6Routes(conf)
def ensureTap(self, user, conf):
tap = self.name
user = user.name
check_file = '/sys/devices/virtual/net/%s/owner' % tap
if os.path.exists(check_file):
with open(check_file) as f:
owner_id = f.read().strip()
try:
owner_id = int(owner_id)
except ValueError:
owner_id = pwd.getpwnam(owner_id).pw_uid
user_id = pwd.getpwnam(user).pw_uid
if owner_id != user_id:
conf.abort(
"Wrong owner of TAP %s, expected %s (%s) got %s",
self.name, user_id, user, owner_id
)
else:
call(['ip', 'tuntap', 'add', 'dev', tap, 'mode', 'TAP', 'user', user])
call(['ip', 'link', 'set', tap, 'up'])
def ensureIPv4Routes(self, conf):
tap = self.name
ipv4 = self.ipv4_address
if ipv4:
cidr = str(ipv4)
code, out = call(['ip', 'route', 'show', cidr], throw=False)
if code != 0 or cidr not in out or tap not in out:
call(['ip', 'route', 'add', cidr, 'dev', tap])
if ipv4.network.prefixlen < 32:
network = str(ipv4.network)
code, out = call(['ip', 'route', 'show', network], throw=False)
if code != 0 or 'dev ' + tap not in out:
call(['ip', 'route', 'add', network, 'dev', tap, 'via', cidr])
elif 'via ' + cidr not in out:
conf.abort(
"Route to network %s (%s) exists but is not via %s",
network, tap, cidr
)
def ensureIPv6Routes(self, conf):
tap = self.name
ipv6 = self.ipv6_gateway
if ipv6:
cidr = str(ipv6)
code, out = call(['ip', '-6', 'route', 'show', cidr], throw=False)
if code != 0 or cidr not in out or tap not in out:
call(['ip', '-6', 'route', 'add', cidr, 'dev', tap])
network = str(ipv6.network)
code, out = call(['ip', '-6', 'route', 'show', network], throw=False)
if code != 0 or 'dev ' + tap not in out:
call(['ip', '-6', 'route', 'add', network, 'dev', tap, 'via', cidr])
elif 'via ' + cidr not in out:
conf.warn(
"Route to network %s (%s) exists but is not via %s, fixing it",
network, tap, cidr
)
call(['ip', '-6', 'route', 'del', network, 'dev', tap])
call(['ip', '-6', 'route', 'add', network, 'dev', tap, 'via', cidr])
# Utilities
def call(args, throw=True):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment