]>
git.ipfire.org Git - ipfire-2.x.git/blob - config/unbound/unbound-dhcp-leases-bridge
2 ###############################################################################
4 # IPFire.org - A linux based firewall #
5 # Copyright (C) 2016 Michael Tremer #
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. #
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. #
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/>. #
20 ###############################################################################
26 import logging
.handlers
31 import inotify
.adapters
35 def setup_logging(loglevel
=logging
.INFO
):
36 log
= logging
.getLogger("dhcp")
37 log
.setLevel(loglevel
)
39 handler
= logging
.handlers
.SysLogHandler(address
="/dev/log", facility
="daemon")
40 handler
.setLevel(loglevel
)
42 formatter
= logging
.Formatter("%(name)s[%(process)d]: %(message)s")
43 handler
.setFormatter(formatter
)
45 log
.addHandler(handler
)
49 log
= logging
.getLogger("dhcp")
51 class UnboundDHCPLeasesBridge(object):
52 def __init__(self
, dhcp_leases_file
, unbound_leases_file
):
53 self
.leases_file
= dhcp_leases_file
55 self
.unbound
= UnboundConfigWriter(unbound_leases_file
)
59 log
.info("Unbound DHCP Leases Bridge started on %s" % self
.leases_file
)
62 # Initially read leases file
63 self
.update_dhcp_leases()
65 i
= inotify
.adapters
.Inotify([self
.leases_file
])
67 for event
in i
.event_gen():
68 # End if we are requested to terminate
75 header
, type_names
, watch_path
, filename
= event
77 # Update leases after leases file has been modified
78 if "IN_MODIFY" in type_names
:
79 self
.update_dhcp_leases()
81 log
.info("Unbound DHCP Leases Bridge terminated")
83 def update_dhcp_leases(self
):
84 log
.info("Reading DHCP leases from %s" % self
.leases_file
)
86 leases
= DHCPLeases(self
.leases_file
)
87 self
.unbound
.update_dhcp_leases(leases
)
93 class DHCPLeases(object):
94 regex_leaseblock
= re
.compile(r
"lease (?P<ipaddr>\d+\.\d+\.\d+\.\d+) {(?P<config>[\s\S]+?)\n}")
96 def __init__(self
, path
):
99 self
._leases
= self
._parse
()
102 return iter(self
._leases
)
107 with
open(self
.path
) as f
:
108 # Read entire leases file
111 for match
in self
.regex_leaseblock
.finditer(data
):
112 block
= match
.groupdict()
114 ipaddr
= block
.get("ipaddr")
115 config
= block
.get("config")
117 properties
= self
._parse
_block
(config
)
119 # Skip any abandoned leases
120 if not "hardware" in properties
:
123 lease
= Lease(ipaddr
, properties
)
125 # Check if a lease for this Ethernet address already
126 # exists in the list of known leases. If so replace
127 # if with the most recent lease
128 for i
, l
in enumerate(leases
):
129 if l
.hwaddr
== lease
.hwaddr
:
130 leases
[i
] = max(lease
, l
)
138 def _parse_block(self
, block
):
141 for line
in block
.splitlines():
145 # Remove trailing ; from line
146 if line
.endswith(";"):
149 # Invalid line if it doesn't end with ;
153 # Remove any leading whitespace
156 # We skip all options and sets
157 if line
.startswith("option") or line
.startswith("set"):
160 # Split by first space
161 key
, val
= line
.split(" ", 1)
162 properties
[key
] = val
168 def __init__(self
, ipaddr
, properties
):
170 self
._properties
= properties
173 return "<%s %s for %s (%s)>" % (self
.__class
__.__name
__,
174 self
.ipaddr
, self
.hwaddr
, self
.hostname
)
176 def __eq__(self
, other
):
177 return self
.ipaddr
== other
.ipaddr
and self
.hwaddr
== other
.hwaddr
179 def __gt__(self
, other
):
180 if not self
.ipaddr
== other
.ipaddr
:
183 if not self
.hwaddr
== other
.hwaddr
:
186 return self
.time_starts
> other
.time_starts
189 def binding_state(self
):
190 state
= self
._properties
.get("binding")
193 state
= state
.split(" ", 1)
198 return self
.binding_state
== "active"
202 hardware
= self
._properties
.get("hardware")
207 ethernet
, address
= hardware
.split(" ", 1)
213 hostname
= self
._properties
.get("client-hostname")
217 hostname
= hostname
.replace("\"", "")
227 return "%s.%s" % (self
.hostname
, self
.domain
)
231 return datetime
.datetime
.strptime(s
, "%w %Y/%m/%d %H:%M:%S")
234 def time_starts(self
):
235 starts
= self
._properties
.get("starts")
238 return self
._parse
_time
(starts
)
242 ends
= self
._properties
.get("ends")
244 if not ends
or ends
== "never":
247 return self
._parse
_time
(ends
)
251 if not self
.time_ends
:
252 return self
.time_starts
> datetime
.datetime
.utcnow()
254 return self
.time_starts
> datetime
.datetime
.utcnow() > self
.time_ends
260 (self
.fqdn
, LOCAL_TTL
, "IN A", self
.ipaddr
),
263 (self
.ipaddr
, LOCAL_TTL
, "IN PTR", self
.fqdn
),
267 class UnboundConfigWriter(object):
268 def __init__(self
, path
):
271 self
._cached
_leases
= []
273 def update_dhcp_leases(self
, leases
):
274 # Strip all non-active or expired leases
275 leases
= [l
for l
in leases
if l
.active
and not l
.expired
]
277 # Find any leases that have expired or do not exist any more
278 removed_leases
= [l
for l
in self
._cached
_leases
if l
.expired
or l
not in leases
]
280 # Find any leases that have been added
281 new_leases
= [l
for l
in leases
if l
not in self
._cached
_leases
]
283 # End here if nothing has changed
284 if not new_leases
and not removed_leases
:
287 self
._cached
_leases
= leases
289 # Write out all leases
290 self
.write_dhcp_leases(leases
)
292 # Update unbound about changes
293 for l
in removed_leases
:
294 self
._control
("local_data_remove", l
.fqdn
)
298 self
._control
("local_data", *rr
)
301 def write_dhcp_leases(self
, leases
):
302 with
open(self
.path
, "w") as f
:
305 f
.write("local-data: \"%s\"\n" % " ".join(rr
))
307 def _control(self
, *args
):
308 command
= ["unbound-control", "-q"]
312 subprocess
.check_call(command
)
315 except subprocess
.CalledProcessError
as e
:
316 log
.critical("Could not run %s, error code: %s: %s" % (
317 " ".join(command
), e
.returncode
, e
.output
))
320 if __name__
== "__main__":
321 parser
= argparse
.ArgumentParser(description
="Bridge for DHCP Leases and Unbound DNS")
324 parser
.add_argument("--daemon", "-d", action
="store_true",
325 help="Launch as daemon in background")
326 parser
.add_argument("--verbose", "-v", action
="count", help="Be more verbose")
329 parser
.add_argument("--dhcp-leases", default
="/var/state/dhcp/dhcpd.leases",
330 metavar
="PATH", help="Path to the DHCPd leases file")
331 parser
.add_argument("--unbound-leases", default
="/etc/unbound/dhcp-leases.conf",
332 metavar
="PATH", help="Path to the unbound configuration file")
334 # Parse command line arguments
335 args
= parser
.parse_args()
338 if args
.verbose
== 1:
339 loglevel
= logging
.INFO
340 elif args
.verbose
>= 2:
341 loglevel
= logging
.DEBUG
343 loglevel
= logging
.WARN
345 setup_logging(loglevel
)
347 bridge
= UnboundDHCPLeasesBridge(args
.dhcp_leases
, args
.unbound_leases
)
349 ctx
= daemon
.DaemonContext(detach_process
=args
.daemon
)
351 signal
.SIGHUP
: bridge
.update_dhcp_leases
,
352 signal
.SIGTERM
: bridge
.terminate
,