udhcpc.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. from enum import Enum
  2. import ipaddress
  3. import json
  4. import struct
  5. import binascii
  6. import os
  7. from os import path
  8. from select import select
  9. import threading
  10. import subprocess
  11. import signal
  12. import time
  13. import logging
  14. from eventfd import EventFD
  15. import posix_ipc
  16. HANDLER_SCRIPT = path.join(path.dirname(__file__), 'udhcpc_handler.py')
  17. AWAIT_INTERVAL = 0.1
  18. VENDOR_ID = 'docker'
  19. class EventType(Enum):
  20. BOUND = 'bound'
  21. RENEW = 'renew'
  22. DECONFIG = 'deconfig'
  23. LEASEFAIL = 'leasefail'
  24. logger = logging.getLogger('net-dhcp')
  25. class DHCPClientError(Exception):
  26. pass
  27. def _nspopen_wrapper(netns):
  28. return lambda cmd, *args, **kwargs: subprocess.Popen(['nsenter', f'-n{netns}', '--'] + cmd, *args, **kwargs)
  29. class DHCPClient:
  30. def __init__(self, iface, v6=False, once=False, hostname=None, event_listener=None):
  31. self.iface = iface
  32. self.v6 = v6
  33. self.once = once
  34. self.event_listeners = [DHCPClient._attr_listener]
  35. if event_listener:
  36. self.event_listeners.append(event_listener)
  37. self.netns = None
  38. if isinstance(iface, dict):
  39. self.netns = iface['netns']
  40. logger.debug('udhcpc using netns %s', self.netns)
  41. Popen = _nspopen_wrapper(self.netns) if self.netns else subprocess.Popen
  42. bin_path = '/usr/bin/udhcpc6' if v6 else '/sbin/udhcpc'
  43. cmdline = [bin_path, '-s', HANDLER_SCRIPT, '-i', iface['ifname'], '-f']
  44. cmdline.append('-q' if once else '-R')
  45. if hostname:
  46. cmdline.append('-x')
  47. if v6:
  48. # TODO: We encode the fqdn for DHCPv6 because udhcpc6 seems to be broken
  49. # flags: S bit set (see RFC4704)
  50. enc_hostname = hostname.encode('utf-8')
  51. enc_hostname = struct.pack('BB', 0b0001, len(enc_hostname)) + enc_hostname
  52. enc_hostname = binascii.hexlify(enc_hostname).decode('ascii')
  53. hostname_opt = f'0x27:{enc_hostname}'
  54. else:
  55. hostname_opt = f'hostname:{hostname}'
  56. cmdline.append(hostname_opt)
  57. if not v6:
  58. cmdline += ['-V', VENDOR_ID]
  59. self._suffix = '6' if v6 else ''
  60. self._event_queue = posix_ipc.MessageQueue(f'/udhcpc{self._suffix}_{iface["address"].replace(":", "_")}', \
  61. flags=os.O_CREAT | os.O_EXCL, max_messages=2, max_message_size=1024)
  62. self.proc = Popen(cmdline, env={'EVENT_QUEUE': self._event_queue.name}, stdin=subprocess.DEVNULL,
  63. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  64. if hostname:
  65. logger.debug('[udhcpc%s#%d] using hostname "%s"', self._suffix, self.proc.pid, hostname)
  66. self._has_lease = threading.Event()
  67. self.ip = None
  68. self.gateway = None
  69. self.domain = None
  70. self._shutdown_event = EventFD()
  71. self.shutdown = False
  72. self._event_thread = threading.Thread(target=self._read_events)
  73. self._event_thread.start()
  74. def _attr_listener(self, event_type, event):
  75. if event_type in (EventType.BOUND, EventType.RENEW):
  76. self.ip = ipaddress.ip_interface(event['ip'])
  77. if 'gateway' in event:
  78. self.gateway = ipaddress.ip_address(event['gateway'])
  79. else:
  80. self.gateway = None
  81. self.domain = event.get('domain')
  82. self._has_lease.set()
  83. elif event_type == EventType.DECONFIG:
  84. self._has_lease.clear()
  85. self.ip = None
  86. self.gateway = None
  87. self.domain = None
  88. def _read_events(self):
  89. while True:
  90. r, _w, _e = select([self._shutdown_event, self._event_queue.mqd], [], [])
  91. if self._shutdown_event in r:
  92. break
  93. msg, _priority = self._event_queue.receive()
  94. event = json.loads(msg.decode('utf-8'))
  95. try:
  96. event['type'] = EventType(event['type'])
  97. except ValueError:
  98. logger.warning('udhcpc%s#%d unknown event "%s"', self._suffix, self.proc.pid, event)
  99. continue
  100. logger.debug('[udhcp%s#%d event] %s', self._suffix, self.proc.pid, event)
  101. for listener in self.event_listeners:
  102. try:
  103. listener(self, event['type'], event)
  104. except Exception as ex:
  105. logger.exception(ex)
  106. self.shutdown = True
  107. del self._shutdown_event
  108. def await_ip(self, timeout=10):
  109. if not self._has_lease.wait(timeout=timeout):
  110. raise DHCPClientError(f'Timed out waiting for lease from udhcpc{self._suffix}')
  111. return self.ip
  112. def finish(self, timeout=5):
  113. if self.shutdown or self._shutdown_event.is_set():
  114. return
  115. try:
  116. if self.proc.returncode is not None and (not self.once or self.proc.returncode != 0):
  117. raise DHCPClientError(f'udhcpc{self._suffix} exited early with code {self.proc.returncode}')
  118. if self.once:
  119. self.await_ip()
  120. else:
  121. os.kill(self.proc.pid, signal.SIGUSR2)
  122. time.sleep(0.05)
  123. self.proc.terminate()
  124. if self.proc.wait(timeout=timeout) != 0:
  125. raise DHCPClientError(f'udhcpc{self._suffix} exited with non-zero exit code {self.proc.returncode}')
  126. return self.ip
  127. finally:
  128. self._shutdown_event.set()
  129. self._event_thread.join()
  130. self._event_queue.close()
  131. self._event_queue.unlink()