udhcpc.py 5.3 KB

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