mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
346 lines
13 KiB
Python
346 lines
13 KiB
Python
"""Start a DHCP server in the background"""
|
|
import os
|
|
import tempfile as tf
|
|
import subprocess
|
|
|
|
|
|
class Server:
|
|
config_file = '/tmp/udhcpd.conf'
|
|
leases_file = '/tmp/udhcpd.leases'
|
|
|
|
def __init__(self, netns, start='192.168.0.100', end='192.168.0.110',
|
|
netmask='255.255.255.0', ip=None, router=None, prefix=None,
|
|
hostname=None, iface="iface"):
|
|
self.process = None
|
|
self.netns = netns
|
|
self.iface = iface
|
|
self._create_files(start, end, netmask, ip, router, prefix, hostname)
|
|
|
|
def __del__(self):
|
|
"""Clean up config and lease files"""
|
|
try:
|
|
if os.path.exists(self.config_file):
|
|
os.unlink(self.config_file)
|
|
if os.path.exists(self.leases_file):
|
|
os.unlink(self.leases_file)
|
|
except:
|
|
pass
|
|
|
|
def __enter__(self):
|
|
self.start()
|
|
|
|
def __exit__(self, _, __, ___):
|
|
self.stop()
|
|
|
|
def _create_files(self, start, end, netmask, ip, router, prefix, hostname):
|
|
f = open(self.leases_file, "w")
|
|
f.close()
|
|
|
|
with open(self.config_file, 'w') as f:
|
|
if ip:
|
|
start=ip
|
|
end=ip
|
|
f.write(f'''# Generated by Infamy DHCP
|
|
lease_file {self.leases_file}
|
|
interface {self.iface}
|
|
start {start}
|
|
end {end}
|
|
max_leases 1
|
|
option subnet {netmask}
|
|
option lease 864000
|
|
''')
|
|
if router:
|
|
f.write(f"option router {router}\n")
|
|
if prefix and router:
|
|
f.write(f"option staticroutes {prefix} {router}\n")
|
|
if hostname:
|
|
f.write(f"option hostname {hostname}\n")
|
|
|
|
def get_pid(self):
|
|
return self.process.pid
|
|
|
|
def start(self):
|
|
if not os.path.exists(self.config_file):
|
|
raise Exception("Config file does not exist. Please create it first.")
|
|
cmd = f"udhcpd -f {self.config_file}"
|
|
self.process = self.netns.popen(cmd.split(" "))
|
|
|
|
def stop(self):
|
|
if self.process:
|
|
self.process.terminate()
|
|
self.process.wait()
|
|
self.process = None
|
|
|
|
|
|
class Server6Dnsmasq:
|
|
"""DHCPv6 server using dnsmasq"""
|
|
|
|
def __init__(self, netns, start=None, end=None, dns=None, domain=None,
|
|
iface="iface", address=None):
|
|
"""Initialize DHCPv6 server
|
|
|
|
Args:
|
|
netns: Network namespace to run server in
|
|
start: Starting IPv6 address for range (e.g., "2001:db8::100")
|
|
end: Ending IPv6 address for range (e.g., "2001:db8::200")
|
|
dns: DNS server addresses (list or string)
|
|
domain: DNS search domain
|
|
iface: Interface to listen on
|
|
"""
|
|
self.process = None
|
|
self.netns = netns
|
|
self.iface = iface
|
|
self.config_file = tf.NamedTemporaryFile(mode='w', prefix='dnsmasq6_',
|
|
suffix='.conf', delete=False)
|
|
self.config_path = self.config_file.name
|
|
self.leases_file = tf.NamedTemporaryFile(mode='w', prefix='dnsmasq6_',
|
|
suffix='.leases', delete=False)
|
|
self.leases_path = self.leases_file.name
|
|
self.leases_file.close()
|
|
|
|
self.hosts_file = tf.NamedTemporaryFile(mode='w', delete=False)
|
|
self.hosts_path = self.hosts_file.name
|
|
|
|
self._create_config(start, end, dns, domain, address)
|
|
|
|
def __del__(self):
|
|
try:
|
|
if os.path.exists(self.config_path):
|
|
os.unlink(self.config_path)
|
|
if os.path.exists(self.leases_path):
|
|
os.unlink(self.leases_path)
|
|
if os.path.exists(self.hosts_path):
|
|
os.unlink(self.hosts_path)
|
|
except:
|
|
pass
|
|
|
|
def __enter__(self):
|
|
self.start()
|
|
return self
|
|
|
|
def __exit__(self, _, __, ___):
|
|
self.stop()
|
|
|
|
def _create_config(self, start, end, dns, domain, address):
|
|
"""Create dnsmasq configuration for DHCPv6"""
|
|
with self.hosts_file:
|
|
# NOTE: most basic tooling expect the server to reply with
|
|
# and A record, not just a AAAA record, so a client
|
|
# like ping, in dhcpv6_basic, will fail even if -6
|
|
self.hosts_file.write("# Generated by Infamy DHCPv6\n")
|
|
self.hosts_file.write(f"10.10.0.1 server.{domain}\n") # dummy
|
|
self.hosts_file.write(f"{address} server.{domain}\n")
|
|
|
|
with self.config_file:
|
|
self.config_file.write("# Generated by Infamy DHCPv6\n")
|
|
self.config_file.write(f"interface={self.iface}\n")
|
|
self.config_file.write(f"addn-hosts={self.hosts_path}\n")
|
|
self.config_file.write(f"domain={domain}\n")
|
|
self.config_file.write("enable-ra\n")
|
|
self.config_file.write(f"dhcp-leasefile={self.leases_path}\n")
|
|
self.config_file.write(f"dhcp-range={self.iface},")
|
|
|
|
if start and end:
|
|
# Stateful DHCPv6 - assign addresses
|
|
self.config_file.write(f"{start},{end},64,3600\n")
|
|
else:
|
|
# Stateless DHCPv6 - only provide options
|
|
# TODO: needs more testing with ra-names + slaac
|
|
self.config_file.write(f"::10,::fff,constructor:{self.iface},")
|
|
self.config_file.write("64,3600\n")
|
|
|
|
if dns:
|
|
if isinstance(dns, list):
|
|
dns_str = ','.join(dns)
|
|
else:
|
|
dns_str = dns
|
|
self.config_file.write(f"dhcp-option=option6:dns-server,{dns_str}\n")
|
|
|
|
if domain:
|
|
self.config_file.write(f"dhcp-option=option6:domain-search,{domain}\n")
|
|
|
|
self.config_file.write("log-debug\n")
|
|
self.config_file.write("log-dhcp\n")
|
|
|
|
def start(self):
|
|
"""Start the DHCPv6 server"""
|
|
if not os.path.exists(self.config_path):
|
|
raise Exception("Config file does not exist")
|
|
|
|
# Drop DEVNULL redirec to debug
|
|
cmd = f"dnsmasq --conf-file={self.config_path} --no-daemon"
|
|
self.process = self.netns.popen(cmd.split(" "),
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
|
|
def stop(self):
|
|
"""Stop the DHCPv6 server"""
|
|
if self.process:
|
|
self.process.terminate()
|
|
self.process.wait()
|
|
self.process = None
|
|
|
|
class Server6Dhcpd:
|
|
"""DHCPv6 server using ISC dhcpd with prefix delegation support"""
|
|
|
|
def __init__(self, netns, start=None, end=None, prefix=None, prefix_len=64,
|
|
dns=None, domain=None, iface="iface", address=None, subnet="2001:db8::/48"):
|
|
"""Initialize DHCPv6 server with ISC dhcpd
|
|
|
|
Args:
|
|
netns: Network namespace to run server in
|
|
start: Starting IPv6 address for IA_NA range (e.g., "2001:db8::100")
|
|
end: Ending IPv6 address for IA_NA range (e.g., "2001:db8::200")
|
|
prefix: IPv6 prefix pool for prefix delegation (e.g., "2001:db8:100::")
|
|
prefix_len: Length of delegated prefixes (e.g., 64 for /64 prefixes)
|
|
dns: DNS server addresses (list or string)
|
|
domain: DNS search domain
|
|
iface: Interface to listen on
|
|
address: Server address on the interface (for subnet config)
|
|
subnet: Subnet declaration (e.g., "2001:db8::/48")
|
|
"""
|
|
self.process = None
|
|
self.netns = netns
|
|
self.iface = iface
|
|
self.subnet = subnet
|
|
|
|
self.config_file = tf.NamedTemporaryFile(mode='w', prefix='dhcpd6_',
|
|
suffix='.conf', delete=False)
|
|
self.config_path = self.config_file.name
|
|
|
|
self.leases_file = tf.NamedTemporaryFile(mode='w', prefix='dhcpd6_',
|
|
suffix='.leases', delete=False)
|
|
self.leases_path = self.leases_file.name
|
|
self.leases_file.close()
|
|
|
|
self.pid_file = tf.NamedTemporaryFile(mode='w', prefix='dhcpd6_',
|
|
suffix='.pid', delete=False)
|
|
self.pid_path = self.pid_file.name
|
|
self.pid_file.close()
|
|
|
|
self._create_config(start, end, prefix, prefix_len, dns, domain)
|
|
|
|
def __del__(self):
|
|
try:
|
|
if os.path.exists(self.config_path):
|
|
os.unlink(self.config_path)
|
|
if os.path.exists(self.leases_path):
|
|
os.unlink(self.leases_path)
|
|
if os.path.exists(self.pid_path):
|
|
os.unlink(self.pid_path)
|
|
except:
|
|
pass
|
|
|
|
def __enter__(self):
|
|
self.start()
|
|
return self
|
|
|
|
def __exit__(self, _, __, ___):
|
|
self.stop()
|
|
|
|
def _create_config(self, start, end, prefix, prefix_len, dns, domain):
|
|
"""Create ISC dhcpd configuration for DHCPv6 with prefix delegation"""
|
|
with self.config_file:
|
|
self.config_file.write("# Generated by Infamy DHCPv6\n")
|
|
|
|
# Global options
|
|
self.config_file.write("default-lease-time 3600;\n")
|
|
self.config_file.write("max-lease-time 7200;\n")
|
|
self.config_file.write("log-facility local7;\n")
|
|
self.config_file.write("\n")
|
|
|
|
# DNS options
|
|
if dns:
|
|
if isinstance(dns, list):
|
|
dns_str = ', '.join(dns)
|
|
else:
|
|
dns_str = dns
|
|
self.config_file.write(f"option dhcp6.name-servers {dns_str};\n")
|
|
|
|
if domain:
|
|
self.config_file.write(f"option dhcp6.domain-search \"{domain}\";\n")
|
|
|
|
self.config_file.write("\n")
|
|
|
|
# Subnet declaration
|
|
self.config_file.write(f"subnet6 {self.subnet} {{\n")
|
|
|
|
# Address range for IA_NA (regular address assignment)
|
|
if start and end:
|
|
self.config_file.write(f" range6 {start} {end};\n")
|
|
|
|
# Prefix delegation pool
|
|
if prefix:
|
|
# Calculate end of prefix pool based on prefix_len
|
|
# For simplicity, we'll create a reasonable range
|
|
# e.g., if prefix is 2001:db8:100:: and prefix_len is 64,
|
|
# delegate /64 prefixes from 2001:db8:100:: to 2001:db8:1ff::
|
|
prefix_base = prefix.rstrip(':')
|
|
# Simple approach: if prefix ends with ::, add range
|
|
if prefix.endswith('::'):
|
|
prefix_start = prefix
|
|
# Create end address by incrementing last hex digit before ::
|
|
# For 2001:db8:100::, end would be 2001:db8:1ff::
|
|
parts = prefix_base.split(':')
|
|
if len(parts) >= 2:
|
|
last_part = parts[-2] if parts[-2] else parts[-3]
|
|
try:
|
|
last_val = int(last_part, 16)
|
|
end_val = last_val + 0xff
|
|
parts_copy = parts[:-1]
|
|
parts_copy[-1] = f"{end_val:x}"
|
|
prefix_end = ':'.join(parts_copy) + '::'
|
|
except:
|
|
prefix_end = prefix # Fallback
|
|
else:
|
|
prefix_end = prefix
|
|
else:
|
|
prefix_start = prefix
|
|
prefix_end = prefix
|
|
|
|
self.config_file.write(f" prefix6 {prefix_start} {prefix_end} /{prefix_len};\n")
|
|
|
|
self.config_file.write("}\n")
|
|
|
|
def start(self):
|
|
"""Start the DHCPv6 server"""
|
|
if not os.path.exists(self.config_path):
|
|
raise Exception("Config file does not exist")
|
|
|
|
# Debug: show config and interface status
|
|
# self.netns.popen(f"cat {self.config_path}".split(" "))
|
|
# self.netns.popen("ifconfig")
|
|
|
|
# ISC dhcpd command for IPv6
|
|
# -6: IPv6 mode
|
|
# -f: Stay in foreground (no daemon)
|
|
# -d: Debug/log to stderr
|
|
# -cf: Config file
|
|
# -lf: Lease file
|
|
# -pf: PID file
|
|
cmd = [
|
|
"dhcpd",
|
|
"-6", # IPv6 mode
|
|
"-f", # Foreground
|
|
"-d", # Debug output
|
|
"-cf", self.config_path,
|
|
"-lf", self.leases_path,
|
|
"-pf", self.pid_path,
|
|
self.iface # Interface to listen on
|
|
]
|
|
|
|
self.process = self.netns.popen(cmd,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
|
|
def stop(self):
|
|
"""Stop the DHCPv6 server"""
|
|
if self.process:
|
|
self.process.terminate()
|
|
try:
|
|
self.process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
self.process.kill()
|
|
self.process.wait()
|
|
self.process = None
|