]>
Commit | Line | Data |
---|---|---|
a8b1843b | 1 | #!/usr/bin/python3 |
0fbd7c3c MT |
2 | ############################################################################### |
3 | # # | |
4 | # IPFire.org - A linux based firewall # | |
5 | # Copyright (C) 2016 Michael Tremer # | |
6 | # # | |
7 | # This program is free software: you can redistribute it and/or modify # | |
8 | # it under the terms of the GNU General Public License as published by # | |
9 | # the Free Software Foundation, either version 3 of the License, or # | |
10 | # (at your option) any later version. # | |
11 | # # | |
12 | # This program is distributed in the hope that it will be useful, # | |
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | |
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # | |
15 | # GNU General Public License for more details. # | |
16 | # # | |
17 | # You should have received a copy of the GNU General Public License # | |
18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. # | |
19 | # # | |
20 | ############################################################################### | |
21 | ||
22 | import argparse | |
23 | import datetime | |
24 | import daemon | |
749bf859 | 25 | import filecmp |
091cb62a | 26 | import functools |
74a5ab67 | 27 | import ipaddress |
0fbd7c3c MT |
28 | import logging |
29 | import logging.handlers | |
b666975e | 30 | import os |
0fbd7c3c MT |
31 | import re |
32 | import signal | |
7be4822f | 33 | import stat |
0fbd7c3c | 34 | import subprocess |
29a36036 | 35 | import sys |
b666975e | 36 | import tempfile |
6d6e7949 | 37 | import time |
0fbd7c3c MT |
38 | |
39 | import inotify.adapters | |
40 | ||
077ea717 MT |
41 | LOCAL_TTL = 60 |
42 | ||
29a36036 MT |
43 | log = logging.getLogger("dhcp") |
44 | log.setLevel(logging.DEBUG) | |
45 | ||
46 | def setup_logging(daemon=True, loglevel=logging.INFO): | |
0fbd7c3c MT |
47 | log.setLevel(loglevel) |
48 | ||
29a36036 | 49 | # Log to syslog by default |
0fbd7c3c | 50 | handler = logging.handlers.SysLogHandler(address="/dev/log", facility="daemon") |
29a36036 | 51 | log.addHandler(handler) |
0fbd7c3c | 52 | |
29a36036 | 53 | # Format everything |
0fbd7c3c MT |
54 | formatter = logging.Formatter("%(name)s[%(process)d]: %(message)s") |
55 | handler.setFormatter(formatter) | |
56 | ||
29a36036 | 57 | handler.setLevel(loglevel) |
0fbd7c3c | 58 | |
29a36036 MT |
59 | # If we are running in foreground, we should write everything to the console, too |
60 | if not daemon: | |
61 | handler = logging.StreamHandler() | |
62 | log.addHandler(handler) | |
0fbd7c3c | 63 | |
29a36036 MT |
64 | handler.setLevel(loglevel) |
65 | ||
66 | return log | |
0fbd7c3c | 67 | |
e22bcd38 MT |
68 | def ip_address_to_reverse_pointer(address): |
69 | parts = address.split(".") | |
70 | parts.reverse() | |
71 | ||
72 | return "%s.in-addr.arpa" % ".".join(parts) | |
73 | ||
74 | def reverse_pointer_to_ip_address(rr): | |
75 | parts = rr.split(".") | |
76 | ||
77 | # Only take IP address part | |
78 | parts = reversed(parts[0:4]) | |
79 | ||
80 | return ".".join(parts) | |
81 | ||
0fbd7c3c | 82 | class UnboundDHCPLeasesBridge(object): |
7354d294 | 83 | def __init__(self, dhcp_leases_file, fix_leases_file, unbound_leases_file, hosts_file): |
0fbd7c3c | 84 | self.leases_file = dhcp_leases_file |
86c9deb2 | 85 | self.fix_leases_file = fix_leases_file |
7354d294 | 86 | self.hosts_file = hosts_file |
0fbd7c3c | 87 | |
6d6e7949 MT |
88 | self.watches = { |
89 | self.leases_file : inotify.constants.IN_MODIFY, | |
90 | self.fix_leases_file : 0, | |
91 | self.hosts_file : 0, | |
92 | } | |
93 | ||
0fbd7c3c MT |
94 | self.unbound = UnboundConfigWriter(unbound_leases_file) |
95 | self.running = False | |
96 | ||
97 | def run(self): | |
98 | log.info("Unbound DHCP Leases Bridge started on %s" % self.leases_file) | |
99 | self.running = True | |
100 | ||
6d6e7949 MT |
101 | i = inotify.adapters.Inotify() |
102 | ||
103 | # Add watches for the directories of every relevant file | |
104 | for f, mask in self.watches.items(): | |
105 | i.add_watch( | |
106 | os.path.dirname(f), | |
107 | mask | inotify.constants.IN_CLOSE_WRITE | inotify.constants.IN_MOVED_TO, | |
108 | ) | |
109 | ||
110 | # Enabled so that we update hosts and leases on startup | |
111 | update_hosts = update_leases = True | |
112 | ||
113 | while self.running: | |
114 | log.debug("Wakeup of main loop") | |
0fbd7c3c | 115 | |
6d6e7949 MT |
116 | # Process the entire inotify queue and identify what we need to do |
117 | for event in i.event_gen(): | |
118 | # Nothing to do | |
119 | if event is None: | |
120 | break | |
0fbd7c3c | 121 | |
6d6e7949 MT |
122 | # Decode the event |
123 | header, type_names, path, filename = event | |
0fbd7c3c | 124 | |
6d6e7949 | 125 | file = os.path.join(path, filename) |
0fbd7c3c | 126 | |
6d6e7949 | 127 | log.debug("inotify event received for %s: %s", file, " ".join(type_names)) |
0fbd7c3c | 128 | |
6d6e7949 MT |
129 | # Did the hosts file change? |
130 | if self.hosts_file == file: | |
131 | update_hosts = True | |
7354d294 | 132 | |
6d6e7949 MT |
133 | # We will need to update the leases on any change |
134 | update_leases = True | |
135 | ||
136 | # Update hosts (if needed) | |
137 | if update_hosts: | |
138 | self.hosts = self.read_static_hosts() | |
139 | ||
140 | # Update leases (if needed) | |
141 | if update_leases: | |
0fbd7c3c MT |
142 | self.update_dhcp_leases() |
143 | ||
6d6e7949 MT |
144 | # Reset |
145 | update_hosts = update_leases = False | |
146 | ||
147 | # Wait a moment before we start the next iteration | |
148 | time.sleep(5) | |
86c9deb2 | 149 | |
0fbd7c3c MT |
150 | log.info("Unbound DHCP Leases Bridge terminated") |
151 | ||
152 | def update_dhcp_leases(self): | |
86c9deb2 MT |
153 | leases = [] |
154 | ||
155 | for lease in DHCPLeases(self.leases_file): | |
5d4f3a42 MT |
156 | # Don't bother with any leases that don't have a hostname |
157 | if not lease.fqdn: | |
158 | continue | |
159 | ||
86c9deb2 MT |
160 | leases.append(lease) |
161 | ||
162 | for lease in FixLeases(self.fix_leases_file): | |
163 | leases.append(lease) | |
0fbd7c3c | 164 | |
c7b83f9b MT |
165 | # Skip any leases that also are a static host |
166 | leases = [l for l in leases if not l.fqdn in self.hosts] | |
167 | ||
168 | # Remove any inactive or expired leases | |
169 | leases = [l for l in leases if l.active and not l.expired] | |
170 | ||
171 | # Dump leases | |
172 | if leases: | |
173 | log.debug("DHCP Leases:") | |
174 | for lease in leases: | |
175 | log.debug(" %s:" % lease.fqdn) | |
176 | log.debug(" State: %s" % lease.binding_state) | |
177 | log.debug(" Start: %s" % lease.time_starts) | |
178 | log.debug(" End : %s" % lease.time_ends) | |
179 | if lease.expired: | |
180 | log.debug(" Expired") | |
181 | ||
0fbd7c3c MT |
182 | self.unbound.update_dhcp_leases(leases) |
183 | ||
7354d294 MT |
184 | def read_static_hosts(self): |
185 | log.info("Reading static hosts from %s" % self.hosts_file) | |
186 | ||
187 | hosts = {} | |
188 | with open(self.hosts_file) as f: | |
189 | for line in f.readlines(): | |
190 | line = line.rstrip() | |
191 | ||
192 | try: | |
4a465756 | 193 | enabled, ipaddr, hostname, domainname, generateptr = line.split(",") |
7354d294 MT |
194 | except: |
195 | log.warning("Could not parse line: %s" % line) | |
196 | continue | |
197 | ||
198 | # Skip any disabled entries | |
199 | if not enabled == "on": | |
200 | continue | |
201 | ||
202 | if hostname and domainname: | |
203 | fqdn = "%s.%s" % (hostname, domainname) | |
204 | elif hostname: | |
205 | fqdn = hostname | |
206 | elif domainname: | |
207 | fqdn = domainname | |
208 | ||
209 | try: | |
210 | hosts[fqdn].append(ipaddr) | |
211 | hosts[fqdn].sort() | |
212 | except KeyError: | |
213 | hosts[fqdn] = [ipaddr,] | |
214 | ||
215 | # Dump everything in the logs | |
216 | log.debug("Static hosts:") | |
96c86cf6 MT |
217 | for name in hosts: |
218 | log.debug(" %-20s : %s" % (name, ", ".join(hosts[name]))) | |
7354d294 MT |
219 | |
220 | return hosts | |
221 | ||
0fbd7c3c MT |
222 | def terminate(self): |
223 | self.running = False | |
224 | ||
225 | ||
226 | class DHCPLeases(object): | |
227 | regex_leaseblock = re.compile(r"lease (?P<ipaddr>\d+\.\d+\.\d+\.\d+) {(?P<config>[\s\S]+?)\n}") | |
228 | ||
229 | def __init__(self, path): | |
230 | self.path = path | |
231 | ||
232 | self._leases = self._parse() | |
233 | ||
234 | def __iter__(self): | |
235 | return iter(self._leases) | |
236 | ||
237 | def _parse(self): | |
86c9deb2 MT |
238 | log.info("Reading DHCP leases from %s" % self.path) |
239 | ||
0fbd7c3c MT |
240 | leases = [] |
241 | ||
242 | with open(self.path) as f: | |
243 | # Read entire leases file | |
244 | data = f.read() | |
245 | ||
246 | for match in self.regex_leaseblock.finditer(data): | |
247 | block = match.groupdict() | |
248 | ||
249 | ipaddr = block.get("ipaddr") | |
250 | config = block.get("config") | |
251 | ||
252 | properties = self._parse_block(config) | |
253 | ||
254 | # Skip any abandoned leases | |
255 | if not "hardware" in properties: | |
256 | continue | |
257 | ||
258 | lease = Lease(ipaddr, properties) | |
259 | ||
260 | # Check if a lease for this Ethernet address already | |
261 | # exists in the list of known leases. If so replace | |
262 | # if with the most recent lease | |
263 | for i, l in enumerate(leases): | |
8b1eb795 | 264 | if l.ipaddr == lease.ipaddr: |
0fbd7c3c MT |
265 | leases[i] = max(lease, l) |
266 | break | |
267 | ||
268 | else: | |
269 | leases.append(lease) | |
270 | ||
271 | return leases | |
272 | ||
273 | def _parse_block(self, block): | |
274 | properties = {} | |
275 | ||
276 | for line in block.splitlines(): | |
277 | if not line: | |
278 | continue | |
279 | ||
280 | # Remove trailing ; from line | |
281 | if line.endswith(";"): | |
282 | line = line[:-1] | |
283 | ||
284 | # Invalid line if it doesn't end with ; | |
285 | else: | |
286 | continue | |
287 | ||
288 | # Remove any leading whitespace | |
289 | line = line.lstrip() | |
290 | ||
291 | # We skip all options and sets | |
292 | if line.startswith("option") or line.startswith("set"): | |
293 | continue | |
294 | ||
295 | # Split by first space | |
296 | key, val = line.split(" ", 1) | |
297 | properties[key] = val | |
298 | ||
299 | return properties | |
300 | ||
301 | ||
86c9deb2 MT |
302 | class FixLeases(object): |
303 | cache = {} | |
304 | ||
305 | def __init__(self, path): | |
306 | self.path = path | |
307 | ||
308 | self._leases = self.cache[self.path] = self._parse() | |
309 | ||
310 | def __iter__(self): | |
311 | return iter(self._leases) | |
312 | ||
313 | def _parse(self): | |
314 | log.info("Reading fix leases from %s" % self.path) | |
315 | ||
316 | leases = [] | |
317 | now = datetime.datetime.utcnow() | |
318 | ||
319 | with open(self.path) as f: | |
320 | for line in f.readlines(): | |
321 | line = line.rstrip() | |
322 | ||
323 | try: | |
324 | hwaddr, ipaddr, enabled, a, b, c, hostname = line.split(",") | |
325 | except ValueError: | |
326 | log.warning("Could not parse line: %s" % line) | |
327 | continue | |
328 | ||
329 | # Skip any disabled leases | |
330 | if not enabled == "on": | |
331 | continue | |
332 | ||
333 | l = Lease(ipaddr, { | |
334 | "binding" : "state active", | |
335 | "client-hostname" : hostname, | |
336 | "hardware" : "ethernet %s" % hwaddr, | |
337 | "starts" : now.strftime("%w %Y/%m/%d %H:%M:%S"), | |
338 | "ends" : "never", | |
339 | }) | |
340 | leases.append(l) | |
341 | ||
342 | # Try finding any deleted leases | |
343 | for lease in self.cache.get(self.path, []): | |
344 | if lease in leases: | |
345 | continue | |
346 | ||
347 | # Free the deleted lease | |
348 | lease.free() | |
349 | leases.append(lease) | |
350 | ||
351 | return leases | |
352 | ||
353 | ||
0fbd7c3c MT |
354 | class Lease(object): |
355 | def __init__(self, ipaddr, properties): | |
356 | self.ipaddr = ipaddr | |
357 | self._properties = properties | |
358 | ||
359 | def __repr__(self): | |
360 | return "<%s %s for %s (%s)>" % (self.__class__.__name__, | |
361 | self.ipaddr, self.hwaddr, self.hostname) | |
362 | ||
363 | def __eq__(self, other): | |
364 | return self.ipaddr == other.ipaddr and self.hwaddr == other.hwaddr | |
365 | ||
366 | def __gt__(self, other): | |
367 | if not self.ipaddr == other.ipaddr: | |
368 | return | |
369 | ||
370 | if not self.hwaddr == other.hwaddr: | |
371 | return | |
372 | ||
373 | return self.time_starts > other.time_starts | |
374 | ||
375 | @property | |
376 | def binding_state(self): | |
377 | state = self._properties.get("binding") | |
378 | ||
379 | if state: | |
380 | state = state.split(" ", 1) | |
381 | return state[1] | |
382 | ||
86c9deb2 MT |
383 | def free(self): |
384 | self._properties.update({ | |
385 | "binding" : "state free", | |
386 | }) | |
387 | ||
0fbd7c3c MT |
388 | @property |
389 | def active(self): | |
390 | return self.binding_state == "active" | |
391 | ||
392 | @property | |
393 | def hwaddr(self): | |
394 | hardware = self._properties.get("hardware") | |
395 | ||
396 | if not hardware: | |
397 | return | |
398 | ||
399 | ethernet, address = hardware.split(" ", 1) | |
400 | ||
401 | return address | |
402 | ||
403 | @property | |
404 | def hostname(self): | |
405 | hostname = self._properties.get("client-hostname") | |
406 | ||
998e880b MT |
407 | if hostname is None: |
408 | return | |
409 | ||
0fbd7c3c | 410 | # Remove any "" |
998e880b | 411 | hostname = hostname.replace("\"", "") |
0fbd7c3c | 412 | |
998e880b MT |
413 | # Only return valid hostnames |
414 | m = re.match(r"^[A-Z0-9\-]{1,63}$", hostname, re.I) | |
415 | if m: | |
416 | return hostname | |
0fbd7c3c MT |
417 | |
418 | @property | |
419 | def domain(self): | |
74a5ab67 MT |
420 | # Load ethernet settings |
421 | ethernet_settings = self.read_settings("/var/ipfire/ethernet/settings") | |
422 | ||
423 | # Load DHCP settings | |
424 | dhcp_settings = self.read_settings("/var/ipfire/dhcp/settings") | |
425 | ||
426 | subnets = {} | |
427 | for zone in ("GREEN", "BLUE"): | |
428 | if not dhcp_settings.get("ENABLE_%s" % zone) == "on": | |
429 | continue | |
430 | ||
431 | netaddr = ethernet_settings.get("%s_NETADDRESS" % zone) | |
432 | submask = ethernet_settings.get("%s_NETMASK" % zone) | |
433 | ||
434 | subnet = ipaddress.ip_network("%s/%s" % (netaddr, submask)) | |
435 | domain = dhcp_settings.get("DOMAIN_NAME_%s" % zone) | |
436 | ||
437 | subnets[subnet] = domain | |
438 | ||
439 | address = ipaddress.ip_address(self.ipaddr) | |
440 | ||
96c86cf6 | 441 | for subnet in subnets: |
74a5ab67 | 442 | if address in subnet: |
96c86cf6 | 443 | return subnets[subnet] |
74a5ab67 | 444 | |
ca4de263 MT |
445 | # Load main settings |
446 | settings = self.read_settings("/var/ipfire/main/settings") | |
447 | ||
448 | # Fall back to the host domain if no match could be found | |
449 | return settings.get("DOMAINNAME", "localdomain") | |
74a5ab67 MT |
450 | |
451 | @staticmethod | |
091cb62a | 452 | @functools.cache |
74a5ab67 MT |
453 | def read_settings(filename): |
454 | settings = {} | |
455 | ||
456 | with open(filename) as f: | |
457 | for line in f.readlines(): | |
458 | # Remove line-breaks | |
459 | line = line.rstrip() | |
460 | ||
461 | k, v = line.split("=", 1) | |
462 | settings[k] = v | |
463 | ||
464 | return settings | |
0fbd7c3c MT |
465 | |
466 | @property | |
467 | def fqdn(self): | |
998e880b MT |
468 | if self.hostname: |
469 | return "%s.%s" % (self.hostname, self.domain) | |
0fbd7c3c MT |
470 | |
471 | @staticmethod | |
472 | def _parse_time(s): | |
473 | return datetime.datetime.strptime(s, "%w %Y/%m/%d %H:%M:%S") | |
474 | ||
475 | @property | |
476 | def time_starts(self): | |
477 | starts = self._properties.get("starts") | |
478 | ||
479 | if starts: | |
480 | return self._parse_time(starts) | |
481 | ||
482 | @property | |
483 | def time_ends(self): | |
484 | ends = self._properties.get("ends") | |
485 | ||
486 | if not ends or ends == "never": | |
487 | return | |
488 | ||
489 | return self._parse_time(ends) | |
490 | ||
491 | @property | |
492 | def expired(self): | |
493 | if not self.time_ends: | |
494 | return self.time_starts > datetime.datetime.utcnow() | |
495 | ||
496 | return self.time_starts > datetime.datetime.utcnow() > self.time_ends | |
497 | ||
498 | @property | |
499 | def rrset(self): | |
998e880b MT |
500 | # If the lease does not have a valid FQDN, we cannot create any RRs |
501 | if self.fqdn is None: | |
502 | return [] | |
503 | ||
0fbd7c3c MT |
504 | return [ |
505 | # Forward record | |
b8dd42b9 | 506 | (self.fqdn, "%s" % LOCAL_TTL, "IN A", self.ipaddr), |
0fbd7c3c MT |
507 | |
508 | # Reverse record | |
e22bcd38 MT |
509 | (ip_address_to_reverse_pointer(self.ipaddr), "%s" % LOCAL_TTL, |
510 | "IN PTR", self.fqdn), | |
0fbd7c3c MT |
511 | ] |
512 | ||
513 | ||
514 | class UnboundConfigWriter(object): | |
515 | def __init__(self, path): | |
516 | self.path = path | |
517 | ||
0fbd7c3c | 518 | def update_dhcp_leases(self, leases): |
0fbd7c3c | 519 | # Write out all leases |
749bf859 MT |
520 | if self.write_dhcp_leases(leases): |
521 | log.debug("Reloading Unbound...") | |
0fbd7c3c | 522 | |
749bf859 MT |
523 | # Reload the configuration without dropping the cache |
524 | self._control("reload_keep_cache") | |
0fbd7c3c MT |
525 | |
526 | def write_dhcp_leases(self, leases): | |
f20ca78e | 527 | log.debug("Writing DHCP leases...") |
b666975e | 528 | |
c3cabfa0 | 529 | with tempfile.NamedTemporaryFile(mode="w") as f: |
749bf859 | 530 | for l in sorted(leases, key=lambda x: x.ipaddr): |
0fbd7c3c MT |
531 | for rr in l.rrset: |
532 | f.write("local-data: \"%s\"\n" % " ".join(rr)) | |
533 | ||
c3cabfa0 MT |
534 | # Flush the file |
535 | f.flush() | |
536 | ||
749bf859 | 537 | # Compare if the new leases file has changed from the previous version |
4bf50efa MT |
538 | try: |
539 | if filecmp.cmp(f.name, self.path, shallow=False): | |
540 | log.debug("The generated leases file has not changed") | |
749bf859 | 541 | |
4bf50efa MT |
542 | return False |
543 | ||
544 | # Remove the old file | |
545 | os.unlink(self.path) | |
546 | ||
547 | # If the previous file did not exist, just keep falling through | |
548 | except FileNotFoundError: | |
549 | pass | |
749bf859 | 550 | |
7be4822f MT |
551 | # Make file readable for everyone |
552 | os.fchmod(f.fileno(), stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IROTH) | |
553 | ||
f20ca78e | 554 | # Move the file to its destination |
c3cabfa0 | 555 | os.link(f.name, self.path) |
b666975e | 556 | |
749bf859 MT |
557 | return True |
558 | ||
0fbd7c3c | 559 | def _control(self, *args): |
b8dd42b9 | 560 | command = ["unbound-control"] |
0fbd7c3c MT |
561 | command.extend(args) |
562 | ||
563 | try: | |
3ec5ba50 | 564 | subprocess.check_output(command) |
0fbd7c3c MT |
565 | |
566 | # Log any errors | |
567 | except subprocess.CalledProcessError as e: | |
568 | log.critical("Could not run %s, error code: %s: %s" % ( | |
569 | " ".join(command), e.returncode, e.output)) | |
570 | ||
f20ca78e | 571 | raise e |
3ec5ba50 | 572 | |
0fbd7c3c MT |
573 | |
574 | if __name__ == "__main__": | |
575 | parser = argparse.ArgumentParser(description="Bridge for DHCP Leases and Unbound DNS") | |
576 | ||
577 | # Daemon Stuff | |
578 | parser.add_argument("--daemon", "-d", action="store_true", | |
579 | help="Launch as daemon in background") | |
580 | parser.add_argument("--verbose", "-v", action="count", help="Be more verbose") | |
581 | ||
582 | # Paths | |
583 | parser.add_argument("--dhcp-leases", default="/var/state/dhcp/dhcpd.leases", | |
584 | metavar="PATH", help="Path to the DHCPd leases file") | |
585 | parser.add_argument("--unbound-leases", default="/etc/unbound/dhcp-leases.conf", | |
586 | metavar="PATH", help="Path to the unbound configuration file") | |
86c9deb2 MT |
587 | parser.add_argument("--fix-leases", default="/var/ipfire/dhcp/fixleases", |
588 | metavar="PATH", help="Path to the fix leases file") | |
7354d294 MT |
589 | parser.add_argument("--hosts", default="/var/ipfire/main/hosts", |
590 | metavar="PATH", help="Path to static hosts file") | |
0fbd7c3c MT |
591 | |
592 | # Parse command line arguments | |
593 | args = parser.parse_args() | |
594 | ||
595 | # Setup logging | |
83e5f672 MT |
596 | loglevel = logging.WARN |
597 | ||
598 | if args.verbose: | |
599 | if args.verbose == 1: | |
600 | loglevel = logging.INFO | |
601 | elif args.verbose >= 2: | |
602 | loglevel = logging.DEBUG | |
0fbd7c3c | 603 | |
7354d294 MT |
604 | bridge = UnboundDHCPLeasesBridge(args.dhcp_leases, args.fix_leases, |
605 | args.unbound_leases, args.hosts) | |
0fbd7c3c | 606 | |
1918174d MT |
607 | with daemon.DaemonContext( |
608 | detach_process=args.daemon, | |
609 | stderr=None if args.daemon else sys.stderr, | |
610 | signal_map = { | |
611 | signal.SIGHUP : bridge.update_dhcp_leases, | |
612 | signal.SIGTERM : bridge.terminate, | |
613 | }, | |
614 | ) as daemon: | |
f9611355 MT |
615 | setup_logging(daemon=args.daemon, loglevel=loglevel) |
616 | ||
0fbd7c3c | 617 | bridge.run() |