Commit 4c8dde7d authored by Ad Schellevis's avatar Ad Schellevis

(legacy/unbound) refactor dhcp lease update

parent 22ed3586
......@@ -458,8 +458,7 @@ function system_dhcpleases_configure()
global $config, $g;
/* Start the monitoring process for dynamic dhcpclients. */
if ((isset($config['dnsmasq']['enable']) && isset($config['dnsmasq']['regdhcp']))
|| (isset($config['unbound']['enable']) && isset($config['unbound']['regdhcp']))) {
if (isset($config['dnsmasq']['enable']) && isset($config['dnsmasq']['regdhcp'])) {
/* Make sure we do not error out */
mwexec("/bin/mkdir -p {$g['dhcpd_chroot_path']}/var/db");
if (!file_exists("{$g['dhcpd_chroot_path']}/var/db/dhcpd.leases")) {
......
......@@ -340,9 +340,6 @@ include: {$g['unbound_chroot_path']}/host_entries.conf
# Domain overrides
include: {$g['unbound_chroot_path']}/domainoverrides.conf
# TODO: DHCP leases should be included here
##include: {$g['unbound_chroot_path']}/dhcpleases.conf
{$custom_options}
{$forward_conf}
......@@ -354,6 +351,11 @@ include: {$g['unbound_chroot_path']}/remotecontrol.conf
EOD;
if (isset($config['unbound']['regdhcp'])) {
// include dynamic leases
@touch("{$g['unbound_chroot_path']}/dhcpleases.conf");
$unboundconf .= "\ninclude: {$g['unbound_chroot_path']}/dhcpleases.conf\n"
}
file_put_contents("{$g['unbound_chroot_path']}/unbound.conf", $unboundconf);
return 0;
......@@ -378,7 +380,7 @@ EOF;
file_put_contents("{$g['unbound_chroot_path']}/remotecontrol.conf", $remotcfg);
// Generate our keys
do_as_unbound_user("unbound-control-setup");
execute_unbound_command("unbound-control-setup");
}
}
......@@ -418,25 +420,31 @@ function sync_unbound_service()
bootstrap_unbound_root();
// Configure our Unbound service
do_as_unbound_user("unbound-anchor");
execute_unbound_command("unbound-anchor");
unbound_remote_control_setup();
unbound_generate_config();
do_as_unbound_user("start");
execute_unbound_command("start");
if (is_process_running('unbound')) {
do_as_unbound_user("restore_cache");
execute_unbound_command("restore_cache");
}
}
// Execute commands as the user unbound
function do_as_unbound_user($cmd)
function execute_unbound_command($cmd)
{
global $g;
global $g, $config;
switch ($cmd) {
case 'start':
$local_domain = !empty($config['system']['domain']) ? $config['system']['domain'] : "local";
killbypid('/var/run/unbound_dhcpd.pid');
if (isset($config['unbound']['regdhcp'])) {
mwexec('/usr/local/opnsense/scripts/dns/unbound_dhcpd.py /domain "'.$local_domain.'"');
}
mwexec("/usr/local/sbin/unbound -c {$g['unbound_chroot_path']}/unbound.conf");
break;
case 'stop':
killbypid('/var/run/unbound_dhcpd.pid');
mwexec("chroot -u unbound -g unbound / /usr/local/sbin/unbound-control stop", true);
break;
case 'unbound-anchor':
......@@ -579,23 +587,6 @@ function unbound_add_host_entries() {
$unbound_entries .= $host_entries;
}
// Handle DHCPLeases added host entries
if (isset($config['unbound']['regdhcp'])) {
$dhcplcfg = read_hosts();
$host_entries = "";
if (is_array($dhcplcfg)) {
foreach($dhcplcfg as $key=>$host) {
$host_entries .= "local-data-ptr: \"{$host['ipaddr']} {$host['fqdn']}\"\n";
$host_entries .= "local-data: \"{$host['fqdn']} IN A {$host['ipaddr']}\"\n";
if (!empty($host['name'])) {
$host_entries .= "local-data-ptr: \"{$host['ipaddr']} {$host['name']}\"\n";
$host_entries .= "local-data: \"{$host['name']} IN A {$host['ipaddr']}\"\n";
}
}
$unbound_entries .= $host_entries;
}
}
// Write out entries
bootstrap_unbound_root();
file_put_contents("{$g['unbound_chroot_path']}/host_entries.conf", $unbound_entries);
......@@ -611,31 +602,31 @@ function unbound_control($action) {
// Start Unbound
if ($config['unbound']['enable'] == "on") {
if (!is_process_running("unbound")) {
do_as_unbound_user("start");
execute_unbound_command("start");
}
}
break;
case "stop":
if ($config['unbound']['enable'] == "on") {
do_as_unbound_user("stop");
execute_unbound_command("stop");
}
break;
case "reload":
if ($config['unbound']['enable'] == "on") {
do_as_unbound_user("reload");
execute_unbound_command("reload");
}
break;
case "dump_cache":
// Dump Unbound's Cache
if ($config['unbound']['dumpcache'] == "on") {
do_as_unbound_user("dump_cache");
execute_unbound_command("dump_cache");
}
break;
case "restore_cache":
// Restore Unbound's Cache
if ((is_process_running("unbound")) && ($config['unbound']['dumpcache'] == "on")) {
if (file_exists($cache_dumpfile) && filesize($cache_dumpfile) > 0) {
do_as_unbound_user("load_cache < /tmp/unbound_cache");
execute_unbound_command("load_cache < /tmp/unbound_cache");
}
}
break;
......
#!/usr/local/bin/python2.7
"""
Copyright (c) 2016 Ad Schellevis
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------------
watch dhcp lease file and build include file for unbound
"""
import os
import sys
import signal
sys.path.insert(0, "/usr/local/site-python")
import time
from daemonize import Daemonize
import watchers.dhcpd
import params
# parse input params
app_params={'pid': '/var/run/unbound_dhcpd.pid',
'domain': 'local',
'target': '/var/unbound/dhcpleases.conf',
'background':'1'}
params.update_params(app_params)
def main():
# cleanup interval (seconds)
cleanup_interval = 60
# initiate lease watcher and setup cache
dhcpdleases = watchers.dhcpd.DHCPDLease()
cached_leases = dict()
# start watching dhcp leases
last_cleanup = time.time()
while True:
dhcpd_changed = False
for lease in dhcpdleases.watch():
if 'ends' in lease and lease['ends'] > time.time() and 'client-hostname' in lease and 'address' in lease:
cached_leases[lease['address']] = lease
dhcpd_changed = True
if time.time() - last_cleanup > cleanup_interval:
# cleanup every x seconds
last_cleanup = time.time()
addresses = cached_leases.keys()
for address in addresses:
if cached_leases[address]['ends'] > time.time():
del cached_leases[address]
dhcpd_changed = True
if dhcpd_changed:
# dump dns output to target
with open(app_params['target'], 'w') as unbound_conf:
for address in cached_leases:
unbound_conf.write('local-data: "%s.%s IN A %s"\n' % (cached_leases[address]['client-hostname'],
app_params['domain'],
address))
unbound_conf.write("local-data-ptr: %s IN A %s.%s\n" % (address,
cached_leases[address]['client-hostname'],
app_params['domain']))
# wait for next cycle
time.sleep(1)
# startup
if app_params['background'] == '1':
daemon = Daemonize(app="unbound_dhcpd", pid=app_params['pid'], action=main)
daemon.start()
else:
main()
"""
Copyright (c) 2016 Ad Schellevis
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
Copyright (c) 2012, 2013, 2014 Ilya Otyutskiy <ilya.otyutskiy@icloud.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# #!/usr/bin/python
import fcntl
import os
import pwd
import grp
import sys
import signal
import resource
import logging
import atexit
from logging import handlers
__version__ = "2.4.3"
class Daemonize(object):
"""
Daemonize object.
Object constructor expects three arguments.
:param app: contains the application name which will be sent to syslog.
:param pid: path to the pidfile.
:param action: your custom function which will be executed after daemonization.
:param keep_fds: optional list of fds which should not be closed.
:param auto_close_fds: optional parameter to not close opened fds.
:param privileged_action: action that will be executed before drop privileges if user or
group parameter is provided.
If you want to transfer anything from privileged_action to action, such as
opened privileged file descriptor, you should return it from
privileged_action function and catch it inside action function.
:param user: drop privileges to this user if provided.
:param group: drop privileges to this group if provided.
:param verbose: send debug messages to logger if provided.
:param logger: use this logger object instead of creating new one, if provided.
:param foreground: stay in foreground; do not fork (for debugging)
:param chdir: change working directory if provided or /
"""
def __init__(self, app, pid, action,
keep_fds=None, auto_close_fds=True, privileged_action=None,
user=None, group=None, verbose=False, logger=None,
foreground=False, chdir="/"):
self.app = app
self.pid = pid
self.action = action
self.keep_fds = keep_fds or []
self.privileged_action = privileged_action or (lambda: ())
self.user = user
self.group = group
self.logger = logger
self.verbose = verbose
self.auto_close_fds = auto_close_fds
self.foreground = foreground
self.chdir = chdir
def sigterm(self, signum, frame):
"""
These actions will be done after SIGTERM.
"""
self.logger.warn("Caught signal %s. Stopping daemon." % signum)
sys.exit(0)
def exit(self):
"""
Cleanup pid file at exit.
"""
self.logger.warn("Stopping daemon.")
os.remove(self.pid)
sys.exit(0)
def start(self):
"""
Start daemonization process.
"""
# If pidfile already exists, we should read pid from there; to overwrite it, if locking
# will fail, because locking attempt somehow purges the file contents.
if os.path.isfile(self.pid):
with open(self.pid, "r") as old_pidfile:
old_pid = old_pidfile.read()
# Create a lockfile so that only one instance of this daemon is running at any time.
try:
lockfile = open(self.pid, "w")
except IOError:
print("Unable to create the pidfile.")
sys.exit(1)
try:
# Try to get an exclusive lock on the file. This will fail if another process has the file
# locked.
fcntl.flock(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
print("Unable to lock on the pidfile.")
# We need to overwrite the pidfile if we got here.
with open(self.pid, "w") as pidfile:
pidfile.write(old_pid)
sys.exit(1)
# skip fork if foreground is specified
if not self.foreground:
# Fork, creating a new process for the child.
process_id = os.fork()
if process_id < 0:
# Fork error. Exit badly.
sys.exit(1)
elif process_id != 0:
# This is the parent process. Exit.
sys.exit(0)
# This is the child process. Continue.
# Stop listening for signals that the parent process receives.
# This is done by getting a new process id.
# setpgrp() is an alternative to setsid().
# setsid puts the process in a new parent group and detaches its controlling terminal.
process_id = os.setsid()
if process_id == -1:
# Uh oh, there was a problem.
sys.exit(1)
# Add lockfile to self.keep_fds.
self.keep_fds.append(lockfile.fileno())
# Close all file descriptors, except the ones mentioned in self.keep_fds.
devnull = "/dev/null"
if hasattr(os, "devnull"):
# Python has set os.devnull on this system, use it instead as it might be different
# than /dev/null.
devnull = os.devnull
if self.auto_close_fds:
for fd in range(3, resource.getrlimit(resource.RLIMIT_NOFILE)[0]):
if fd not in self.keep_fds:
try:
os.close(fd)
except OSError:
pass
devnull_fd = os.open(devnull, os.O_RDWR)
os.dup2(devnull_fd, 0)
os.dup2(devnull_fd, 1)
os.dup2(devnull_fd, 2)
if self.logger is None:
# Initialize logging.
self.logger = logging.getLogger(self.app)
self.logger.setLevel(logging.DEBUG)
# Display log messages only on defined handlers.
self.logger.propagate = False
# Initialize syslog.
# It will correctly work on OS X, Linux and FreeBSD.
if sys.platform == "darwin":
syslog_address = "/var/run/syslog"
else:
syslog_address = "/dev/log"
# We will continue with syslog initialization only if actually have such capabilities
# on the machine we are running this.
if os.path.exists(syslog_address):
syslog = handlers.SysLogHandler(syslog_address)
if self.verbose:
syslog.setLevel(logging.DEBUG)
else:
syslog.setLevel(logging.INFO)
# Try to mimic to normal syslog messages.
formatter = logging.Formatter("%(asctime)s %(name)s: %(message)s",
"%b %e %H:%M:%S")
syslog.setFormatter(formatter)
self.logger.addHandler(syslog)
# Set umask to default to safe file permissions when running as a root daemon. 027 is an
# octal number which we are typing as 0o27 for Python3 compatibility.
os.umask(0o27)
# Change to a known directory. If this isn't done, starting a daemon in a subdirectory that
# needs to be deleted results in "directory busy" errors.
os.chdir(self.chdir)
# Execute privileged action
privileged_action_result = self.privileged_action()
if not privileged_action_result:
privileged_action_result = []
# Change owner of pid file, it's required because pid file will be removed at exit.
uid, gid = -1, -1
if self.group:
try:
gid = grp.getgrnam(self.group).gr_gid
except KeyError:
self.logger.error("Group {0} not found".format(self.group))
sys.exit(1)
if self.user:
try:
uid = pwd.getpwnam(self.user).pw_uid
except KeyError:
self.logger.error("User {0} not found.".format(self.user))
sys.exit(1)
if uid != -1 or gid != -1:
os.chown(self.pid, uid, gid)
# Change gid
if self.group:
try:
os.setgid(gid)
except OSError:
self.logger.error("Unable to change gid.")
sys.exit(1)
# Change uid
if self.user:
try:
uid = pwd.getpwnam(self.user).pw_uid
except KeyError:
self.logger.error("User {0} not found.".format(self.user))
sys.exit(1)
try:
os.setuid(uid)
except OSError:
self.logger.error("Unable to change uid.")
sys.exit(1)
try:
lockfile.write("%s" % (os.getpid()))
lockfile.flush()
except IOError:
self.logger.error("Unable to write pid to the pidfile.")
print("Unable to write pid to the pidfile.")
sys.exit(1)
# Set custom action on SIGTERM.
signal.signal(signal.SIGTERM, self.sigterm)
atexit.register(self.exit)
self.logger.warn("Starting daemon.")
self.action(*privileged_action_result)
"""
Copyright (c) 2015-2016 Ad Schellevis
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
import sys
def update_params(parameters):
""" update predefined parameters with given list from shell (as switches)
for example /a valA /b valB
converts to
{'a':'valA','b':'valB'}
(assuming parameters contains both a and b)
:param parameters: parameter dictionary
:return:
"""
cmd = None
for arg in sys.argv[1:]:
if cmd is None:
cmd = arg[1:]
else:
if cmd in parameters and arg.strip() != '':
parameters[cmd] = arg.strip()
cmd = None
"""
Copyright (c) 2016 Ad Schellevis
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
import os
import time
import datetime
class DHCPDLease(object):
watch_file = '/tmp/dhcpd.leases'
def __init__(self):
""" init watcher
:return: watcher object
"""
self._section_data = []
self._fhandle = None
self._last_pos = None
self._open()
def _open(self):
""" (re)open watched file
:return: watcher object
"""
try:
self._fhandle = open(self.watch_file, 'r')
self._last_pos = None
self._section_data = []
return True
except IOError:
self._fhandle = None
return False
@staticmethod
def parse_lease(lines):
""" parse dhcp lease
:param lines: lease section as list item
:return: dictionary
"""
lease = dict()
lease['address'] = lines[0].split()[1]
for line in lines:
parts = line.split()
field_name = parts[0]
field_value = None
if field_name in ('starts', 'ends', 'tstp', 'tsfp', 'atsfp', 'cltt') and len(parts) >= 3:
dt = '%s %s'%(parts[2], parts[3])
try:
field_value = time.mktime(datetime.datetime.strptime(dt, "%Y/%m/%d %H:%M:%S;").timetuple())
except ValueError:
field_value = None
elif field_name == 'hardware' and len(parts) >= 3:
field_value = {'hardware-type': parts[1], 'mac-address': parts[2]}
elif field_name in('uid', 'client-hostname') and len(parts) >= 2 and parts[1].find('"') > -1:
field_value = parts[1].split('"')[1]
if field_value is not None:
lease[field_name] = field_value
return lease
def watch(self):
""" watch file, return lease dictionaries
:return: iterator for leases
"""
if self._fhandle is None or os.fstat(self._fhandle.fileno()).st_nlink == 0:
# nothing to watch, try to (re)open return when failed
if not self._open():
return
elif self._last_pos is not None:
self._fhandle.seek(self._last_pos)
while True:
line = self._fhandle.readline()
if line:
if len(line) > 5 and line[0:5] == 'lease':
self._section_data.append(line)
elif len(line) > 1 and line[0] == '}':
self._section_data.append(line)
yield self.parse_lease(self._section_data)
self._section_data = []
elif len(self._section_data) > 0:
self._section_data.append(line)
else:
break
self._last_pos = self._fhandle.tell()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment