Files
2026-01-23 06:12:24 +01:00

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