pax_global_header00006660000000000000000000000064124220035000014476gustar00rootroot0000000000000052 comment=cfe592cb6853c3de2bbde36f81fe49135707f065 dhcpy6d/000077500000000000000000000000001242200350000124035ustar00rootroot00000000000000dhcpy6d/Changelog000066400000000000000000000023241242200350000142160ustar00rootroot00000000000000Changelog for dhcpy6d 2014-10-22 0.4 listen on VLAN interfaces access neighbor cache natively on Linux allows empty answers do not cache MAC/LLIP addresses longterm as default ability to generate server DUID complete manpages more complete configuration correctness checks fixed single address definition error 2013-07-29 0.3 added ability to run as non-privileged user 2013-05-31 0.2 Attention: leases database scheme changed. Possibly leases database has to be recreated! 'range' address lease management got more robust logging output changed 2013-05-23 0.1.5 fixed race condition bug in category 'range' address lease storage 2013-05-18 0.1.4.1 fixed lease storage bug 2013-05-17 0.1.4 fixed race condition bug with already advertised addresses 2013-05-07 0.1.3 added RFC 3646 compliant domain search list option 24 reuse addresses of category "range" in a sensible way fixed bug with case sensitive textfile client config options 2013-03-19 0.1.2 fixed multiple addresses renew bug 2013-01-15 0.1.1 reverted to Handler.finish() method to prevent empty extra answer packets 2013-01-11 0.1 initial stable release dhcpy6d/MANIFEST.in000066400000000000000000000010351242200350000141400ustar00rootroot00000000000000# necessary because of buggy distutils include Changelog include dhcpy6d include doc/LICENSE include doc/clients-example.conf include doc/config.sql include doc/dhcpy6d-example.conf include doc/dhcpy6d-minimal.conf include doc/volatile.sql include man/man5/dhcpy6d.conf.5 include man/man5/dhcpy6d-clients.conf.5 include man/man8/dhcpy6d.8 include var/lib/volatile.sqlite include var/log/dhcpy6d.log include etc/dhcpy6d.conf include etc/logrotate.d/dhcpy6d include etc/init.d/dhcpy6d include etc/default/dhcpy6d include redhat/init.d/dhcpy6d dhcpy6d/README.md000066400000000000000000000011571242200350000136660ustar00rootroot00000000000000dhcpy6d ======= Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack transistion, addresses may be generated randomly, by range, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically. Supported platforms include Linux, OpenBSD, FreeBSD, NetBSD and MacOS X. At any other POSIX OS it might work too. Homepage: http://dhcpy6d.ifw-dresden.de Documentation: http://dhcpy6d.ifw-dresden.de/documentation dhcpy6d/build.sh000077500000000000000000000021371242200350000140440ustar00rootroot00000000000000#!/bin/sh # # # simple build script for dhcpy6d # # if [ -f /etc/debian_version ] then echo "Building .deb package" debuild clean debuild binary-indep elif [ -f /etc/redhat-release ] then echo "Building .rpm package" TOPDIR=$HOME/dhcpy6d.$$ SPEC=redhat/dhcpy6d.spec # create source folder for rpmbuild mkdir -p $TOPDIR/SOURCES # init needed in TOPDIR/SOURCES cp -pf redhat/init.d/dhcpy6d $TOPDIR/SOURCES # use setup.py sdist build output to get package name FILE=`python setup.py sdist --dist-dir $TOPDIR/SOURCES | grep "creating dhcpy6d-" | head -n1 | cut -d" " -f2` echo Source file: $FILE.tar.gz # version VERSION=`echo $FILE | cut -d"-" -f 2` # replace version in the spec file sed -i "s|Version:.*|Version: $VERSION|" $SPEC # finally build binary rpm rpmbuild -bb --define "_topdir $TOPDIR" $SPEC # get rpm file cp -f `find $TOPDIR/RPMS -name "$FILE-1.*noarch.rpm"` . # clean rm -rf $TOPDIR else echo "Package creation is only supported on Debian and RedHat derivatives." fi dhcpy6d/debian/000077500000000000000000000000001242200350000136255ustar00rootroot00000000000000dhcpy6d/debian/changelog000066400000000000000000000140301242200350000154750ustar00rootroot00000000000000dhcpy6d (0.4-1) unstable; urgency=low [ Henri Wahl ] * New upstream release + new options: log_mac_llip, cache_mac_llip (avoids cache poisoning) [ Axel Beckert ] * Add get-orig-source target to debian/rules for easier snapshot packaging. * Depend on ${python:Depends} instead of a hardcoded python (>= 2.6). * Add "Pre-Depends: dpkg (>= 1.16.5) for "start-stop-daemon --no-close" * Drop dependency on iproute/iproute2 as /sbin/ip is no more used. -- Axel Beckert Wed, 22 Oct 2014 21:03:56 +0200 dhcpy6d (0.3.99+git2014.09.18-1) unstable; urgency=medium * New upstream release candidate + snapshot + allow VLAN interface definitions + check if used interfaces exist + improved usability with more clear mesages if there are configuration errors + full man pages dhcpy6d.8 and dhcpy6d.conf.5 added + added command line argument --generate-duid for DUID generation at setup [ Henri Wahl ] * Append generated DUID to /etc/default/dhcpy6d if not yet present * Added command line arguments --really-do-it and --duid to be configured in /etc/defaults/dhcpy6d [ Axel Beckert ] * Switch section from "utils" to "net" like most other DHCP servers. * Update debian/source/options to follow upstream directory name changes * Bump Standards-Version to 3.9.6 (no changes) -- Axel Beckert Thu, 02 Oct 2014 18:25:44 +0200 dhcpy6d (0.3+git2014.07.23-1) unstable; urgency=medium * New upstream snapshot. + Man pages moved from Debian to Upstream + Don't ship man pages installed to /usr/share/doc/dhcpy6d/ * Delete dhcpy6d's log files upon package purge. * Add missing dependency on iproute2 or iproute. Thanks Henri! * Complete the switch from now deprecated python-support to dh_python2. + Only debian/control changes. (debian/rules was fine already.) + Fixes lintian warning build-depends-on-obsolete-package. -- Axel Beckert Thu, 24 Jul 2014 14:27:31 +0200 dhcpy6d (0.3+git2014.03.21-1) unstable; urgency=low * New upstream snapshot * First upload to Debian (Closes: #715010) * Switch back to non-native packaging * Set myself as primary package maintainer * Switch to source format "3.0 (quilt)". + Remove now obsolete README.source * Drop unnecessary build-dependency on quilt. Fixes lintian warning quilt-build-dep-but-no-series-file. * Add machine-readable debian/copyright. Fixes lintian warning no-debian-copyright. * Add "set -e" to postinst script to bail out on any error. * Move adduser from Suggests to Depends. Used in the postinst script. * Don't ship additional LICENSE file installed by upstream. Fixes lintian warning extra-license-file. * Add a debian/watch file. Fixes lintian warning debian-watch-file-is-missing. * Use short description from GitHub as short description. * Don't ship empty log file, create it at install time. Fixes lintian warning file-in-unusual-dir. * Add minimal man page with pointer to online documentation. Fixes lintian warning binary-without-manpage. * Also fix the following lintian warnings: + maintainer-address-malformed + maintainer-also-in-uploaders + no-standards-version-field + maintainer-script-lacks-debhelper-token + debhelper-but-no-misc-depends + description-starts-with-package-name + description-synopsis-might-not-be-phrased-properly + description-too-long (refers to first line) + extended-description-is-empty * Apply wrap-and-sort -- Axel Beckert Wed, 21 May 2014 14:25:27 +0200 dhcpy6d (0.3) unstable; urgency=low * New upstream - running as non-root user/group dhcpy6d - deb improvements - rpm improvements -- Henri Wahl Mon, 29 Jul 2013 13:14:00 +0200 dhcpy6d (0.2-1) unstable; urgency=low * New upstream - next fix in 'range' lease storage, getting more robust - better logging -- Henri Wahl Fri, 31 May 2013 14:40:00 +0200 dhcpy6d (0.1.5-1) unstable; urgency=low * New upstream - fixed race condition in 'range' lease storage -- Henri Wahl Thu, 23 May 2013 11:00:00 +0200 dhcpy6d (0.1.4.1-1) unstable; urgency=low * New upstream - fixed lease storage bug -- Henri Wahl Sat, 18 May 2013 00:50:00 +0200 dhcpy6d (0.1.4-1) unstable; urgency=low * New upstream - fixed advertised address handling for categories 'range' and 'random' -- Henri Wahl Fri, 17 May 2013 14:50:00 +0200 dhcpy6d (0.1.3-1) unstable; urgency=low * New upstream - added domain_search_list option - fixed case-sensitive MAC address config -- Henri Wahl Mon, 06 May 2013 14:50:00 +0200 dhcpy6d (0.1.2-1) unstable; urgency=low * New upstream - fixed multiple addresses renew bug -- Henri Wahl Tue, 19 Mar 2013 9:02:00 +0200 dhcpy6d (0.1.1-1) unstable; urgency=low * New upstream - reverted to Handler.finish() -- Henri Wahl Tue, 15 Jan 2013 07:35:00 +0200 dhcpy6d (0.1-1) unstable; urgency=low * New upstream - inital stable release -- Henri Wahl Wed, 11 Jan 2013 14:10:00 +0200 dhcpy6d (20130111-1) unstable; urgency=low * New upstream - more polishing for rpm packaging support -- Henri Wahl Wed, 11 Jan 2013 13:18:00 +0200 dhcpy6d (20130109-1) unstable; urgency=low * New upstream - polishing packaging support -- Henri Wahl Wed, 09 Jan 2013 14:16:00 +0200 dhcpy6d (20121221-1) unstable; urgency=low * New upstream - finished Debian support -- Henri Wahl Thu, 21 Dec 2012 11:25:00 +0200 dhcpy6d (20121220-1) unstable; urgency=low * New upstream - testing Debian support -- Henri Wahl Thu, 20 Dec 2012 11:25:00 +0200 dhcpy6d (20121219-1) unstable; urgency=low * New upstream - testing Debian support -- Henri Wahl Wed, 19 Dec 2012 11:25:00 +0200 dhcpy6d/debian/compat000066400000000000000000000000021242200350000150230ustar00rootroot000000000000007 dhcpy6d/debian/control000066400000000000000000000020731242200350000152320ustar00rootroot00000000000000Source: dhcpy6d Section: net X-Python-Version: >= 2.6 Priority: optional Maintainer: Axel Beckert Uploaders: Henri Wahl Build-Depends: debhelper (>= 7.0.50~), python-all (>= 2.6.6-3~) Build-Depends-Indep: dh-python Homepage: http://dhcpy6d.ifw-dresden.de Vcs-Git: git://github.com/HenriWahl/dhcpy6d.git Vcs-Browser: https://github.com/HenriWahl/dhcpy6d Standards-Version: 3.9.6 Package: dhcpy6d Architecture: all Depends: adduser, ${misc:Depends}, ${python:Depends} Pre-Depends: dpkg (>= 1.16.5) Suggests: python-dnspython, python-mysqldb Description: MAC address aware DHCPv6 server written in Python Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack transistion, addresses may be generated randomly, by range, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically. dhcpy6d/debian/copyright000066400000000000000000000023561242200350000155660ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: dhcpy6d Upstream-Contact: Henri Wahl Source: https://dhcpy6d.ifw-dresden.de/ Files: * Copyright: 2012-2014 Henri Wahl License: GPL-2+ Files: debian/* Copyright: 2012-2014 Henri Wahl 2014 Axel Beckert License: GPL-2+ License: GPL-2+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA . On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. dhcpy6d/debian/dhcpy6d.default000077700000000000000000000000001242200350000224252../etc/default/dhcpy6dustar00rootroot00000000000000dhcpy6d/debian/dhcpy6d.init000077700000000000000000000000001242200350000215052../etc/init.d/dhcpy6dustar00rootroot00000000000000dhcpy6d/debian/dhcpy6d.logrotate000077700000000000000000000000001242200350000235772../etc/logrotate.d/dhcpy6dustar00rootroot00000000000000dhcpy6d/debian/dhcpy6d.postinst000077500000000000000000000040721242200350000170010ustar00rootroot00000000000000#!/bin/sh # # attempting to create lower privileged user/group for dhcpy6d # take from http://www.debian.org/doc/manuals/securing-debian-howto/ch9.en.html#s-bpp-lower-privs # set -e case "$1" in install|upgrade|configure) # Sane defaults: [ -z "$SERVER_HOME" ] && SERVER_HOME=/var/lib/dhcpy6d [ -z "$SERVER_USER" ] && SERVER_USER=dhcpy6d [ -z "$SERVER_NAME" ] && SERVER_NAME="DHCPv6 server dhcpy6d" [ -z "$SERVER_GROUP" ] && SERVER_GROUP=dhcpy6d # Groups that the user will be added to, if undefined, then none. ADDGROUP="" # create user to avoid running server as root # 1. create group if not existing if ! getent group | grep -q "^$SERVER_GROUP:" ; then echo -n "Adding group $SERVER_GROUP.." addgroup --quiet --system $SERVER_GROUP 2>/dev/null ||true echo "..done" fi # 2. create homedir if not existing test -d $SERVER_HOME || mkdir $SERVER_HOME # 3. create user if not existing if ! getent passwd | grep -q "^$SERVER_USER:"; then echo -n "Adding system user $SERVER_USER.." adduser --quiet \ --system \ --ingroup $SERVER_GROUP \ --no-create-home \ --disabled-password \ $SERVER_USER 2>/dev/null || true echo "..done" fi # 4. adjust passwd entry usermod -c "$SERVER_NAME" \ -d $SERVER_HOME \ -g $SERVER_GROUP \ $SERVER_USER 2> /dev/null # 5. adjust file and directory permissions chown -R $SERVER_USER:$SERVER_GROUP $SERVER_HOME chmod -R 0770 $SERVER_HOME if [ ! -e /var/log/dhcpy6d.log ]; then touch /var/log/dhcpy6d.log fi chown $SERVER_USER:$SERVER_GROUP /var/log/dhcpy6d.log chmod 0770 /var/log/dhcpy6d.log # 6. add DUID entry to /etc/default/dhcpy6d if not yet existing if [ ! $(grep "DUID=" /etc/default/dhcpy6d) ]; then echo >> /etc/default/dhcpy6d echo "# LLT DUID generated by Debian" >> /etc/default/dhcpy6d echo "DUID=$(dhcpy6d --generate-duid)" >> /etc/default/dhcpy6d fi ;; esac #DEBHELPER# dhcpy6d/debian/dhcpy6d.postrm000077500000000000000000000002411242200350000164340ustar00rootroot00000000000000#!/bin/sh # # Delete dhcpy6d's log files upon package purge. # set -e case "$1" in purge) rm -f /var/log/dhcpy6d.log* ;; esac #DEBHELPER# dhcpy6d/debian/manpages000066400000000000000000000001131242200350000153360ustar00rootroot00000000000000man/man8/dhcpy6d.8 man/man5/dhcpy6d.conf.5 man/man5/dhcpy6d-clients.conf.5 dhcpy6d/debian/rules000077500000000000000000000007201242200350000147040ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with python2 override_dh_auto_install: dh_auto_install -- --install-scripts=/usr/sbin rm -f debian/dhcpy6d/usr/share/doc/dhcpy6d/LICENSE rm -f debian/dhcpy6d/var/log/dhcpy6d.log rm -f debian/dhcpy6d/usr/share/doc/dhcpy6d/*.[0-9] # make -f debian/rules get-orig-source get-orig-source: python setup.py sdist mv -v dist/dhcpy6d-*.tar.gz ../dhcpy6d_`dpkg-parsechangelog -SVersion | cut -d- -f1`.orig.tar.gz rm -r MANIFEST dist dhcpy6d/debian/source/000077500000000000000000000000001242200350000151255ustar00rootroot00000000000000dhcpy6d/debian/source/format000066400000000000000000000000141242200350000163330ustar00rootroot000000000000003.0 (quilt) dhcpy6d/debian/source/options000066400000000000000000000001651242200350000165450ustar00rootroot00000000000000extend-diff-ignore=MANIFEST\.in extend-diff-ignore=README\.md extend-diff-ignore=build\.sh extend-diff-ignore=redhat dhcpy6d/debian/watch000066400000000000000000000001151242200350000146530ustar00rootroot00000000000000version=3 https://dhcpy6d.ifw-dresden.de/download/ .*/dhcpy6d-(.*)\.tar\.gz dhcpy6d/dhcpy6/000077500000000000000000000000001242200350000136005ustar00rootroot00000000000000dhcpy6d/dhcpy6/Config.py000066400000000000000000001150531242200350000153640ustar00rootroot00000000000000# encoding: utf8 # # DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2014 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import ConfigParser import stat import os.path import uuid import time import shlex import copy import platform import pwd import grp import getopt import re import ctypes from Helpers import * # use ctypes for libc access in GetLibC from Helpers LIBC = GetLibC() # needed for boolean options BOOLPOOL = {"0":False, "1":True, "no":False, "yes":True, "false":False, "true":True, False:False, True:True, "on":True, "off":False} # whitespace for options with more than one value WHITESPACE = " ," # default usage text - to be extended USAGE = """ dhcpy6d - DHCPv6 server Usage: dhcpy6d --config [--user ] [--group ] [--duid ] [--really-do-it |] dhcpy6d --generate-duid See manpage dhcpy6d(8) for details. """ def GenerateDUID(): return "00010001%08x%012x" % (time.time(), uuid.getnode()) class Config(object): """ general settings """ def __init__(self): """ define defaults """ # default settings # Server cfg.INTERFACE + addresses self.INTERFACE = "eth0" self.MCAST = "ff02::1:2" self.PORT = "547" self.ADDRESS = "2001:db8::1" # effective user and group - will have to be set mainly by distribution package self.USER = "root" self.GROUP = "root" # lets make the water turn black... or build a shiny server DUID # in case someone will ever debug something here: Wireshark shows # year 2042 even if it is 2012 - time itself is OK self.SERVERDUID = "00010001%08x%012x" % (time.time(), uuid.getnode()) self.NAMESERVER = "" # domain for FQDN hostnames self.DOMAIN = "domain" # domain search list for option 24, according to RFC 3646 # defaults to DOMAIN self.DOMAIN_SEARCH_LIST = "" # IA_NA Options # Default preferred lifetime for addresses self.PREFERRED_LIFETIME = "5400" # Default valid lifetime for addresses self.VALID_LIFETIME = "7200" # T1 RENEW self.T1 = "2700" # T2 REBIND self.T2 = "4050" # Server Preference self.SERVER_PREFERENCE = "255" # SNTP SERVERS Option 31 self.SNTP_SERVERS = [ self.ADDRESS ] # INFORMATION REFRESH TIME option 32 for option 11 (INFORMATION REQUEST) # see RFC http://tools.ietf.org/html/rfc4242 self.INFORMATION_REFRESH_TIME = "6000" # config type # one of file, mysql, sqlite or none self.STORE_CONFIG = "none" # one of mysql or sqlite self.STORE_VOLATILE = "sqlite" # file for client information self.STORE_FILE_CONFIG = "clients.conf" # DB data self.STORE_MYSQL_HOST = "localhost" self.STORE_MYSQL_DB = "dhcpy6d" self.STORE_MYSQL_USER = "user" self.STORE_MYSQL_PASSWORD = "password" self.STORE_SQLITE_CONFIG = "config.sqlite" self.STORE_SQLITE_VOLATILE = "volatile.sqlite" # whether MAC-LLIP pairs should be stored forever or retrieved freshly if needed self.CACHE_MAC_LLIP = "False" # DNS Update settings self.DNS_UPDATE = "False" self.DNS_UPDATE_NAMESERVER = "::1" self.DNS_TTL = 86400 self.DNS_RNDC_KEY = "rndc-key" self.DNS_RNDC_SECRET = "0000000000000000000000000000000000000000000000000000000000000" # DNS RFC 4704 client DNS wishes # use client supplied hostname self.DNS_USE_CLIENT_HOSTNAME = "False" # ignore client ideas about DNS (if at all, what name to use, self-updating...) self.DNS_IGNORE_CLIENT = "True" # Log ot not self.LOG = "False" # Log level self.LOG_LEVEL = "INFO" # Log on console self.LOG_CONSOLE = "False" # Logfile self.LOG_FILE = "" # Log to syslog self.LOG_SYSLOG = "False" # Syslog facility self.LOG_SYSLOG_FACILITY = "daemon" # Local syslog socket or server:port if platform.system() in ["Linux", "OpenBSD"]: self.LOG_SYSLOG_DESTINATION = "/dev/log" else: self.LOG_SYSLOG_DESTINATION = "/var/run/log" # Log newly found MAC addresses - if CACHE_MAC_LLIP is false this might be way too much self.LOG_MAC_LLIP = "False" # some 128 bits self.AUTHENTICATION_INFORMATION = "00000000000000000000000000000000" # for debugging - if False nothing is done self.REALLY_DO_IT = "True" # interval for TidyUp thread - time to sleep in TidyUpThread self.CLEANING_INTERVAL = 5 # Address and class schemes self.ADDRESSES = dict() self.CLASSES = dict() self.IDENTIFICATION = "mac" self.IDENTIFICATION_MODE = "match_all" # regexp filters for hostnames etc. self.FILTERS = {"mac":[], "duid":[], "hostname":[]} # define a fallback default class and address scheme self.ADDRESSES["default"] = ConfigAddress(ia_type="na", prefix_length="64", category="mac", pattern="fdef::$mac$", aclass="default", atype="default", prototype="fdef0000000000000000XXXXXXXXXXXX") self.CLASSES["default"] = Class() self.CLASSES["default"].ADDRESSES.append("default") # define dummy address scheme for fixed addresses # pattern and prototype are not really needed as this # addresses are fixed self.ADDRESSES["fixed"] = ConfigAddress(ia_type="na", prefix_length="64", category="fixed", pattern="fdef0000000000000000000000000001", aclass="default", atype="fixed", prototype="fdef0000000000000000000000000000") # config file from command line # default config file and cli values configfile = self.cli_options = self.cli_user = self.cli_group = self.cli_duid = self.cli_really_do_it = None # get multiple options try: self.cli_options, cli_remains = getopt.gnu_getopt(sys.argv[1:], "c:g:u:d:Gr:", ["config=", "user=", "group=", "duid=", "generate-duid", "really-do-it="]) for opt, arg in self.cli_options: if opt in ("-c", "--config"): configfile = arg if opt in ("-g", "--group"): self.cli_group = arg if opt in ("-u", "--user"): self.cli_user = arg if opt in ("-d", "--duid"): self.cli_duid = arg if opt in ("-r", "--really-do-it"): self.cli_really_do_it = arg if opt in ("-G", "--generate-duid"): print GenerateDUID() sys.exit(0) except getopt.GetoptError, err: print err print USAGE sys.exit(1) if configfile == None: ErrorExit("No config file given - please use --config ") if os.path.exists(configfile): if not (os.path.isfile(configfile) or os.path.islink(configfile)): ErrorExit("Configuration file '%s' is no file or link." % (configfile)) else: ErrorExit("Configuration file '%s' does not exist." % (configfile)) # read config at once self.ReadConfig(configfile) def ReadConfig(self, configfile): """ read configuration from file, should work with included files too - at least this is the plan """ # instantiate Configparser config = ConfigParser.ConfigParser() config.read(configfile) # whyever sections classes get overwritten sometimes and so some configs had been missing # so create classes and addresses here for section in config.sections(): if section.startswith("class_"): self.CLASSES[section.split("class_")[1]] = Class(name=section.split("class_")[1].strip()) if section.startswith("address_"): self.ADDRESSES[section.split("address_")[1].strip()] = ConfigAddress() for section in config.sections(): # go through all items for item in config.items(section): if section.upper() == "DHCPY6D": # check if keyword is known - if not, exit if not item[0].upper() in self.__dict__: ErrorExit("Keyword '%s' in section '[%s]' of configuration file '%s' is unknown." % (item[0], section, configfile)) # ConfigParser seems to be not case sensitive so settings get normalized object.__setattr__(self, item[0].upper(), str(item[1]).strip()) else: # global address schemes if section.startswith("address_"): # check if keyword is known - if not, exit if not item[0].upper() in self.ADDRESSES[section.split("address_")[1]].__dict__: ErrorExit("Keyword '%s' in section '[%s]' of configuration file '%s' is unknown." % (item[0], section, configfile)) self.ADDRESSES[section.split("address_")[1]].__setattr__(item[0].upper(), str(item[1]).strip()) # global classes with their addresses elif section.startswith("class_"): # check if keyword is known - if not, exit if not item[0].upper() in self.CLASSES[section.split("class_")[1]].__dict__: ErrorExit("Keyword '%s' in section '[%s]' of configuration file '%s' is unknown." % (item[0], section, configfile)) if item[0].upper() == "ADDRESSES": # strip whitespace and separators of addresses lex = shlex.shlex(item[1]) lex.whitespace = WHITESPACE lex.wordchars += ":." for address in lex: if len(address) > 0: self.CLASSES[section.split("class_")[1]].ADDRESSES.append(address) elif item[0].upper() == "INTERFACE": # strip whitespace and separators of interfaces lex = shlex.shlex(item[1]) lex.whitespace = WHITESPACE lex.wordchars += ":." for interface in lex: if not interface in self.INTERFACE: ErrorExit("Interface '%s' used in section '[%s]' of configuration file '%s' is not defined in general settings." % (interface, section, configfile)) else: self.CLASSES[section.split("class_")[1]].__setattr__(item[0].upper(), str(item[1]).strip()) # The next paragraphs contain finetuning self.IDENTIFICATION = ListifyOption(self.IDENTIFICATION) # get interfaces as list self.INTERFACE = ListifyOption(self.INTERFACE) # create default classes for each interface - if not defined # derive from default "default" class for i in self.INTERFACE: if not "default_" + i in self.CLASSES: self.CLASSES["default_" + i] = copy.copy(self.CLASSES["default"]) self.CLASSES["default_" + i].NAME = "default_" + i self.CLASSES["default_" + i].INTERFACE = i # lower storage self.STORE_CONFIG = self.STORE_CONFIG.lower() self.STORE_VOLATILE = self.STORE_VOLATILE.lower() # boolize none-config-store if self.STORE_CONFIG.lower() == "none": self.STORE_CONFIG = False # if no domain search list has been given use DOMAIN if len(self.DOMAIN_SEARCH_LIST) == 0: self.DOMAIN_SEARCH_LIST = self.DOMAIN # domain search list has to be a list self.DOMAIN_SEARCH_LIST = ListifyOption(self.DOMAIN_SEARCH_LIST) # get nameservers as list if len(self.NAMESERVER) > 0: self.NAMESERVER = ListifyOption(self.NAMESERVER) # convert to boolean value self.DNS_UPDATE = BOOLPOOL[self.DNS_UPDATE.lower()] self.DNS_USE_CLIENT_HOSTNAME = BOOLPOOL[self.DNS_USE_CLIENT_HOSTNAME.lower()] self.DNS_IGNORE_CLIENT = BOOLPOOL[self.DNS_IGNORE_CLIENT.lower()] self.REALLY_DO_IT = BOOLPOOL[self.REALLY_DO_IT.lower()] self.LOG = BOOLPOOL[self.LOG.lower()] self.LOG_CONSOLE = BOOLPOOL[self.LOG_CONSOLE.lower()] self.LOG_LEVEL = self.LOG_LEVEL.upper() self.LOG_SYSLOG = BOOLPOOL[self.LOG_SYSLOG.lower()] self.CACHE_MAC_LLIP = BOOLPOOL[self.CACHE_MAC_LLIP.lower()] self.LOG_MAC_LLIP= BOOLPOOL[self.LOG_MAC_LLIP.lower()] self.LOG_SYSLOG_FACILITY = self.LOG_SYSLOG_FACILITY.upper() # index of classes which add some identification rules etc. for c in self.CLASSES.values(): if c.FILTER_MAC != "": self.FILTERS["mac"].append(c) if c.FILTER_DUID != "": self.FILTERS["duid"].append(c) if c.FILTER_HOSTNAME != "": self.FILTERS["hostname"].append(c) if c.NAMESERVER != "": c.NAMESERVER = ListifyOption(c.NAMESERVER) if c.INTERFACE != "": c.INTERFACE = ListifyOption(c.INTERFACE) else: # use general setting if none specified c.INTERFACE = self.INTERFACE # use default T1 and T2 if not defined if c.T1 == 0: c.T1 = self.T1 if c.T2 == 0: c.T2 = self.T2 # set type properties for addresses for a in self.ADDRESSES: # name for address, important for leases db self.ADDRESSES[a].TYPE = a if self.ADDRESSES[a].VALID_LIFETIME == 0: self.ADDRESSES[a].VALID_LIFETIME = self.VALID_LIFETIME if self.ADDRESSES[a].PREFERRED_LIFETIME == 0: self.ADDRESSES[a].PREFERRED_LIFETIME = self.PREFERRED_LIFETIME # normalize ranges self.ADDRESSES[a].RANGE = self.ADDRESSES[a].RANGE.lower() # add prototype for later fast validity comparison of rebinding leases # also use as proof of validity of address patterns self.ADDRESSES[a]._build_prototype() # convert boolean string to boolean value self.ADDRESSES[a].DNS_UPDATE = BOOLPOOL[self.ADDRESSES[a].DNS_UPDATE] if self.ADDRESSES[a].DNS_ZONE == "": self.ADDRESSES[a].DNS_ZONE = self.DOMAIN if self.ADDRESSES[a].DNS_TTL == "0": self.ADDRESSES[a].DNS_TTL = self.DNS_TTL # check if some options are set by cli options if not self.cli_user == None: self.USER = self.cli_user if not self.cli_group == None: self.GROUP = self.cli_group if not self.cli_duid == None: self.SERVERDUID = self.cli_duid if not self.cli_really_do_it == None: self.REALLY_DO_IT = BOOLPOOL[self.cli_really_do_it.lower()] # check config msg_prefix = "General configuration:" # check user and group try: pwd.getpwnam(self.USER) except: ErrorExit("%s User '%s' does not exist" % (msg_prefix, self.USER)) try: grp.getgrnam(self.GROUP) except: ErrorExit("%s Group '%s' does not exist" % (msg_prefix, self.GROUP)) # check interface for i in self.INTERFACE: # also accept Linux VLAN and other definitions but interface must exist if LIBC.if_nametoindex(i) == 0 or not re.match("^[a-z0-9_:%-]*$", i, re.IGNORECASE): ErrorExit("%s Interface '%s' is invalid." % (msg_prefix, i)) # check multicast address try: DecompressIP6(self.MCAST) except Exception, err: ErrorExit("%s Multicast address '%s' is invalid." % (msg_prefix, err)) if not self.MCAST.lower().startswith("ff"): ErrorExit("Multicast address '%s' is invalid." % (msg_prefix)) # check DHCPv6 port if not self.PORT.isdigit(): ErrorExit("%s Port '%s' is invalid" % (msg_prefix, self.PORT)) elif not 0 < int(self.PORT) <= 65535: ErrorExit("%s Port '%s' is invalid" % (msg_prefix, self.PORT)) # check server's address try: DecompressIP6(self.ADDRESS) except Exception, err: ErrorExit("%s Server address '%s' is invalid." % (msg_prefix, err)) # check server duid if not self.SERVERDUID.isalnum(): ErrorExit("%s Server DUID '%s' must be alphanumeric." % (msg_prefix, self.SERVERDUID)) # check nameserver to be given to client for nameserver in self.NAMESERVER: try: DecompressIP6(nameserver) except Exception, err: ErrorExit("%s Name server address '%s' is invalid." % (msg_prefix, err)) # partly check of domain name validity if not re.match("^[a-z0-9.-]*$", self.DOMAIN, re.IGNORECASE): ErrorExit("%s Domain name '%s' is invalid." % (msg_prefix, self.DOMAIN)) # partly check of domain name validity if not self.DOMAIN.lower()[0].isalpha() or \ not self.DOMAIN.lower()[-1].isalpha(): ErrorExit("%s Domain name '%s' is invalid." % (msg_prefix, self.DOMAIN)) # check domain search list domains for d in self.DOMAIN_SEARCH_LIST: # partly check of domain name validity if not re.match("^[a-z0-9.-]*$", d, re.IGNORECASE): ErrorExit("%s Domain search list domain name '%s' is invalid." % (msg_prefix, d)) # partly check of domain name validity if not d.lower()[0].isalpha() or \ not d.lower()[-1].isalpha(): ErrorExit("%s Domain search list domain name '%s' is invalid." % (msg_prefix, d)) # check if valid lifetime is a number if not self.VALID_LIFETIME.isdigit(): ErrorExit("%s Valid lifetime '%s' is invalid." % (msg_prefix, self.VALID_LIFETIME)) # check if preferred lifetime is a number if not self.PREFERRED_LIFETIME.isdigit(): ErrorExit("%s Preferred lifetime '%s' is invalid." % (msg_prefix, self.PREFERRED_LIFETIME)) # check if valid lifetime is longer than preferred lifetime if not int(self.VALID_LIFETIME) > int(self.PREFERRED_LIFETIME): ErrorExit("%s Valid lifetime '%s' is shorter than preferred lifetime '%s' and thus invalid." %\ (msg_prefix, self.VALID_LIFETIME, self.PREFERRED_LIFETIME)) # check if T1 is a number if not self.T1.isdigit(): ErrorExit("%s T1 '%s' is invalid." % (msg_prefix, self.T1)) # check if T2 is a number if not self.T2.isdigit(): ErrorExit("%s T2 '%s' is invalid." % (msg_prefix, self.T2)) # check T2 is not smaller than T1 if not int(self.T2) >= int(self.T1): ErrorExit("%s T2 '%s' is shorter than T1 '%s' and thus invalid." %\ (msg_prefix, self.T2, self.T1)) # check if T1 <= T2 <= PREFERRED_LIFETIME <= VALID_LIFETIME if not (int(self.T1) <= int(self.T2) <= int(self.PREFERRED_LIFETIME) <= int(self.VALID_LIFETIME)): ErrorExit("%s Time intervals T1 '%s' <= T2 '%s' <= preferred_lifetime '%s' <= valid_lifetime '%s' are wrong." %\ (msg_prefix, self.T1, self.T2, self.PREFERRED_LIFETIME, self.VALID_LIFETIME)) # check server preference if not self.SERVER_PREFERENCE.isdigit(): ErrorExit("%s Server preference '%s' is invalid." % (msg_prefix, self.SERVER_PREFERENCE)) elif not 0 <= int(self.SERVER_PREFERENCE) <= 255: ErrorExit("Server preference '%s' is invalid" % (self.SERVER_PREFERENCE)) # check information refresh time if not self.INFORMATION_REFRESH_TIME.isdigit(): ErrorExit("%s Information refresh time '%s' is invalid." % (msg_prefix, self.INFORMATION_REFRESH_TIME)) elif not 0 < int(self.INFORMATION_REFRESH_TIME): ErrorExit("%s Information refresh time preference '%s' is pretty short." % (msg_prefix, self.INFORMATION_REFRESH_TIME)) # check validity of configuration source if not self.STORE_CONFIG in ["mysql", "sqlite", "file", False]: ErrorExit("%s Unknown config storage type '%s' is invalid." % (msg_prefix, self.STORAGE)) # check which type of storage to use for leases if not self.STORE_VOLATILE in ["mysql", "sqlite"]: ErrorExit("%s Unknown volatile storage type '%s' is invalid." % (msg_prefix, self.VOLATILE)) # check validity of config file if self.STORE_CONFIG == "file": if os.path.exists(self.STORE_FILE_CONFIG): if not (os.path.isfile(self.STORE_FILE_CONFIG) or os.path.islink(self.STORE_FILE_CONFIG)): ErrorExit("%s Config file '%s' is no file or link." % (msg_prefix, self.STORE_FILE_CONFIG)) else: ErrorExit("%s Config file '%s' does not exist." % (msg_prefix, self.STORE_FILE_CONFIG)) # check validity of config db sqlite file if self.STORE_CONFIG == "sqlite": if os.path.exists(self.STORE_SQLITE_CONFIG): if not (os.path.isfile(self.STORE_SQLITE_CONFIG) or os.path.islink(self.STORE_SQLITE_CONFIG)): ErrorExit("%s SQLite file '%s' is no file or link." % (msg_prefix, self.STORE_SQLITE_CONFIG)) else: ErrorExit("%s SQLite file '%s' does not exist." % (msg_prefix, self.STORE_SQLITE_CONFIG)) # check validity of volatile db sqlite file if self.STORE_VOLATILE == "sqlite": if os.path.exists(self.STORE_SQLITE_VOLATILE): if not (os.path.isfile(self.STORE_SQLITE_VOLATILE) or os.path.islink(self.STORE_SQLITE_VOLATILE)): ErrorExit("%s SQLite file '%s' is no file or link." % (msg_prefix, self.STORE_SQLITE_VOLATILE)) else: ErrorExit("%s SQLite file '%s' does not exist." % (msg_prefix, self.STORE_SQLITE_VOLATILE)) # check log validity if self.LOG: if self.LOG_FILE != "": if os.path.exists(self.LOG_FILE): if not (os.path.isfile(self.LOG_FILE) or os.path.islink(self.LOG_FILE)): ErrorExit("%s Logfile '%s' is no file or link." % (msg_prefix, self.LOG_FILE)) else: ErrorExit("%s Logfile '%s' does not exist." % (msg_prefix, self.LOG_FILE)) # check ownership of logfile stat_result = os.stat(self.LOG_FILE) if not stat_result.st_uid == pwd.getpwnam(self.USER).pw_uid: ErrorExit("%s User %s is not owner of logfile '%s'." % (msg_prefix, self.USER, self.LOG_FILE)) if not stat_result.st_gid == grp.getgrnam(self.GROUP).gr_gid: ErrorExit("%s Group %s is not owner of logfile '%s'." % (msg_prefix, self.GROUP, self.LOG_FILE)) else: ErrorExit("%s No logfile configured." % (msg_prefix)) if not self.LOG_LEVEL in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: ErrorExit("Log level %s is invalid" % (self.LOG_LEVEL)) if self.LOG_SYSLOG: if not self.LOG_SYSLOG_FACILITY in ["KERN", "USER", "MAIL", "DAEMON", "AUTH", "LPR", "NEWS", "UUCP", "CRON", "SYSLOG", "LOCAL0", "LOCAL1", "LOCAL2", "LOCAL3", "LOCAL4", "LOCAL5", "LOCAL6", "LOCAL7"]: ErrorExit("%s Syslog facility '%s' is invalid." % (msg_prefix, self.LOG_SYSLOG_FACILITY)) if self.LOG_SYSLOG_DESTINATION.startswith("/"): stat_result = os.stat(self.LOG_SYSLOG_DESTINATION) if not stat.S_ISSOCK(stat_result.st_mode): ErrorExit("%s Syslog destination '%s' is no socket." % (msg_prefix, self.LOG_SYSLOG_DESTINATION)) elif self.LOG_SYSLOG_DESTINATION.count(":") > 0: if self.LOG_SYSLOG_DESTINATION.count(":") > 1: ErrorExit("%s Syslog destination '%s' is no valid host:port destination." % (msg_prefix, self.LOG_SYSLOG_DESTINATION)) # check authentification information if not self.AUTHENTICATION_INFORMATION.isalnum(): ErrorExit("%s Authentification information '%s' must be alphanumeric." % (msg_prefix, self.AUTHENTICATION_INFORMATION)) # check validity of identification attributes for i in self.IDENTIFICATION: if not i in ["mac", "hostname", "duid"]: ErrorExit("%s Identification must consist of 'mac', 'hostname' and/or 'duid'." % (msg_prefix)) # check validity of identification mode if not self.IDENTIFICATION_MODE.strip() in ["match_all", "match_some"]: ErrorExit("%s Identification mode must be one of 'match_all' or 'macht_some'." % (msg_prefix)) # cruise through classes # more checks to come... for c in self.CLASSES: msg_prefix = "Class '%s':" % (c) if not self.CLASSES[c].ANSWER in ["normal", "noaddress", "none"]: ErrorExit("%s answer type must be one of 'normal', 'noaddress' and 'none'." % (msg_prefix)) # check interface for i in self.CLASSES[c].INTERFACE: # also accept Linux VLAN and other definitions but interface must exist if LIBC.if_nametoindex(i) == 0 or not re.match("^[a-z0-9_:%-]*$", i, re.IGNORECASE): ErrorExit("%s Interface '%s' is invalid." % (msg_prefix, i)) # check nameserver to be given to client for nameserver in self.CLASSES[c].NAMESERVER: try: DecompressIP6(nameserver) except Exception, err: ErrorExit("%s Name server address '%s' is invalid." % (msg_prefix, err)) # check if T1 is a number if not self.CLASSES[c].T1.isdigit(): ErrorExit("%s T1 '%s' is invalid." % (msg_prefix, self.CLASSES[c].T1)) # check if T2 is a number if not self.CLASSES[c].T2.isdigit(): ErrorExit("%s T2 '%s' is invalid." % (msg_prefix, self.CLASSES[c].T2)) # check T2 is not smaller than T1 if not int(self.CLASSES[c].T2) >= int(self.CLASSES[c].T1): ErrorExit("%s T2 '%s' is shorter than T1 '%s' and thus invalid." %\ (msg_prefix, self.CLASSES[c].T2, self.CLASSES[c].T1)) # check every single address of a class for a in self.CLASSES[c].ADDRESSES: msg_prefix = "Class '%s' Address type '%s':" % (c, a) # test if used addresses are defined if not a in self.ADDRESSES: ErrorExit("%s Address type '%s' is not defined." % (msg_prefix, a)) # test validity of category if not self.ADDRESSES[a].CATEGORY.strip() in ["fixed", "range", "random", "mac", "id"]: ErrorExit("%s Category '%s' is invalid. Category must be one of 'fixed', 'range', 'random', 'mac' and 'id'." % (msg_prefix, self.ADDRESSES[a].CATEGORY)) # test numberness and length of prefix if not self.ADDRESSES[a].PREFIX_LENGTH.strip().isdigit(): ErrorExit("%s Prefix length '%s' is not a number." % (msg_prefix, self.ADDRESSES[a].PREFIX_LENGTH.strip())) elif not 0 <= int(self.ADDRESSES[a].PREFIX_LENGTH) <= 128: ErrorExit("%s Prefix length '%s' is out of range." % (msg_prefix, self.ADDRESSES[a].PREFIX_LENGTH.strip())) # test validity of pattern - has its own error output self.ADDRESSES[a]._build_prototype() # test existence of category specific variable in pattern if self.ADDRESSES[a].CATEGORY == "range": if not 0 < self.ADDRESSES[a].PATTERN.count("$range$") < 2: ErrorExit("%s Pattern '%s' contains wrong number of '$range$' variables for category 'range'." %\ (msg_prefix, self.ADDRESSES[a].PATTERN.strip())) elif not self.ADDRESSES[a].PATTERN.endswith("$range$"): ErrorExit("%s Pattern '%s' must end with '$range$' variable for category 'range'." %\ (msg_prefix, self.ADDRESSES[a].PATTERN.strip())) if self.ADDRESSES[a].CATEGORY == "mac": if not 0 < self.ADDRESSES[a].PATTERN.count("$mac$") < 2: ErrorExit("%s Pattern '%s' contains wrong number of '$mac$' variables for category 'mac'." %\ (msg_prefix, self.ADDRESSES[a].PATTERN.strip())) if self.ADDRESSES[a].CATEGORY == "id": if not self.ADDRESSES[a].PATTERN.count("$id$") == 1: ErrorExit("%s Pattern '%s' contains wrong number of '$id$' variables for category 'id'." %\ (msg_prefix, self.ADDRESSES[a].PATTERN.strip())) if self.ADDRESSES[a].CATEGORY == "random": if not self.ADDRESSES[a].PATTERN.count("$random64$") == 1: ErrorExit("%s Pattern '%s' contains wrong number of '$random64$' variables for category 'random'." %\ (msg_prefix, self.ADDRESSES[a].PATTERN.strip())) # check ia_type if not self.ADDRESSES[a].IA_TYPE.strip().lower() in ["na", "ta"]: ErrorExit("%s: IA type '%s' must be one of 'na' or 'ta'." % (msg_prefix, self.ADDRESSES[a].IA_TYPE.strip())) # check if valid lifetime is a number if not self.ADDRESSES[a].VALID_LIFETIME.isdigit(): ErrorExit("%s Valid lifetime '%s' is invalid." % (msg_prefix, self.ADDRESSES[a].VALID_LIFETIME)) # check if preferred lifetime is a number if not self.ADDRESSES[a].PREFERRED_LIFETIME.isdigit(): ErrorExit("%s Preferred lifetime '%s' is invalid." % (msg_prefix, self.ADDRESSES[a].PREFERRED_LIFETIME)) # check if valid lifetime is longer than preferred lifetime if not int(self.ADDRESSES[a].VALID_LIFETIME) >= int(self.ADDRESSES[a].PREFERRED_LIFETIME): ErrorExit("%s Valid lifetime '%s' is shorter than preferred lifetime '%s' and thus invalid." %\ (msg_prefix, self.ADDRESSES[a].VALID_LIFETIME, self.ADDRESSES[a].PREFERRED_LIFETIME)) # check if T1 <= T2 <= PREFERRED_LIFETIME <= VALID_LIFETIME if not (int(self.CLASSES[c].T1) <= int(self.CLASSES[c].T2) <=\ int(self.ADDRESSES[a].PREFERRED_LIFETIME) <= int(self.ADDRESSES[a].VALID_LIFETIME)): ErrorExit("%s Time intervals T1 '%s' <= T2 '%s' <= preferred_lifetime '%s' <= valid_lifetime '%s' are wrong." %\ (msg_prefix, self.CLASSES[c].T1, self.CLASSES[c].T2, self.ADDRESSES[a].PREFERRED_LIFETIME, self.ADDRESSES[a].VALID_LIFETIME)) class ConfigAddress(object): """ class for address definition, used for config """ def __init__(self, address=None, ia_type="na", prefix_length="64", category="random", pattern="2001:db8::$random64$", preferred_lifetime=0, valid_lifetime=0, atype="default", aclass="default", prototype="", range="", dns_update=False, dns_zone="", dns_rev_zone="0.8.b.d.1.0.0.2.ip6.arpa", dns_ttl = "0", valid = True): self.PREFIX_LENGTH = prefix_length self.CATEGORY = category self.PATTERN = pattern self.IA_TYPE = ia_type self.PREFERRED_LIFETIME = preferred_lifetime self.VALID_LIFETIME = valid_lifetime self.ADDRESS = address self.RANGE = range.lower() # because "class" is a python keyword we use "aclass" here self.CLASS = aclass # same with type self.TYPE = atype # a prototypical address to be compared with leases given by # clients - if prototype and lease address kind of match # give back the lease as valid self.PROTOTYPE = prototype # flag for updating address in DNS or not self.DNS_UPDATE = dns_update # DNS zone data self.DNS_ZONE = dns_zone.lower() self.DNS_REV_ZONE = dns_rev_zone.lower() self.DNS_TTL = dns_ttl # flag invalid addresses as invalid, valid ones as valid self.VALID = valid def _build_prototype(self): """ build prototype of pattern for later comparison with leases """ a = self.PATTERN # check different client address categories - to be extended! if self.CATEGORY in ["mac", "id", "range", "random"]: if self.CATEGORY == "mac": a = a.replace("$mac$", "XXXX:XXXX:XXXX") elif self.CATEGORY == "id": a = a.replace("$id$", "XXXX") elif self.CATEGORY == "random": a = a.replace("$random64$", "XXXX:XXXX:XXXX:XXXX") elif self.CATEGORY == "range": a = a.replace("$range$", "XXXX") try: # build complete "address" and ignore all the Xs (strict=False) a = DecompressIP6(a, strict=False) except: #print "Address", self.TYPE + ": address pattern", self.PATTERN, "is not valid!" ErrorExit("Address type '%s' address pattern '%s' is not valid." % (self.TYPE, self.PATTERN)) self.PROTOTYPE = a def matches_prototype(self, address): """ test if given address matches prototype and therefore this address' DNS zone information might be used only used for address types, not client instances """ match = False # compare all chars of address and prototype, if they do match or # prototype has placeholder X return finally True, otherwise stop # at the first difference and give back False for i in range(32): if self.PROTOTYPE[i] == address[i] or self.PROTOTYPE[i] == "X": match = True else: match = False break return match class ClientAddress(object): """ class for address definition, used for clients """ def __init__(self, address=None, ia_type="na", prefix_length="64", category="random", preferred_lifetime=0, valid_lifetime=0, atype="default", aclass="default", dns_update=False, dns_zone="", dns_rev_zone="0.8.b.d.1.0.0.2.ip6.arpa", dns_ttl = "0", valid = True, ): self.PREFIX_LENGTH = prefix_length self.CATEGORY = category self.IA_TYPE = ia_type self.PREFERRED_LIFETIME = preferred_lifetime self.VALID_LIFETIME = valid_lifetime self.ADDRESS = address # because "class" is a python keyword we use "aclass" here # this property stores the class the address is used for self.CLASS = aclass # same with type self.TYPE = atype # flag for updating address in DNS or not self.DNS_UPDATE = dns_update # DNS zone data self.DNS_ZONE = dns_zone.lower() self.DNS_REV_ZONE = dns_rev_zone.lower() self.DNS_TTL = dns_ttl # flag invalid addresses as invalid, valid ones as valid self.VALID = valid class Class(object): """ class for class definition """ def __init__(self, name=""): self.NAME = name self.ADDRESSES = list() self.NAMESERVER = "" self.FILTER_MAC = "" self.FILTER_HOSTNAME = "" self.FILTER_DUID = "" self.IDENTIFICATION_MODE = "match_all" # RENEW time self.T1 = 0 # REBIND time self.T2 = 0 # at which interface this class of clients is served self.INTERFACE = "" # in certain cases it might be useful not to give any address to clients, for example if only a defined group # of hosts should get IPv6 addresses and others not. They will get a "NoAddrsAvail" response if this option # is set to "noaddress" or no answer at all if set to "none" self.ANSWER = "normal" dhcpy6d/dhcpy6/Constants.py000066400000000000000000000076771242200350000161470ustar00rootroot00000000000000# encoding: utf8 # # DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2014 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # DHCPv6 MESSAGE_TYPES = { 1:"SOLICIT", 2:"ADVERTISE", 3:"REQUEST", 4:"CONFIRM", 5:"RENEW", \ 6:"REBIND", 7:"REPLY", 8:"RELEASE", 9:"DECLINE", 10:"RECONFIGURE", \ 11:"INFORMATION-REQUEST", 12:"RELAY-FORW", 13:"RELAY-REPL" } # DUID DUID_TYPES = { 1:"DUID-LLT", 2:"DUID-EN", 3:"DUID-LL" } # see http://www.iana.org/assignments/dhcpv6-parameters/ OPTION_REQUEST = { 1:"OPTION_CLIENTID",\ 2:"OPTION_SERVERID",\ 3:"OPTION_IA_NA",\ 4:"OPTION_IA_TA",\ 5:"OPTION_IAADDR",\ 6:"OPTION_ORO",\ 7:"OPTION_PREFERENCE",\ 8:"OPTION_ELAPSED_TIME",\ 9:"OPTION_RELAY_MSG",\ 10:"Unassigned",\ 11:"OPTION_AUTH",\ 12:"OPTION_UNICAST", \ 13:"OPTION_STATUS_CODE", \ 14:"OPTION_RAPID_COMMIT",\ 15:"OPTION_USER_CLASS",\ 16:"OPTION_VENDOR_CLASS",\ 17:"OPTION_VENDOR_OPTS",\ 18:"OPTION_INTERFACE_ID",\ 19:"OPTION_RECONF_MSG",\ 20:"OPTION_RECONF_ACCEPT",\ 21:"SIP Servers Domain Name List",\ 22:"SIP Servers IPv6 Address List",\ 23:"DNS Recursive Name Server Option",\ 24:"Domain Search List option",\ 25:"OPTION_IA_PD",\ 26:"OPTION_IAPREFIX",\ 27:"OPTION_NIS_SERVERS",\ 28:"OPTION_NISP_SERVERS",\ 29:"OPTION_NIS_DOMAIN_NAME",\ 30:"OPTION_NISP_DOMAIN_NAME",\ 31:"OPTION_SNTP_SERVERS",\ 32:"OPTION_INFORMATION_REFRESH_TIME",\ 33:"OPTION_BCMCS_SERVER_D",\ 34:"OPTION_BCMCS_SERVER_A",\ 35:"Unassigned",\ 36:"OPTION_GEOCONF_CIVIC",\ 37:"OPTION_REMOTE_ID",\ 38:"OPTION_SUBSCRIBER_ID",\ 39:"OPTION_CLIENT_FQDN",\ 40:"OPTION_PANA_AGENT",\ 41:"OPTION_NEW_POSIX_TIMEZONE",\ 42:"OPTION_NEW_TZDB_TIMEZONE",\ 43:"OPTION_ERO",\ 44:"OPTION_LQ_QUERY",\ 45:"OPTION_CLIENT_DATA",\ 46:"OPTION_CLT_TIME",\ 47:"OPTION_LQ_RELAY_DATA",\ 48:"OPTION_LQ_CLIENT_LINK",\ 49:"OPTION_MIP6_HNINF",\ 50:"OPTION_MIP6_RELAY",\ 51:"OPTION_V6_LOST",\ 52:"OPTION_CAPWAP_AC_V6",\ 53:"OPTION_RELAY_ID",\ 54:"OPTION-IPv6_Address-MoS",\ 55:"OPTION-IPv6_FQDN-MoS"\ } STATUS_CODE = { 0:"Success",\ 1:"Failure",\ 2:"No Addresses available",\ 3:"No Binding",\ 4:"Prefix not appropriate for link",\ 5:"Use Multicast" }dhcpy6d/dhcpy6/Helpers.py000066400000000000000000000465561242200350000155740ustar00rootroot00000000000000# encoding: utf8 # # DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2014 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import binascii import random import sys import shlex import logging # needed for neighbor cache access import select import socket import struct import binascii import ctypes import platform import time # used for NETLINK in GetNeighborCacheLinux() access by Github/vokac RTM_NEWNEIGH = 28 RTM_DELNEIGH = 29 RTM_GETNEIGH = 30 NLM_F_REQUEST = 1 # Modifiers to GET request NLM_F_ROOT = 0x100 NLM_F_MATCH = 0x200 NLM_F_DUMP = (NLM_F_ROOT | NLM_F_MATCH) # NETLINK message is alsways the same except header seq MSG = struct.pack("B", socket.AF_INET6) # always the same length... MSG_HEADER_LENGTH = 17 # ...type... MSG_HEADER_TYPE = RTM_GETNEIGH # ...flags. MSG_HEADER_FLAGS = (NLM_F_REQUEST | NLM_F_DUMP) # state of peer NUD_REACHABLE = 2 NLMSG_NOOP = 0x1 #/* Nothing. */ NLMSG_ERROR = 0x2 #/* Error */ NLMSG_DONE = 0x3 #/* End of a dump */ NLMSG_OVERRUN = 0x4 #/* Data lost */ NUD_INCOMPLETE = 0x01 NUD_REACHABLE = 0x02 NUD_STALE = 0x04 NUD_DELAY = 0x08 NUD_PROBE = 0x10 NUD_FAILED = 0x20 NUD_NOARP = 0x40 NUD_PERMANENT = 0x80 NUD_NONE = 0x00 NDA = { 0: 'NDA_UNSPEC', 1: 'NDA_DST', 2: 'NDA_LLADDR', 3: 'NDA_CACHEINFO', 4: 'NDA_PROBES', 5: 'NDA_VLAN', 6: 'NDA_PORT', 7: 'NDA_VNI', 8: 'NDA_IFINDEX', } NLMSG_ALIGNTO = 4 NLA_ALIGNTO = 4 # whitespace for options with more than one value WHITESPACE = " ," def ConvertDNS2Binary(name): """ convert domain name as described in RFC 1035, 3.1 """ binary = "" domain_parts = name.split(".") for p in domain_parts: binary += "%02x" % (len(p)) # length of Domain Name Segements binary += binascii.b2a_hex(p) # final zero size octet following RFC 1035 binary += "00" return binary def ConvertBinary2DNS(binary): """ convert domain name from hex like in RFC 1035, 3.1 """ name = "" binary_parts = binary while len(binary_parts) > 0: # RFC 1035 - domain names are sequences of labels separated by length octets length = int(binary_parts[0:2], 16) # lenght*2 because 2 charse represent a byte label = binascii.a2b_hex(binary_parts[2:2+length*2]) binary_parts = binary_parts[2+length*2:] name += label # insert "." if this is not the last label of FQDN # >2 because last byte is the zero byte terminator if len(binary_parts) > 2: name += "." return str(name) def BuildOption(number, payload): """ glue option with payload """ # option number and length take 2 byte each so the string has to be 4 chars long option = "%04x" % (number) # option number option += "%04x" % (len(payload)/2) # payload length, /2 because 2 chars are 1 byte option += payload return option def CorrectMAC(mac): """ OpenBSD shortens MAC addresses in ndp output - here they grow again """ decompressed = map(lambda m: "%02x" % (int(m, 16)), mac.split(":")) return ":".join(decompressed) def ColonifyMAC(mac): """ return complete MAC address with colons """ return ":".join((mac[0:2], mac[2:4], mac[4:6],\ mac[6:8], mac[8:10], mac[10:12])) def DecompressIP6(ip6, strict=True): """ decompresses shortened IPv6 address and returns it as ":"-less 32 character string additionally allows testing for prototype address with less strict set of allowed characters """ # if in strict mode there are no hex numbers and ":" something is wrong if strict == True: for c in ip6.lower(): #if not c in [":", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]: if not c in ":0123456789abcdef": raise Exception('%s should consist only of : 0 1 2 3 4 5 6 7 8 9 a b c d e f' %s (ip6)) #return None else: # used for comparison of leases with address pattern - X replace the dynamic part of the address for c in ip6.lower(): #if not c in [":", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "x"]: if not c in ":0123456789abcdefx": raise Exception('%s should consist only of : 0 1 2 3 4 5 6 7 8 9 a b c d e f x' % (ip6)) #return None # nothing to do if len(ip6) == 32 and ip6.count(":") == 0: return ip6 # larger heaps of :: smell like something wrong if ip6.count("::") > 1 or ip6.count(":::") >= 1: raise Exception('%s has too many accumulated ":"' % (ip6)) # less than 7 ":" but no "::" also make a bad impression if ip6.count(":") < 7 and ip6.count("::") <> 1: raise Exception('%s is missing some ":"' % (ip6)) # replace :: with :0000:: - the last ":" will be cut of finally while ip6.count(":") < 8 and ip6.count("::") == 1: ip6 = ip6.replace("::", ":0000::") # remaining ":" will be cut off ip6 = ip6.replace("::", ":") # ":" at the beginning have to be filled up with 0000 too if ip6.startswith(":"): ip6 = "0000" + ip6 # if a segment is shorter than 4 chars the gaps get filled with zeros ip6_segments_source = ip6.split(":") ip6_segments_target = list() for s in ip6_segments_source: while len(s) < 4: s = "0" + s if len(s) > 4: raise Exception ip6_segments_target.append(s) # return with separator (mostly "") return "".join(ip6_segments_target) def ColonifyIP6(address): """ return complete IPv6 address with colons """ if address: return ":".join((address[0:4], address[4:8], address[8:12], address[12:16],\ address[16:20], address[20:24], address[24:28], address[28:32])) else: return "N/A" def ErrorExit(message="An error occured.", status=1): """ exit with given error message allow prefix, especially for spitting out section of configuration errors """ sys.stderr.write("\n%s\n\n" % (message)) sys.exit(status) def ListifyOption(option): """ return any comma or space separated option as list """ if option: lex = shlex.shlex(option) lex.whitespace = WHITESPACE lex.wordchars += ":.-" return list(lex) else: return None class NeighborCacheRecord(object): """ object for neighbor cache entries to be returned by GetNeighborCacheLinux() and in CollectedMACs .interface is only interesting for real neighbor cache records, to be ignored for collected MACs stored in DB """ def __init__(self, llip="", mac="", interface=""): self.llip = llip self.mac = mac self.interface = interface self.timestamp = time.time() def GetNeighborCacheLinux(cfg, IF_NAME, IF_NUMBER, LIBC, log): """ imported version of https://github.com/vokac/dhcpy6d https://github.com/vokac/dhcpy6d/commit/bd34d3efb18ba6016a2b3afea0b6a3fcdfb524a4 Thanks for donating! """ # result result = dict() # open raw NETLINK socket # NETLINK_ROUTE has neighbor cache information too s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE) # PID 0 means AUTOPID, let socket choose s.bind((0, 0)) pid, groups = s.getsockname() # random sequence for NETLINK access seq = random.randint(0, pow(2,31)) # netlink message header (struct nlmsghdr) MSG_HEADER = struct.pack("IHHII", MSG_HEADER_LENGTH, MSG_HEADER_TYPE, MSG_HEADER_FLAGS, seq, pid) # NETLINK message is always the same except header seq (struct ndmsg) MSG = struct.pack("B", socket.AF_INET6) # send message with header s.send(MSG_HEADER + MSG) # read all data from socket answer = '' while True: r,w,e = select.select([s], [], [], 0.) if s not in r: break # no more data answer += s.recv(16384) result = {} curr_pos = 0 answer_pos = 0 answer_len = len(answer) nlmsghdr_fmt = 'IHHII' # struct nlmsghdr nlattr_fmt = 'HH' # struct nlattr ndmsg_fmt = 'BBHiHBB' # struct ndmsg nlmsg_header_len = (struct.calcsize(nlmsghdr_fmt)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) # alignment to 4 nla_header_len = (struct.calcsize(nlattr_fmt)+NLA_ALIGNTO-1) & ~(NLA_ALIGNTO-1) # alignment to 4 # parse netlink answer to RTM_GETNEIGH try: while answer_pos < answer_len: curr_pos = answer_pos if log.getEffectiveLevel() <= logging.DEBUG: log.debug("nlm[%i:]: parsing up to %i..." % (answer_pos, answer_len)) nlmsg_len, nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid = \ struct.unpack_from("<%s" % nlmsghdr_fmt, answer, answer_pos) # basic safety checks for received data (imitates NLMSG_OK) if nlmsg_len < struct.calcsize("<%s" % nlmsghdr_fmt): log.warn("broken data from netlink (position %i, nlmsg_len %i): "\ "nlmsg_len is smaler then structure size" % (answer_pos, nlmsg_len)) break if answer_len-answer_pos < struct.calcsize("<%s" % nlmsghdr_fmt): log.warn("broken data from netlink (position %i, length avail %i): "\ "received data size is smaler then structure size" % \ (answer_pos, answer_len-answer_pos)) break if answer_len-answer_pos < nlmsg_len: log.warn("broken data from netlink (position %i, length avail %i): "\ "received data size is smaller then nlmsg_len" % \ (answer_pos, answer_len-answer_pos)) break if (pid != nlmsg_pid or seq != nlmsg_seq): log.warn("broken data from netlink (position %i, length avail %i): "\ "invalid seq (%s x %s) or pid (%s x %s)" % \ (answer_pos, answer_len-answer_pos, seq, nlmsg_seq, pid, nlmsg_pid)) break # data for this Routing/device hook record nlmsg_data = answer[answer_pos+nlmsg_header_len:answer_pos+nlmsg_len] if log.getEffectiveLevel() <= logging.DEBUG: log.debug("nlm[%i:%i]%s: %s" % (answer_pos, answer_pos+nlmsg_len, \ str(struct.unpack_from("<%s" % nlmsghdr_fmt, answer, answer_pos)), \ binascii.b2a_hex(nlmsg_data))) if nlmsg_type == NLMSG_DONE: break if nlmsg_type == NLMSG_ERROR: nlmsgerr_error, nlmsgerr_len, nlmsgerr_type, nlmsgerr_flags, nlmsgerr_seq, nlmsgerr_pid = \ struct.unpack_from(" %s, state = %s, %s %s" % (nda.get('NDM_IFINDEX'), IF_NUMBER.get(nda.get('NDM_IFINDEX', '')), ndm_state, nda.get('NDA_DST'), nda.get('NDA_LLADDR'))) if nda['NDM_STATE'] & ~(NUD_INCOMPLETE|NUD_FAILED|NUD_NOARP): if not IF_NUMBER.has_key(nda['NDM_IFINDEX']): log.debug("can't find device for interface index %i" % nda['NDM_IFINDEX']) elif not nda.has_key('NDA_DST'): log.warn("can't find destination address (wrong entry state: %i?!)" % nda['NDM_STATE']) elif not nda.has_key('NDA_LLADDR'): log.warn("can't find local hardware address (wrong entry state: %i?!)" % nda['NDM_STATE']) else: if_name = IF_NUMBER[nda['NDM_IFINDEX']] if if_name in cfg.INTERFACE and not nda['NDA_LLADDR'].startswith('33:33:'): # store neighbor caches entries record = NeighborCacheRecord(llip=DecompressIP6(nda['NDA_DST']), mac=nda['NDA_LLADDR'], interface=if_name) result[str(record.llip)] = record # move to next record answer_pos += nlmsg_len except struct.error, e: log.warn("broken data from netlink (position %i, data[%i:%i] = %s...): %s" % \ (answer_pos, curr_pos, answer_len, \ binascii.b2a_hex(answer[curr_pos:curr_pos+8]), str(e))) # clean up s.close() return result def GetLibC(): """ return libC-object to be used for NIC handling in dhcpy6d and Config.py first get the library to connect to - OS-dependent """ OS = platform.system() if OS == "Linux": libc_name = "libc.so.6" elif "BSD" in OS: # libc_ver() returns version number of libc that is hardcoded in # libc file name libc_name = "libc.so." + platform.libc_ver()[1] elif OS == "Darwin": libc_name = "libc.dylib" else: print "\n OS not yet supported. :-( \n" sys.exit(1) # use ctypes for libc access return ctypes.cdll.LoadLibrary(libc_name) def Log(cfg): """ Logging - has been in dhcpy6d main file, easier to access here for GetNeighborCacheLinux """ log = logging.getLogger("dhcpy6d") if cfg.LOG: formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s") log.setLevel(logging.__dict__[cfg.LOG_LEVEL]) if cfg.LOG_FILE != "": os.chown(cfg.LOG_FILE, pwd.getpwnam(cfg.USER).pw_uid, grp.getgrnam(cfg.GROUP).gr_gid) log_handler = logging.handlers.WatchedFileHandler(cfg.LOG_FILE) log_handler.setFormatter(formatter) log.addHandler(log_handler) # std err console output if cfg.LOG_CONSOLE: log_handler = logging.StreamHandler() log_handler.setFormatter(formatter) log.addHandler(log_handler) if cfg.LOG_SYSLOG: # time should be added by syslog daemon hostname = socket.gethostname().split(".")[0] formatter = logging.Formatter(hostname + " %(name)s %(levelname)s %(message)s") # if /socket/file is given use this as addres if cfg.LOG_SYSLOG_DESTINATION.startswith("/") == True: destination = cfg.LOG_SYSLOG_DESTINATION # if host and port are defined use them... elif cfg.LOG_SYSLOG_DESTINATION.count(":") == 1: destination = tuple(cfg.LOG_SYSLOG_DESTINATION.split(":")) # ...otherwise add port 514 to given host address else: destination = (cfg.LOG_SYSLOG_DESTINATION, 514) log_handler = logging.handlers.SysLogHandler(address=destination,\ facility=logging.handlers.SysLogHandler.__dict__["LOG_" + cfg.LOG_SYSLOG_FACILITY]) log_handler.setFormatter(formatter) log.addHandler(log_handler) return log dhcpy6d/dhcpy6/Storage.py000066400000000000000000000743651242200350000155750ustar00rootroot00000000000000# encoding: utf8 # # DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2014 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import datetime import threading import ConfigParser from Helpers import * import os import pwd import grp class QueryQueue(threading.Thread): """ Pump queries around """ def __init__(self, cfg, store, queryqueue, answerqueue): threading.Thread.__init__(self, name="QueryQueue") self.queryqueue = queryqueue self.answerqueue = answerqueue self.store = store self.setDaemon(1) def run(self): """ receive queries and ask the DB interface for answers which will be put into answer queue """ while True: query = self.queryqueue.get() try: answer = self.store.DBQuery(query) except: import traceback traceback.print_exc(file=sys.stdout) answer = "" self.answerqueue.put(answer) class Store(object): """ abstract class to present MySQL or SQLlite """ def __init__(self, cfg, queryqueue, answerqueue, Transactions, CollectedMACs): self.cfg = cfg self.queryqueue = queryqueue self.answerqueue = answerqueue self.Transactions = Transactions self.CollectedMACs = CollectedMACs # table names used for database storage - MySQL additionally needs the database name self.table_leases = "leases" self.table_macs_llips = "macs_llips" self.table_hosts = "hosts" # flag to check if connection is OK self.connected = False def query(self, query): """ put queries received into query queue and return the answers from answer queue """ self.queryqueue.put(query) answer = self.answerqueue.get() return answer def store_lease(self, transaction_id): """ store lease in lease DB """ # only if client exists if self.Transactions[transaction_id].Client: for a in self.Transactions[transaction_id].Client.Addresses: if not a.ADDRESS is None: query = "SELECT address FROM %s WHERE address = '%s'" % (self.table_leases, a.ADDRESS) answer = self.query(query) if answer != None: # if address is not leased yet add it if len(answer) == 0: query = "INSERT INTO %s (address, active, last_message, preferred_lifetime, valid_lifetime, hostname, type, category, ia_type, class, mac, duid, iaid, last_update, preferred_until, valid_until) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \ (self.table_leases,\ a.ADDRESS,\ 1,\ self.Transactions[transaction_id].LastMessageReceivedType,\ a.PREFERRED_LIFETIME,\ a.VALID_LIFETIME,\ self.Transactions[transaction_id].Client.Hostname,\ a.TYPE,\ a.CATEGORY,\ a.IA_TYPE,\ self.Transactions[transaction_id].Client.Class,\ self.Transactions[transaction_id].MAC,\ self.Transactions[transaction_id].DUID,\ self.Transactions[transaction_id].IAID,\ datetime.datetime.now(),\ datetime.datetime.now() + datetime.timedelta(seconds=int(a.PREFERRED_LIFETIME)),\ datetime.datetime.now() + datetime.timedelta(seconds=int(a.VALID_LIFETIME))) answer = self.query(query) # otherwise update it if not a random address elif a.CATEGORY != "random": query = "UPDATE %s SET active = 1, last_message = %s, preferred_lifetime = '%s', valid_lifetime = '%s',\ hostname = '%s', type = '%s', category = '%s', ia_type = '%s', class = '%s', mac = '%s',\ duid = '%s', iaid = '%s', last_update = '%s', preferred_until = '%s',\ valid_until = '%s'\ WHERE address = '%s'" % \ (self.table_leases,\ self.Transactions[transaction_id].LastMessageReceivedType,\ a.PREFERRED_LIFETIME,\ a.VALID_LIFETIME,\ self.Transactions[transaction_id].Client.Hostname,\ a.TYPE,\ a.CATEGORY,\ a.IA_TYPE,\ self.Transactions[transaction_id].Client.Class,\ self.Transactions[transaction_id].MAC,\ self.Transactions[transaction_id].DUID,\ self.Transactions[transaction_id].IAID,\ datetime.datetime.now(),\ datetime.datetime.now() + datetime.timedelta(seconds=int(a.PREFERRED_LIFETIME)),\ datetime.datetime.now() + datetime.timedelta(seconds=int(a.VALID_LIFETIME)),\ a.ADDRESS) answer = self.query(query) else: # set last message type of random address query = "UPDATE %s SET last_message = %s, active = 1 WHERE address = '%s'" % (self.table_leases, self.Transactions[transaction_id].LastMessageReceivedType, a.ADDRESS) answer = self.query(query) return True # if no client -> False return False def get_range_lease_for_recycling(self, prefix="", frange="", trange="", duid="", mac=""): """ ask DB for last known leases of an already known host to be recycled this is most useful for CONFIRM-requests that will get a not-available-answer but get an ADVERTISE with the last known-as-good address for a client SOLICIT message type is 1 """ query = "SELECT address FROM %s WHERE "\ "category = 'range' AND "\ "'%s' <= address AND "\ "address <= '%s' AND "\ "duid = '%s' AND "\ "mac = '%s' AND "\ "last_message != 1 "\ "ORDER BY last_update DESC LIMIT 1" %\ (self.table_leases, prefix+frange, prefix+trange, duid, mac) answer = self.query(query) # SQLite returns list, MySQL tuple - in case someone wonders here... if not (answer == [] or answer == () or answer == None): return answer[0][0] else: return None def get_highest_range_lease(self, prefix="", frange="", trange=""): """ ask DB for highest known leases - if necessary range sensitive """ query = "SELECT address FROM %s WHERE active = 1 AND "\ "category = 'range' AND "\ "'%s' <= address and address <= '%s' ORDER BY address DESC LIMIT 1" %\ (self.table_leases, prefix+frange, prefix+trange) answer = self.query(query) # SQLite returns list, MySQL tuple - in case someone wonders here... if not (answer == [] or answer == () or answer == None): return answer[0][0] else: return None def get_oldest_inactive_range_lease(self, prefix="", frange="", trange=""): """ ask DB for oldest known inactive lease to minimize chance of collisions ordered by valid_until to get leases that are free as long as possible """ query = "SELECT address FROM %s WHERE active = 0 AND category = 'range' AND "\ "'%s' <= address AND address <= '%s' ORDER BY valid_until ASC LIMIT 1" %\ (self.table_leases, prefix+frange, prefix+trange) answer = self.query(query) # SQLite returns list, MySQL tuple - in case someone wonders here... if not (answer == [] or answer == () or answer == None): return answer[0][0] else: return None def get_host_lease(self, address): """ get the hostname, DUID, MAC and IAID to verify a lease to delete its address in the DNS """ query = "SELECT DISTINCT hostname, duid, mac, iaid FROM leases WHERE address='%s'" % (address) answer = self.query(query) if answer != None and len(answer)>0: if len(answer[0]) > 0: return answer[0] else: # calling method expects quartet of hostname, duid, mac, iad - get None if nothing there return (None, None, None, None) else: return (None, None, None, None) def release_lease(self, address): """ release a lease via setting its active flag to False set last_message to 8 because of RELEASE messages having this message id """ query = "UPDATE %s SET active = 0, last_message = 8, last_update = '%s' WHERE address = '%s'" % (self.table_leases, datetime.datetime.now(), address) answer = self.query(query) def check_number_of_leases(self, prefix="", frange="", trange=""): """ check how many leases are stored - used to find out if address range has been exceeded """ query = "SELECT COUNT(address) FROM leases WHERE address LIKE '%s%%' AND "\ "'%s' <= address AND address <= '%s'" % (prefix, prefix+frange, prefix+trange) answer = self.query(query) # SQLite returns list, MySQL tuple - in case someone wonders here... if not (answer == [] or answer == () or answer == None): return answer[0][0] else: return 0 def check_lease(self, address, transaction_id): """ check state of a lease for REBIND and RENEW messages """ # attributes to identify host and lease query = "SELECT hostname, address, type, category, ia_type, class, preferred_until FROM %s WHERE active = 1\ AND address = '%s' AND mac = '%s' AND duid = '%s' AND iaid = '%s'" % \ (self.table_leases, address,\ self.Transactions[transaction_id].MAC,\ self.Transactions[transaction_id].DUID,\ self.Transactions[transaction_id].IAID) answer = self.query(query) return answer def check_advertised_lease(self, transaction_id="", category="", atype=""): """ check if there are already advertised addresses for client """ # attributes to identify host and lease query = "SELECT address FROM %s WHERE last_message = 1\ AND active = 1\ AND mac = '%s' AND duid = '%s' AND iaid = '%s'\ AND category = '%s' AND type = '%s'" % \ (self.table_leases,\ self.Transactions[transaction_id].MAC,\ self.Transactions[transaction_id].DUID,\ self.Transactions[transaction_id].IAID,\ category,\ atype) answer = self.query(query) # SQLite returns list, MySQL tuple - in case someone wonders here... if not (answer == [] or answer == () or answer == None): return answer[0][0] else: return None def release_free_leases(self, timestamp=datetime.datetime.now()): """ release all invalid leases via setting their active flag to False """ query = "UPDATE %s SET active = 0, last_message = 0 WHERE valid_until < '%s'" % (self.table_leases, timestamp) answer = self.query(query) return answer def remove_leases(self, category="random", timestamp=datetime.datetime.now()): """ remove all leases of a certain category like random - they will grow the database but be of no further use """ query = "DELETE FROM %s WHERE active = 0 AND category = '%s' AND valid_until < '%s'" % (self.table_leases, category, timestamp) answer = self.query(query) return answer def unlock_unused_advertised_leases(self, timestamp=datetime.datetime.now()): """ unlock leases marked as advertised but apparently never been delivered let's say a client should have requested its formerly advertised address after 1 minute """ query = "UPDATE %s SET last_message = 0 WHERE last_message = 1 AND last_update < '%s'" % (self.table_leases, timestamp + datetime.timedelta(seconds=int(60))) answer = self.query(query) return answer def build_config_from_db(self, transaction_id): """ get client config from db and build the appropriate config objects and indices """ if self.Transactions[transaction_id].ClientConfigDB == None: query = "SELECT hostname, mac, duid, class, address, id FROM %s WHERE \ hostname = '%s' OR mac LIKE '%%%s%%' OR duid = '%s'" % \ (self.table_hosts,\ self.Transactions[transaction_id].FQDN,\ self.Transactions[transaction_id].MAC,\ self.Transactions[transaction_id].DUID) answer = self.query(query) # add client config which seems to fit to transaction self.Transactions[transaction_id].ClientConfigDB = ClientConfigDB() # read all sections of config file # a section here is a host # lowering MAC and DUID information in case they where upper in database for host in answer: hostname, mac, duid, aclass, address, id = host # lower some attributes to comply with values from request if mac: mac = mac.lower() if duid: duid = duid.lower() if address: address = address.lower() self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname] = ClientConfig(hostname=hostname,\ mac=mac,\ duid=duid,\ aclass=aclass,\ address=address,\ id=id) # in case of various MAC addresses split them... self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].MAC = ListifyOption(self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].MAC) # and put the host objects into index if self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].MAC: for m in self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].MAC: if not m in self.Transactions[transaction_id].ClientConfigDB.IndexMAC: self.Transactions[transaction_id].ClientConfigDB.IndexMAC[m] = [self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname]] else: self.Transactions[transaction_id].ClientConfigDB.IndexMAC[m].append(self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname]) # add DUIDs to IndexDUID if not self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].DUID == "": if not self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].DUID in self.Transactions[transaction_id].ClientConfigDB.IndexDUID: self.Transactions[transaction_id].ClientConfigDB.IndexDUID[self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].DUID] = [self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname]] else: self.Transactions[transaction_id].ClientConfigDB.IndexDUID[self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname].DUID].append(self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname]) # some cleaning del host, mac, duid, address, aclass, id def get_client_config_by_mac(self, transaction_id): """ get host and its information belonging to that mac """ hosts = list() mac = self.Transactions[transaction_id].MAC if mac in self.Transactions[transaction_id].ClientConfigDB.IndexMAC: hosts.extend(self.Transactions[transaction_id].ClientConfigDB.IndexMAC[mac]) return hosts else: return None def get_client_config_by_duid(self, transaction_id): """ get host and its information belonging to that DUID """ # get client config that most probably seems to fit #self.build_config_from_db(transaction_id) hosts = list() duid = self.Transactions[transaction_id].DUID if duid in self.Transactions[transaction_id].ClientConfigDB.IndexDUID: hosts.extend(self.Transactions[transaction_id].ClientConfigDB.IndexDUID[duid]) return hosts else: return None def get_client_config_by_hostname(self, transaction_id): """ get host and its information by hostname """ # get client config that most probably seems to fit #self.build_config_from_db(transaction_id) hostname = self.Transactions[transaction_id].FQDN.split(".")[0] if hostname in self.Transactions[transaction_id].ClientConfigDB.Hosts: return [self.Transactions[transaction_id].ClientConfigDB.Hosts[hostname]] else: return None def get_client_config(self, hostname="", aclass="", duid="", address=[], mac=[], id=""): """ give back ClientConfig object """ return ClientConfig(hostname=hostname, aclass=aclass, duid=duid, address=address, mac=mac, id=id) def store_mac_llip(self, mac, link_local_ip): """ store MAC-link-local-ip-mapping """ query = "SELECT mac FROM macs_llips WHERE mac='%s'" % (mac) db_entry = self.query(query) # if known already update timestamp of MAC-link-local-ip-mapping if not db_entry: query = "INSERT INTO macs_llips (mac, link_local_ip, last_update) VALUES ('%s', '%s', '%s')" % \ (mac, link_local_ip, datetime.datetime.now()) self.query(query) else: query = "UPDATE macs_llips SET link_local_ip = '%s', last_update = '%s' WHERE mac = '%s'" % (link_local_ip, datetime.datetime.now(), mac) self.query(query) def CollectMACsFromDB(self): """ collect all known MACs and link local addresses from database at startup to reduce attempts to read neighbor cache """ query = 'SELECT link_local_ip, mac FROM %s' % (self.table_macs_llips) answer = self.query(query) if answer: for m in answer: try: # m[0] is LLIP, m[1] is the matching MAC # interface is ignored and timestamp comes with instance of NeighborCacheRecord() self.CollectedMACs[m[0]] = NeighborCacheRecord(llip=m[0], mac=m[1]) except Exception, err: #Log("ERROR: CollectMACsFromDB(): " + str(err)) print err import traceback traceback.print_exc(file=sys.stdout) return None def DBQuery(self, query): """ no not execute query on DB - dummy """ # return empty tuple as dummy return () class SQLite(Store): """ file-based SQLite database, might be an option for single installations """ def __init__(self, cfg, queryqueue, answerqueue, Transactions, CollectedMACs, storage_type="volatile"): Store.__init__(self, cfg, queryqueue, answerqueue, Transactions, CollectedMACs) self.connection = None try: self.DBConnect(storage_type) except: import traceback traceback.print_exc(file=sys.stdout) def DBConnect(self, storage_type="volatile"): """ Initialize DB connection """ import sqlite3 try: if storage_type == "volatile": storage = self.cfg.STORE_SQLITE_VOLATILE # set ownership of storage file according to settings os.chown(self.cfg.STORE_SQLITE_VOLATILE, pwd.getpwnam(self.cfg.USER).pw_uid, grp.getgrnam(self.cfg.GROUP).gr_gid) if storage_type == "config": storage = self.cfg.STORE_SQLITE_CONFIG self.connection = sqlite3.connect(storage, check_same_thread = False) self.cursor = self.connection.cursor() self.connected = True except: import traceback traceback.print_exc(file=sys.stdout) return None def DBQuery(self, query): """ execute query on DB """ try: answer = self.cursor.execute(query) # commit only if explicitly wanted if query.startswith("INSERT"): self.connection.commit() if query.startswith("UPDATE"): self.connection.commit() self.connected = True except: self.connected = False return None return answer.fetchall() class Textfile(Store): """ client config in text files """ def __init__(self, cfg, queryqueue, answerqueue, Transactions, CollectedMACs): Store.__init__(self, cfg, queryqueue, answerqueue, Transactions, CollectedMACs) self.connection = None # store config information of hosts self.Hosts = dict() self.IndexMAC = dict() self.IndexDUID = dict() # store IDs for ID-based hosts to check if there are duplicates self.IDs = dict() # instantiate a Configparser config = ConfigParser.ConfigParser() config.read(self.cfg.STORE_FILE_CONFIG) # read al sections of config file # a section here is a host for section in config.sections(): self.Hosts[section] = ClientConfig() for item in config.items(section): # lowercase all MAC addresses, DUIDs and IPv6 addresses if item[0].upper() in ["MAC", "DUID", "ADDRESS"]: self.Hosts[section].__setattr__(item[0].upper(), str(item[1]).lower()) else: self.Hosts[section].__setattr__(item[0].upper(), str(item[1])) # Test if host has ID if cfg.CLASSES.has_key(self.Hosts[section].CLASS): for a in cfg.CLASSES[self.Hosts[section].CLASS].ADDRESSES: if cfg.ADDRESSES[a].CATEGORY == "id" and self.Hosts[section].ID == "": ErrorExit("Textfile client configuration: No ID given for client '%s'" % (self.Hosts[section].HOSTNAME)) else: ErrorExit("Textfile client configuration: Class '%s' of host '%s' is not defined" % (self.Hosts[section].CLASS, self.Hosts[section].HOSTNAME)) if self.Hosts[section].ID != "": if self.Hosts[section].ID in self.IDs.keys(): ErrorExit("Textfile client configuration: ID '%s' of client '%s' is already used by '%s'." % (self.Hosts[section].ID, self.Hosts[section].HOSTNAME, self.IDs[self.Hosts[section].ID])) else: self.IDs[self.Hosts[section].ID] = self.Hosts[section].HOSTNAME # in case of various MAC addresses split them... self.Hosts[section].MAC = ListifyOption(self.Hosts[section].MAC) # and put the host objects into index if self.Hosts[section].MAC: for m in self.Hosts[section].MAC: if not m in self.IndexMAC: self.IndexMAC[m] = [self.Hosts[section]] else: self.IndexMAC[m].append(self.Hosts[section]) # add DUIDs to IndexDUID if not self.Hosts[section].DUID == "": if not self.Hosts[section].DUID in self.IndexDUID: self.IndexDUID[self.Hosts[section].DUID] = [self.Hosts[section]] else: self.IndexDUID[self.Hosts[section].DUID].append(self.Hosts[section]) # not very meaningful in case of databaseless textfile config but for completeness self.connected = True def get_client_config_by_mac(self, transaction_id): """ get host(s?) and its information belonging to that mac """ hosts = list() mac = self.Transactions[transaction_id].MAC if mac in self.IndexMAC: hosts.extend(self.IndexMAC[mac]) return hosts else: return None def get_client_config_by_duid(self, transaction_id): """ get host and its information belonging to that DUID """ hosts = list() duid = self.Transactions[transaction_id].DUID if duid in self.IndexDUID: hosts.extend(self.IndexDUID[duid]) return hosts else: return None def get_client_config_by_hostname(self, transaction_id): """ get host and its information by hostname """ hostname = self.Transactions[transaction_id].FQDN.split(".")[0] if hostname in self.Hosts: return [self.Hosts[hostname]] else: return None def get_client_config(self, hostname="", aclass="", duid="", address=[], mac=[], id=""): """ give back ClientConfig object """ return ClientConfig( hostname=hostname, aclass=aclass, duid=duid, address=address, mac=mac, id=id) class ClientConfig(object): """ static client settings object to be stuffed into Hosts dict of Textfile store """ def __init__(self, hostname="", aclass="default", duid="", address=None, mac=None, id=""): self.HOSTNAME = hostname # MACs self.MAC = mac # fixed addresses if address: addresses = ListifyOption(address) for a in addresses: self.ADDRESS.append(DecompressIP6(a)) else: self.ADDRESS = None self.CLASS = aclass self.ID = id self.DUID = duid class ClientConfigDB(object): """ class for storing client config snippet from DB - used in SQLite and MySQL Storage """ def __init__(self): self.Hosts = dict() self.IndexMAC = dict() self.IndexDUID = dict() class MySQL(Store): """ MySQL database interface for robustness see http://stackoverflow.com/questions/207981/how-to-enable-mysql-client-auto-re-connect-with-mysqldb """ def __init__(self, cfg, queryqueue, answerqueue, Transactions, CollectedMACs): Store.__init__(self, cfg, queryqueue, answerqueue, Transactions, CollectedMACs) self.connection = None try: self.DBConnect() except: import traceback traceback.print_exc(file=sys.stdout) def DBConnect(self): import MySQLdb try: self.connection = MySQLdb.connect(host=self.cfg.STORE_MYSQL_HOST,\ db=self.cfg.STORE_MYSQL_DB,\ user=self.cfg.STORE_MYSQL_USER,\ passwd=self.cfg.STORE_MYSQL_PASSWORD) self.cursor = self.connection.cursor() self.connected = True except: import traceback traceback.print_exc(file=sys.stdout) return None def DBQuery(self, query): try: self.cursor.execute(query) self.connected = True except: import traceback traceback.print_exc(file=sys.stdout) # try to reestablish database connection if not self.DBConnect(): self.connected = False return None else: try: self.cursor.execute(query) self.connected = True except: import traceback traceback.print_exc(file=sys.stdout) self.connected = False return None result = self.cursor.fetchall() return result dhcpy6d/dhcpy6/__init__.py000077500000000000000000000015021242200350000157120ustar00rootroot00000000000000# encoding: utf8 # # DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2014 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Module dhcpy6d""" dhcpy6d/dhcpy6d000077500000000000000000002616341242200350000137060ustar00rootroot00000000000000#!/usr/bin/env python # encoding: utf8 # # DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2014 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import socket import struct import ctypes import platform import binascii import datetime import commands import shlex import sys import time import threading import Queue import os import re import SocketServer import traceback import copy import logging import logging.handlers import pwd import grp # access /usr/share/pyshared on Debian # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=715010 if platform.dist()[0].lower() == "debian": sys.path[0:0] = ["/usr/share/pyshared"] from dhcpy6.Helpers import * from dhcpy6.Constants import * from dhcpy6.Config import * from dhcpy6.Storage import * # create and read config file cfg = Config() # RNDC Key for DNS updates from ISC Bind /etc/rndc.key if cfg.DNS_UPDATE: import dns.update import dns.tsigkeyring import dns.query import dns.resolver import dns.reversename Keyring = dns.tsigkeyring.from_text({cfg.DNS_RNDC_KEY : cfg.DNS_RNDC_SECRET}) # resolver for DNS updates Resolver = dns.resolver.Resolver() Resolver.nameservers = [cfg.DNS_UPDATE_NAMESERVER] # Logging log = logging.getLogger("dhcpy6d") if cfg.LOG: formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s") log.setLevel(logging.__dict__[cfg.LOG_LEVEL]) if cfg.LOG_FILE != "": os.chown(cfg.LOG_FILE, pwd.getpwnam(cfg.USER).pw_uid, grp.getgrnam(cfg.GROUP).gr_gid) log_handler = logging.handlers.WatchedFileHandler(cfg.LOG_FILE) log_handler.setFormatter(formatter) log.addHandler(log_handler) # std err console output if cfg.LOG_CONSOLE: log_handler = logging.StreamHandler() log_handler.setFormatter(formatter) log.addHandler(log_handler) if cfg.LOG_SYSLOG: # time should be added by syslog daemon hostname = socket.gethostname().split(".")[0] formatter = logging.Formatter(hostname + " %(name)s %(levelname)s %(message)s") # if /socket/file is given use this as addres if cfg.LOG_SYSLOG_DESTINATION.startswith("/") == True: destination = cfg.LOG_SYSLOG_DESTINATION # if host and port are defined use them... elif cfg.LOG_SYSLOG_DESTINATION.count(":") == 1: destination = tuple(cfg.LOG_SYSLOG_DESTINATION.split(":")) # ...otherwise add port 514 to given host address else: destination = (cfg.LOG_SYSLOG_DESTINATION, 514) log_handler = logging.handlers.SysLogHandler(address=destination,\ facility=logging.handlers.SysLogHandler.__dict__["LOG_" + cfg.LOG_SYSLOG_FACILITY]) log_handler.setFormatter(formatter) log.addHandler(log_handler) # dictionary to store transactions - key is transaction ID, value a transaction object Transactions = dict() # collected MAC addresses from clients, mapping to link local IPs CollectedMACs = dict() # queues for queries configqueryqueue = Queue.Queue() configanswerqueue = Queue.Queue() volatilequeryqueue = Queue.Queue() volatileanswerqueue = Queue.Queue() # queue for dns actualization dnsqueue = Queue.Queue() # save OS OS = platform.system() if "BSD" in OS: OS = "BSD" # platform-dependant neighbor cache call # every platform has its different output # dev, llip and mac are positions of output of call # len is minimal length a line has to have to be evaluable # # update: has been different to Linux which now access neighbor cache natively # left here as-is just in case there will be other OS one day NC = { "BSD": { "call" : "/usr/sbin/ndp -a -n", "dev" : 2, "llip" : 0, "mac" : 1, "len" : 3}, "Darwin": { "call" : "/usr/sbin/ndp -a -n", "dev" : 2, "llip" : 0, "mac" : 1, "len" : 3} } # libc access via ctypes, needed for interface handling, get it by Helpers.GetLibC() LIBC = GetLibC() # index IF name > number, gets filled in UDPMulticastIPv6 IF_NAME = dict() # index IF number > name IF_NUMBER = dict() # store # because of thread trouble there should not be too much db connections at once # so we need to use the queryqueue way - subject to change # source of configuration of hosts # use client configuration only if needed if cfg.STORE_CONFIG: if cfg.STORE_CONFIG == "file": configstore = Textfile(cfg, configqueryqueue, configanswerqueue, Transactions, CollectedMACs) if cfg.STORE_CONFIG == "mysql": configstore = MySQL(cfg, configqueryqueue, configanswerqueue, Transactions, CollectedMACs) if cfg.STORE_CONFIG == "sqlite": configstore = SQLite(cfg, configqueryqueue, configanswerqueue, Transactions, CollectedMACs, storage_type="config") else: # dummy configstore if no client config is needed configstore = Store(cfg, configqueryqueue, configanswerqueue, Transactions, CollectedMACs) # "none" store is always connected configstore.connected = True # storage for changing data like leases, LLIPs, DUIDs etc. if cfg.STORE_VOLATILE == "mysql": volatilestore = MySQL(cfg, volatilequeryqueue, volatileanswerqueue, Transactions, CollectedMACs) if cfg.STORE_VOLATILE == "sqlite": volatilestore = SQLite(cfg, volatilequeryqueue, volatileanswerqueue, Transactions, CollectedMACs, storage_type="volatile") # do not start if no database connection exists if not configstore.connected: print "\n Configuration database is not connected!\n" sys.exit(1) if not volatilestore.connected: print "\n Database for volatile data is not connected!\n" sys.exit(1) def LegacyAdjustments(): """ adjust some existing data to work with newer versions of dhcpy6d """ try: if volatilestore.query("SELECT last_message FROM leases LIMIT 1") == None: # row 'last_message' in tables 'leases' does not exist yet, comes with version 0.1.6 volatilestore.query('ALTER TABLE leases ADD last_message INT NOT NULL DEFAULT 0') log.info("Adding row 'last_message' to table 'leases' in volatile storage succeeded.") except: print "\n'ALTER TABLE leases ADD last_message INT NOT NULL DEFAULT 0' on volatile database failed." print "Please apply manually or grant necessary permissions.\n" sys.exit(1) def BuildClient(transaction_id): """ builds client object of client config and transaction data checks if filters apply check if lease is still valid for RENEW and REBIND answers check if invalid addresses need to get deleted with lifetime 0 """ try: # create client object client = Client() # configuration from client deriving from general config or filters - defaults to none client_config = None # list to collect filtered client information # if there are more than one entries that do not match the class is not uniquely identified filtered_class = dict() # check if there are identification attributes of any class - classes are sorted by filter types for f in cfg.FILTERS: # look into all classes and their filters for c in cfg.FILTERS[f]: # check further only if class applies to interface if Transactions[transaction_id].Interface in c.INTERFACE: # MACs if c.FILTER_MAC != "": pattern = re.compile(c.FILTER_MAC) # if mac filter fits client mac address add client config if len(pattern.findall(Transactions[transaction_id].MAC)) > 0: client_config = configstore.get_client_config(hostname=Transactions[transaction_id].Hostname,\ mac=[Transactions[transaction_id].MAC],\ duid=Transactions[transaction_id].DUID,\ aclass=c.NAME) # add classname to dictionary - if there are more than one entry classes do not match # and thus are invalid filtered_class[c.NAME] = c # DUIDs if c.FILTER_DUID != "": pattern = re.compile(c.FILTER_DUID) # if duid filter fits client duid address add client config if len(pattern.findall(Transactions[transaction_id].DUID)) > 0: client_config = configstore.get_client_config(hostname=Transactions[transaction_id].Hostname,\ mac=[Transactions[transaction_id].MAC],\ duid=Transactions[transaction_id].DUID,\ aclass=c.NAME) # see above filtered_class[c.NAME] = c # HOSTNAMEs if c.FILTER_HOSTNAME != "": pattern = re.compile(c.FILTER_HOSTNAME) # if hostname filter fits client hostname address add client config if len(pattern.findall(Transactions[transaction_id].Hostname)) > 0: client_config = configstore.get_client_config(hostname=Transactions[transaction_id].Hostname,\ mac=[Transactions[transaction_id].MAC],\ duid=Transactions[transaction_id].DUID,\ aclass=c.NAME) # see above filtered_class[c.NAME] = c # if there are more than 1 different classes matching for the client they are not valid if len(filtered_class) != 1: client_config = None # if filters did not get a result try it the hard way if client_config == None: # check all given identification criteria - if they all match each other the client is identified id_attributes = list() # get client config that most probably seems to fit configstore.build_config_from_db(transaction_id) # check every attribute which is required # depending on identificaton mode empty results are ignored or considered # finally all attributes are grouped in sets and for a correctly identified host # only one entry should appear at the end for i in cfg.IDENTIFICATION: if i == "mac": # get all MACs for client from config macs = configstore.get_client_config_by_mac(transaction_id) if macs: macs = set(macs) id_attributes.append("macs") elif cfg.IDENTIFICATION_MODE == "match_all": macs = set() id_attributes.append("macs") if i == "duid": duids = configstore.get_client_config_by_duid(transaction_id) if duids: duids = set(duids) id_attributes.append("duids") elif cfg.IDENTIFICATION_MODE == "match_all": duids = set() id_attributes.append("duids") if i == "hostname": hostnames = configstore.get_client_config_by_hostname(transaction_id) if hostnames: hostnames = set(hostnames) id_attributes.append("hostnames") elif cfg.IDENTIFICATION_MODE == "match_all": hostnames = set() id_attributes.append("hostnames") # get intersection of all sets of identifying attributes - even the empty ones if len(id_attributes) > 0: client_config = set.intersection(eval("&".join(id_attributes))) # if exactly one client has been identified use that config if len(client_config) == 1: # reuse client_config, grab it out of the set client_config = client_config.pop() else: # in case there is no client config we should maybe log this? client_config = None else: client_config = None # If client gave some addresses for RENEW or REBIND consider them if Transactions[transaction_id].LastMessageReceivedType in (5, 6) and\ not len(Transactions[transaction_id].Addresses) == 0: if not client_config == None: # give client hostname client.Hostname = client_config.HOSTNAME client.Class = client_config.CLASS # apply answer type of client to transaction - useful if no answer or no address available is configured Transactions[transaction_id].Answer = cfg.CLASSES[client.Class].ANSWER else: # use default class if host is unknown client.Hostname = Transactions[transaction_id].Hostname client.Class = "default_" + Transactions[transaction_id].Interface # apply answer type of client to transaction - useful if no answer or no address available is configured Transactions[transaction_id].Answer = cfg.CLASSES[client.Class].ANSWER for address in Transactions[transaction_id].Addresses: # check_lease returns hostname, address, type, category, ia_type, class, preferred_until of leased address answer = volatilestore.check_lease(address, transaction_id) if answer: if len(answer) > 0: for item in answer: a = dict(zip(("hostname", "address", "type", "category", "ia_type", "class", "preferred_until"), item)) # if lease exists but no configured client set class to default if client_config == None: client.Hostname = Transactions[transaction_id].Hostname client.Class = "default_" + Transactions[transaction_id].Interface # check if address type of lease still exists in configuration # and if request interface matches that of class if a["class"] in cfg.CLASSES and client.Class == a["class"] and\ Transactions[transaction_id].Interface in cfg.CLASSES[client.Class].INTERFACE: # type of address must be defined in addresses for this class # or fixed - in which case it is not class related if a["type"] in cfg.CLASSES[a["class"]].ADDRESSES or a["type"] == "fixed": # flag for lease usage use_lease = True # test lease validity against address prototype pattern only if not fixed if a["category"] != "fixed": # test if address matches pattern for i in range(len(address)): if address[i] != cfg.ADDRESSES[a["type"]].PROTOTYPE[i] and \ cfg.ADDRESSES[a["type"]].PROTOTYPE[i] != "X": use_lease = False break elif not address in client_config.ADDRESS: use_lease = False # only use lease if it still matches prototype if use_lease == True: # when category is range, test if it still applies if a["category"] == "range": # borrowed from ParseAddressPattern to find out if lease is still in a meanwhile maybe changed range frange, trange = cfg.ADDRESSES[a["type"]].RANGE.split("-") # correct possible misconfiguration if len(frange)<4: frange ="0"*(4-len(frange)) + frange if len(trange)<4: trange ="0"*(4-len(trange)) + trange if frange > trange: frange, trange = trange, frange # if lease is still inside range boundaries use it if frange <= address[28:].lower() < trange: # build IA partly of leases db, partly of config db ia = ClientAddress(address=a["address"],\ atype=a["type"],\ preferred_lifetime=cfg.ADDRESSES[a["type"]].PREFERRED_LIFETIME,\ valid_lifetime=cfg.ADDRESSES[a["type"]].VALID_LIFETIME,\ category=a["category"],\ ia_type=a["ia_type"],\ aclass=a["class"],\ dns_update=cfg.ADDRESSES[a["type"]].DNS_UPDATE,\ dns_zone=cfg.ADDRESSES[a["type"]].DNS_ZONE,\ dns_rev_zone=cfg.ADDRESSES[a["type"]].DNS_REV_ZONE,\ dns_ttl=cfg.ADDRESSES[a["type"]].DNS_TTL) client.Addresses.append(ia) # de-preferred random address has to be deleted and replaced elif a["category"] == "random" and str(datetime.datetime.now()) > str(a["preferred_until"]): # create new random address if old one is depreferred random_address = ParseAddressPattern(cfg.ADDRESSES[a["type"]], client_config, transaction_id) # create new random address if old one is de-preferred # do not wait until it is invalid if not random_address == None: ia = ClientAddress(address=random_address, ia_type=cfg.ADDRESSES[a["type"]].IA_TYPE,\ preferred_lifetime=cfg.ADDRESSES[a["type"]].PREFERRED_LIFETIME,\ valid_lifetime=cfg.ADDRESSES[a["type"]].VALID_LIFETIME,\ category="random",\ aclass=cfg.ADDRESSES[a["type"]].CLASS,\ atype=cfg.ADDRESSES[a["type"]].TYPE,\ dns_update=cfg.ADDRESSES[a["type"]].DNS_UPDATE,\ dns_zone=cfg.ADDRESSES[a["type"]].DNS_ZONE,\ dns_rev_zone=cfg.ADDRESSES[a["type"]].DNS_REV_ZONE,\ dns_ttl=cfg.ADDRESSES[a["type"]].DNS_TTL) client.Addresses.append(ia) # set de-preferred address invalid client.Addresses.append(ClientAddress(address=a["address"], valid=False,\ preferred_lifetime=0,\ valid_lifetime=0)) else: # build IA partly of leases db, partly of config db ia = ClientAddress(address=a["address"],\ atype=a["type"],\ preferred_lifetime=cfg.ADDRESSES[a["type"]].PREFERRED_LIFETIME,\ valid_lifetime=cfg.ADDRESSES[a["type"]].VALID_LIFETIME,\ category=a["category"],\ ia_type=a["ia_type"],\ aclass=a["class"],\ dns_update=cfg.ADDRESSES[a["type"]].DNS_UPDATE,\ dns_zone=cfg.ADDRESSES[a["type"]].DNS_ZONE,\ dns_rev_zone=cfg.ADDRESSES[a["type"]].DNS_REV_ZONE,\ dns_ttl=cfg.ADDRESSES[a["type"]].DNS_TTL) client.Addresses.append(ia) # important indent here, has to match for...addresses-loop! # look for addresses in transaction that are invalid and add them # to client addresses with flag invalid and a RFC-compliant lifetime of 0 for a in set(Transactions[transaction_id].Addresses).difference(map(lambda x: DecompressIP6(x.ADDRESS), client.Addresses)): client.Addresses.append(ClientAddress(address=a, valid=False,\ preferred_lifetime=0,\ valid_lifetime=0)) return client # build IA addresses from config - fixed ones and dynamic if client_config != None: # give client hostname + class client.Hostname = client_config.HOSTNAME client.Class = client_config.CLASS # apply answer type of client to transaction - useful if no answer or no address available is configured Transactions[transaction_id].Answer = cfg.CLASSES[client.Class].ANSWER # continue only if request interface matches class interfaces if Transactions[transaction_id].Interface in cfg.CLASSES[client.Class].INTERFACE: # if fixed addresses are given build them if not client_config.ADDRESS == None: for address in client_config.ADDRESS: if len(address) > 0: # fixed addresses are assumed to be non-temporary # # todo: lifetime of address should be set by config too # ia = ClientAddress(address=address, ia_type="na",\ preferred_lifetime=cfg.PREFERRED_LIFETIME,\ valid_lifetime=cfg.VALID_LIFETIME, category="fixed",\ aclass="fixed", atype="fixed") client.Addresses.append(ia) if not client_config.CLASS == "": # add all addresses which belong to that class for address in cfg.CLASSES[client_config.CLASS].ADDRESSES: a = ParseAddressPattern(cfg.ADDRESSES[address], client_config, transaction_id) # in case range has been exceeded a will be None if a: ia = ClientAddress(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE,\ preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME,\ valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME,\ category=cfg.ADDRESSES[address].CATEGORY,\ aclass=cfg.ADDRESSES[address].CLASS,\ atype=cfg.ADDRESSES[address].TYPE,\ dns_update=cfg.ADDRESSES[address].DNS_UPDATE,\ dns_zone=cfg.ADDRESSES[address].DNS_ZONE,\ dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE,\ dns_ttl=cfg.ADDRESSES[address].DNS_TTL) client.Addresses.append(ia) if client_config.ADDRESS == client_config.CLASS == "": # use default class if no class or address is given for address in cfg.CLASS["default_" + Transactions[transaction_id].Interface].ADDRESSES: client.Class = "default_" + Transactions[transaction_id].Interface a = ParseAddressPattern(cfg.ADDRESSES[address], client_config, transaction_id) if a: ia = ClientAddress(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE,\ preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME,\ valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME,\ category=cfg.ADDRESSES[address].CATEGORY,\ aclass=cfg.ADDRESSES[address].CLASS,\ atype=cfg.ADDRESSES[address].TYPE,\ dns_update=cfg.ADDRESSES[address].DNS_UPDATE,\ dns_zone=cfg.ADDRESSES[address].DNS_ZONE,\ dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE,\ dns_ttl=cfg.ADDRESSES[address].DNS_TTL) client.Addresses.append(ia) else: # use default class if host is unknown client.Hostname = Transactions[transaction_id].Hostname client.Class = "default_" + Transactions[transaction_id].Interface # apply answer type of client to transaction - useful if no answer or no address available is configured Transactions[transaction_id].Answer = cfg.CLASSES[client.Class].ANSWER for address in cfg.CLASSES["default_" + Transactions[transaction_id].Interface].ADDRESSES: a = ParseAddressPattern(cfg.ADDRESSES[address], client, transaction_id) if a: ia = ClientAddress(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE,\ preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME,\ valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME,\ category=cfg.ADDRESSES[address].CATEGORY,\ aclass=cfg.ADDRESSES[address].CLASS,\ atype=cfg.ADDRESSES[address].TYPE,\ dns_update=cfg.ADDRESSES[address].DNS_UPDATE,\ dns_zone=cfg.ADDRESSES[address].DNS_ZONE,\ dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE,\ dns_ttl=cfg.ADDRESSES[address].DNS_TTL) client.Addresses.append(ia) return client except Exception,err: import traceback traceback.print_exc(file=sys.stdout) log.error("BuildClient(): " + str(err)) return None def ParseAddressPattern(address, client_config, transaction_id): """ parse address pattern and replace variables with current values """ # parse all pattern parts a = address.PATTERN # check different client address categories - to be extended! if address.CATEGORY == "mac": macraw = "".join(Transactions[transaction_id].MAC.split(":")) a = a.replace("$mac$", ":".join((macraw[0:4], macraw[4:8], macraw[8:12]))) elif address.CATEGORY == "id": # if there is an ID build address if str(client_config.ID) <> "": a = a.replace("$id$", str(client_config.ID)) else: return None elif address.CATEGORY == "random": # first check if address already has been advertised advertised_address = volatilestore.check_advertised_lease(transaction_id, category="random", atype=address.TYPE) # when address already has been advertised for this client use it if advertised_address: a = advertised_address else: ra = str(hex(random.getrandbits(64)))[2:][:-1] ra = ":".join((ra[0:4], ra[4:8], ra[8:12], ra[12:16])) # subject to change.... a = a.replace("$random64$", ra) elif address.CATEGORY == "range": frange, trange = address.RANGE.split("-") if len(frange)<4: frange ="0"*(4-len(frange)) + frange if len(trange)<4: trange ="0"*(4-len(trange)) + trange if frange > trange: frange, trange = trange, frange # expecting range-range at the last octet, "prefix" means the first seven octets here # - is just shorter than the_first_seven_octets prefix = DecompressIP6(a.replace("$range$", "0000"))[:28] # the following steps are done to find a collision-free lease in given range # check if address already has been advertised - important for REPLY after SOLICIT-ADVERTISE-REQUEST advertised_address = volatilestore.check_advertised_lease(transaction_id, category="range", atype=address.TYPE) # when address already has been advertised for this client use it if advertised_address: a = advertised_address else: # check if requesting client still has an active lease that could be reused lease = volatilestore.get_range_lease_for_recycling(prefix=prefix, frange=frange, trange=trange,\ duid=Transactions[transaction_id].DUID,\ mac=Transactions[transaction_id].MAC) # the found lease has to be in range - important after changed range boundaries if not lease is None and frange <= lease[28:].lower() <= trange: a = ":".join((lease[0:4], lease[4:8], lease[8:12], lease[12:16],\ lease[16:20], lease[20:24], lease[24:28], lease[28:32])) else: # get highest active lease to increment address about 1 lease = volatilestore.get_highest_range_lease(prefix=prefix, frange=frange, trange=trange) # check if highest active lease still fits into range if not lease is None and frange <= lease[28:].lower() < trange: # if highest lease + 1 would not fit range limit is reached if lease[28:].lower() >= trange: # try to get one of the inactive old leases lease = volatilestore.get_oldest_inactive_range_lease(prefix=prefix, frange=frange, trange=trange) if lease is None: # if none is available limit is reached and nothing returned log.critical("Address space %s[%s-%s] exceeded" % (prefix, frange,trange)) return None else: # if lease is OK use it a = lease else: # otherwise increase current maximum range limit by 1 a = a.replace("$range$", str(hex(int(lease[28:], 16) + 1)).split("x")[1]) else: # if there is no lease yet or range limit is reached try to reactivate an old inactive lease lease = volatilestore.get_oldest_inactive_range_lease(prefix=prefix, frange=frange, trange=trange) if lease is None: # if there are no leases stored yet initiate lease storage # this will be done only once - the first time if there is no other lease yet # so it is safe to start from frange if volatilestore.check_number_of_leases(prefix, frange, trange) <= 1: a = a.replace("$range$", frange) else: # if none is available limit is reached and nothing returned log.critical("Address space %s[%s-%s] exceeded" % (prefix, frange,trange)) return None else: # if there is a lease it might be used a = lease return DecompressIP6(a) def CollectMACs(): """ collect MAC address from clients to link local addresses with MACs if a client has a new MAC the LLIP changes - with privacy extension enabled anyway calls local ip command to get neighbor cache - any more sophisticated idea is welcome! The Linux netlink method is considered stable now. """ try: # Linux can use kernel neighbor cache if OS == "Linux": for host in GetNeighborCacheLinux(cfg, IF_NAME, IF_NUMBER, LIBC, log).values(): if host.interface not in cfg.INTERFACE: continue if not CollectedMACs.has_key(host.llip) and host.llip.lower().startswith("fe80"): CollectedMACs[str(host.llip)] = host if cfg.LOG_MAC_LLIP == True: log.info("Collected MAC %s for LinkLocalIP %s" % (host.mac, ColonifyIP6(host.llip))) if cfg.CACHE_MAC_LLIP == True: volatilestore.store_mac_llip(host.mac, host.llip) else: # subject to change - other distros might have other paths - might become a task # for a setup routine to find appropriate paths for host in commands.getoutput(NC[OS]["call"]).splitlines(): # get fragments of output line f = shlex.split(host) if f[NC[OS]["dev"]] in cfg.INTERFACE and len(f) >= NC[OS]["len"] : # get rid of %interface f[NC[OS]["llip"]] = DecompressIP6(f[NC[OS]["llip"]].split("%")[0]) # correct maybe shortened MAC f[NC[OS]["mac"]] = CorrectMAC(f[NC[OS]["mac"]]) # put non yet existing LLIPs into dictionary - if they have MACs if not CollectedMACs.has_key(f[NC[OS]["llip"]]) and f[NC[OS]["llip"]].lower().startswith("fe80")\ and ":" in f[NC[OS]["mac"]]: CollectedMACs[f[NC[OS]["llip"]]] = NeighborCacheRecord(llip=f[NC[OS]["llip"]], mac=f[NC[OS]["mac"]], interface=f[NC[OS]["dev"]]) if cfg.LOG_MAC_LLIP == True: log.info("Collected MAC %s for LinkLocalIP %s" % (f[NC[OS]["mac"]], ColonifyIP6(f[NC[OS]["llip"]]))) volatilestore.store_mac_llip(f[NC[OS]["mac"]], f[NC[OS]["llip"]]) except Exception,err: import traceback traceback.print_exc(file=sys.stdout) log.error("CollectMACs(): " + str(err)) def DNSUpdate(transaction_id, action="update"): """ update DNS entries on specified nameserver at the moment this only works with Bind uses all addresses of client if they want to be dynamically updated regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: - client wants to update DNS itself -> sends 0 0 0 - client wants server to update DNS -> sends 0 0 1 - client wants no server DNS update -> sends 1 0 0 """ if Transactions[transaction_id].Client: # if allowed use client supplied hostname, otherwise that from config if cfg.DNS_USE_CLIENT_HOSTNAME and not cfg.DNS_IGNORE_CLIENT: hostname = Transactions[transaction_id].Hostname else: hostname = Transactions[transaction_id].Client.Hostname # if address should be updated in DNS update it for a in Transactions[transaction_id].Client.Addresses: if a.DNS_UPDATE and hostname != "" and a.VALID == True: if cfg.DNS_IGNORE_CLIENT or Transactions[transaction_id].DNS_S == 1: # put query into DNS query queue dnsqueue.put((hostname, a, action)) return True else: return False def DNSDelete(transaction_id, address="", action="release"): """ delete DNS entries on specified nameserver at the moment this only works with ISC Bind """ hostname, duid, mac, iaid = volatilestore.get_host_lease(address) # if address should be updated in DNS update it # local flag to check if address should be deleted from DNS delete = False for a in cfg.ADDRESSES.values(): # if there is any address type which prototype matches use its DNS ZONE if a.matches_prototype(address): # kind of RCF-compliant security measure - check if hostname and DUID from transaction fits them of store if duid == Transactions[transaction_id].DUID and\ iaid == Transactions[transaction_id].IAID: delete = True # also check MAC address if MAC counts in general - not RFCish if "mac" in cfg.IDENTIFICATION: if not mac == Transactions[transaction_id].MAC: delete = False if hostname != "" and delete == True: # use address from address types as template for the real # address to be deleted from DNS dns_address = copy.copy(a) dns_address.ADDRESS = ColonifyIP6(address) # put query into DNS query queue dnsqueue.put((hostname, dns_address, action)) # enough break class DNSQueryThread(threading.Thread): """ thread for updating DNS entries of valid leases """ def __init__(self, dnsqueue): threading.Thread.__init__(self, name="DNSQuery") self.setDaemon(1) self.dnsqueue=dnsqueue def run(self): # wait for new queries in queue until the end of the world while True: hostname, a, action = self.dnsqueue.get() # colonify address for DNS address = ColonifyIP6(a.ADDRESS) try: # update AAAA record, delete old entry first update = dns.update.Update(a.DNS_ZONE, keyring=Keyring) update.delete(hostname, "AAAA") # if DNS should be updated do it - not the case if IP is released if action == "update": update.add(hostname, a.DNS_TTL, "AAAA", address) dns.query.tcp(update, cfg.DNS_UPDATE_NAMESERVER) # the reverse record will be first checked if it points # to the current hostname, if not, it will be deleted first update_rev = dns.update.Update(a.DNS_REV_ZONE, keyring=Keyring) try: answer = Resolver.query(dns.reversename.from_address(address), "PTR") for rdata in answer: hostname_ns = str(rdata).split(".")[0] # if ip address is related to another host delete this one if hostname_ns != hostname: update_rev.delete(dns.reversename.from_address(address), "PTR", hostname_ns + "." + a.DNS_ZONE + ".") except dns.resolver.NXDOMAIN: pass # if DNS should be updated do it - not the case if IP is released if action == "update": update_rev.add(dns.reversename.from_address(address), a.DNS_TTL, "PTR", hostname + "." + a.DNS_ZONE + ".") elif action == "release": update_rev.delete(dns.reversename.from_address(address), "PTR") dns.query.tcp(update_rev, cfg.DNS_UPDATE_NAMESERVER) except Exception,err: import traceback traceback.print_exc(file=sys.stdout) log.error("DNSUPDATE: " + str(err)) class TidyUpThread(threading.Thread): """ clean leases and transactions if obsolete """ def __init__(self): threading.Thread.__init__(self, name="TidyUp") self.setDaemon(1) def run(self): try: # counter for database cleaning interval dbcount = 0 #get and delete invalid leases while True: # transaction data can be deleted after transaction is finished now = datetime.datetime.now() timedelta = datetime.timedelta(seconds=cfg.CLEANING_INTERVAL*3) for t in Transactions.copy().keys(): try: if now > Transactions[t].Timestamp + timedelta: Transactions.pop(Transactions[t].ID) except Exception, err: log.error("TidyUp: TransactionID %s has already been deleted" % (str(err))) import traceback traceback.print_exc(file=sys.stdout) # if disconnected try reconnect if not volatilestore.connected: volatilestore.DBConnect() else: # cleaning database once per minute should be enough if dbcount > 60/cfg.CLEANING_INTERVAL: # remove leases which might not be recycled like random addresses for example volatilestore.remove_leases(category="random", timestamp=datetime.datetime.now()) # set leases free whose valid lifetime is over volatilestore.release_free_leases(datetime.datetime.now()) # unlock advertised leases remaining volatilestore.unlock_unused_advertised_leases() dbcount = 0 dbcount += 1 # clean collected MAC addresses after 30 seconds if cfg.CACHE_MAC_LLIP == False: for record in CollectedMACs.values(): if record.timestamp + 30 < time.time(): if cfg.LOG_MAC_LLIP == True: log.info("Deleted MAC %s for LinkLocalIP %s" % (record.mac, ColonifyIP6(record.llip))) CollectedMACs.pop(record.llip) time.sleep(cfg.CLEANING_INTERVAL) except: import traceback traceback.print_exc(file=sys.stdout) class Client(object): """ client object, generated from configuration database or on the fly """ def __init__(self): # Addresses, depending on class or fixed addresses self.Addresses = list() # DUID self.DUID = "" # current link-local IP self.LLIP = "" # Hostname self.Hostname = "" # Class/role of client self.Class = "" # MAC self.MAC = "" # timestamp of last update self.LastUpdate = "" def _getOptionsString(self): """ all attributes in a string for logging """ optionsstring = "" # put own attributes into a string options = self.__dict__.keys() options.sort() for o in options: # ignore some attributes if not o in ["OptionsRaw", "Client", "Timestamp", "DUIDLLAddress", "IAT1", "IAT2", "IP6_old", "LLIP_old"] and not str(self.__dict__[o]) == "": if not o == "Addresses": option = o + ": " + str(self.__dict__[o]) optionsstring = optionsstring + " | " + option else: option = "Addresses:" for a in self.__dict__[o]: option += " " + ColonifyIP6(a.ADDRESS) optionsstring = optionsstring + " | " + option return optionsstring.encode("ascii") class Transaction(object): """ all data of one transaction, to be collected in Transactions """ def __init__(self, transaction_id, client_llip, interface, message_type, options): # Transaction ID self.ID = transaction_id # Link Local IP of client self.ClientLLIP = client_llip # Interface the request came in self.Interface = interface # MAC address self.MAC = None # last message for following the protocol self.LastMessageReceivedType = message_type # dictionary for options self.OptionsRaw = options # default dummy OptionsRequest self.OptionsRequest = list() # timestamp to manage/clean transactions self.Timestamp = datetime.datetime.now() # dummy hostname self.FQDN = "" self.Hostname = "" # DNS Options for option 39 self.DNS_N = 0 self.DNS_O = 0 self.DNS_S = 0 # dummy IAID self.IAID = "00000000" # dummy IAT1 self.IAT1 = cfg.PREFERRED_LIFETIME # dummy IAT2 self.IAT2 = cfg.VALID_LIFETIME # Addresses given by client, for example for RENEW or RELEASE requests self.Addresses = list() # might be used against clients that are running wild # initial 1 as being increased after handling self.Counter = 1 # temporary storage for client configuration from DB config # - only used if config comes from DB self.ClientConfigDB = None # client config from config store self.Client = None # Vendor Class Option self.VendorClassEN = None self.VendorClassData = "" # Rapid Commit flag self.RapidCommit = False # answer type - take from class definition, one of "normal", "noaddress"y or "none" # defaults to "address" as this is the main purpose of dhcpy6d self.Answer = "normal" # DUID of client # 1 Client Identifier Option if options.has_key(1): duid_client = options[1] duid_type = int(options[1][0:4], 16) duid_hardware_type = int(options[1][4:8], 16) # dummy LL address duid_link_layer_address = "00:00:00:00:00:00" # DUID-LLT if duid_type == 1: duid_time = int(options[1][8:16], 16) # temp link layer address = lla lla = options[1][16:] duid_link_layer_address = ":".join((lla[0:2], lla[2:4], lla[4:6], lla[6:8], lla[8:10], lla[10:12])) # DUID-EN elif duid_type == 2: # nothing to to with enterprise DUID at the moment pass # DUID-LL elif duid_type == 3: duid_time = int(options[1][8:16], 16) # temp link layer address = lla lla = options[1][8:] duid_link_layer_address = ":".join((lla[0:2], lla[2:4], lla[4:6], lla[6:8], lla[8:10], lla[10:12])) # whatever for... it is even forbidden to use the DUIDLLAddress.... self.DUID = duid_client self.DUIDType = duid_type self.DUIDLLAddress = duid_link_layer_address # Identity Association for Non-temporary Addresses # 3 Identity Association for Non-temporary Address Option if options.has_key(3): for payload in options[3]: ia_id = payload[0:8] ia_t1 = int(payload[8:16], 16) ia_t2 = int(payload[16:24], 16) self.IAID = ia_id self.IAT1 = ia_t1 self.IAT2 = ia_t2 # addresses given by client if any for a in range(len(payload[32:])/44): address = payload[32:][(a*56):(a*56)+32] # in case an address is asked for twice by one host ignore the twin if not address in self.Addresses: self.Addresses.append(address) # Options Requested # 6 Option Request Option if options.has_key(6): options_request = list() opts = options[6][:] while len(opts) > 0: options_request.append(int(opts[0:4], 16)) opts = opts[4:] self.OptionsRequest = options_request # 14 Rapid Commit flag if options.has_key(14): self.RapidCommit = True # 16 Vendor Class Option if options.has_key(16): self.VendorClassEN = int(options[16][0:8], 16) self.VendorClassData = binascii.unhexlify(options[16][12:]) # FQDN # 39 FQDN Option if options.has_key(39): bits = ("%4s" % (str(bin(int(options[39][1:2]))).strip("0b"))).replace(" ", "0") self.DNS_N = int(bits[1]) self.DNS_O = int(bits[2]) self.DNS_S = int(bits[3]) name = ConvertBinary2DNS(options[39][2:]) # only hostname needed self.FQDN = name.lower() self.Hostname = name.split(".")[0].lower() def _getOptionsString(self): """ get all options in one string for debugging """ optionsstring = "" # put own attributes into a string options = self.__dict__.keys() options.sort() for o in options: # ignore some attributes if not o in ["OptionsRaw", "Client", "Timestamp", "DUIDLLAddress", "IAT1", "IAT2", "ClientConfigDB"] and \ not self.__dict__[o] in [None, False, "", []]: if o == "Addresses": option = "Addresses:" for a in self.__dict__[o]: option += " " + ColonifyIP6(a) optionsstring = optionsstring + " | " + option elif o == "ClientLLIP": option = "ClientLLIP: " + ColonifyIP6(self.__dict__["ClientLLIP"]) optionsstring = optionsstring + " | " + option else: option = o + ": " + str(self.__dict__[o]) optionsstring = optionsstring + " | " + option return optionsstring.encode("ascii") class UDPMulticastIPv6(SocketServer.UnixDatagramServer): """ modify server_bind to work with multicast add DHCPv6 multicast group ff02::1:2 """ def server_bind(self): """ multicast & python: http://code.activestate.com/recipes/442490/ """ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # multicast parameters # hop is one because it is all about the same subnet self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0) self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1) # looks like there is no other way to find interfaces than via libc for i in cfg.INTERFACE: IF_NAME[i] = LIBC.if_nametoindex(i) IF_NUMBER[IF_NAME[i]] = i if_number = struct.pack("I", LIBC.if_nametoindex(i)) mgroup = socket.inet_pton(socket.AF_INET6, cfg.MCAST) + if_number # join multicast group self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mgroup) # bind socket to server address self.socket.bind(self.server_address) # some more requests? self.request_queue_size = 100 class Handler(SocketServer.DatagramRequestHandler): """ manage all incoming datagrams """ def handle(self): """ request handling happens here """ # empty dummy response self.response = "" try: # convert raw message into ascii-bytes bytes = binascii.b2a_hex(self.request[0]) # clean client IP address - might come in short notation, which # should be extended # if sent from non-LLIP there is no interface - the request should be # repeated using multicast - Code 5 of StatusCode message type 13 try: client_llip, interface = self.client_address[0].split("%") client_llip = DecompressIP6(client_llip) except: # interface is set to "" and later evaluated to trigger a statuscode message client_llip, interface = self.client_address[0], "" client_llip = DecompressIP6(client_llip) # bad or too short message is thrown away if not len(bytes) > 8: pass else: message_type = int(bytes[0:2], 16) transaction_id = bytes[2:8] bytes_options = bytes[8:] options = {} while len(bytes_options) > 0: # option type and length are 2 bytes each option = int(bytes_options[0:4], 16) length = int(bytes_options[4:8], 16) # *2 because 2 bytes make 1 char value = bytes_options[8:8 + length*2] # Microsoft behaves a little bit different than the other # clients - in RENEW and REBIND request multiple addresses of an # IAID are not requested all in one option type 3 but # come in several options of type 3 what leads to some confusion if option != 3: options[option] = value else: if options.has_key(3): options[3].append(value) else: options[3] = list() options[3].append(value) # cut off bytes worked on bytes_options = bytes_options[8 + length*2:] # only valid messages will be processed if message_type in MESSAGE_TYPES: # 2. create Transaction object if not yet done if not Transactions.has_key(transaction_id): Transactions[transaction_id] = Transaction(transaction_id, client_llip, interface, message_type, options) # add client MAC address to transaction object if CollectedMACs.has_key(Transactions[transaction_id].ClientLLIP): Transactions[transaction_id].MAC = CollectedMACs[Transactions[transaction_id].ClientLLIP].mac else: Transactions[transaction_id].Timestamp = datetime.datetime.now() Transactions[transaction_id].LastMessageReceivedType = message_type # log incoming messages log.info("%s | TransactionID: %s%s" % (MESSAGE_TYPES[message_type], transaction_id, Transactions[transaction_id]._getOptionsString())) # 3. answer requests # check if client sent a valid DUID (alphanumeric) if Transactions[transaction_id].DUID.isalnum(): # if request was not addressed to multicast do nothing but logging if Transactions[transaction_id].Interface == "": log.info("TransactionID: %s | %s" % (transaction_id, "Multicast necessary but message came from %s" % (ColonifyIP6(Transactions[transaction_id].ClientLLIP)))) # reset transaction counter Transactions[transaction_id].Counter = 0 else: # client will get answer if its LLIP & MAC is known if not Transactions[transaction_id].ClientLLIP in CollectedMACs: # complete MAC collection - will make most sence on Linux and its native neighborcache access CollectMACs() # when still no trace of the client in neighbor cache then send silly signal back if not Transactions[transaction_id].ClientLLIP in CollectedMACs: # if not known send status code option failure to get # LLIP/MAC mapping from neighbor cache # status code "Success" sounds silly but works best self.build_response(7, transaction_id, [13], status=0) # complete MAC collection CollectMACs() # try to add client MAC address to transaction object try: Transactions[transaction_id].MAC = CollectedMACs[Transactions[transaction_id].ClientLLIP].mac except: # MAC not yet found :-( if cfg.LOG_MAC_LLIP == True: log.info("TransactionID: %s | %s" % (transaction_id, "MAC address for LinkLocalIP %s unknown" % (ColonifyIP6(Transactions[transaction_id].ClientLLIP)))) # if finally there is some info about the client try to answer the request if Transactions[transaction_id].ClientLLIP in CollectedMACs: if not Transactions[transaction_id].MAC: Transactions[transaction_id].MAC = CollectedMACs[Transactions[transaction_id].ClientLLIP].mac # ADVERTISE # if last request was a SOLICIT send an ADVERTISE (type 2) back if Transactions[transaction_id].LastMessageReceivedType == 1 \ and Transactions[transaction_id].RapidCommit == False: # preference option (7) is for free self.build_response(2, transaction_id, [3] + [7] + Transactions[transaction_id].OptionsRequest) # store leases for addresses and lock advertised address volatilestore.store_lease(transaction_id) # REQUEST # if last request was a REQUEST (type 3) send a REPLY (type 7) back elif Transactions[transaction_id].LastMessageReceivedType == 3 or \ (Transactions[transaction_id].LastMessageReceivedType == 1 and \ Transactions[transaction_id].RapidCommit == True): # preference option (7) is for free # if RapidCommit was set give it back if not Transactions[transaction_id].RapidCommit: self.build_response(7, transaction_id, [3] + [7] + Transactions[transaction_id].OptionsRequest) else: self.build_response(7, transaction_id, [3] + [7] + [14] + Transactions[transaction_id].OptionsRequest) # store leases for addresses volatilestore.store_lease(transaction_id) if cfg.DNS_UPDATE: DNSUpdate(transaction_id) # CONFIRM # if last request was a CONFIRM (4) send a REPLY (type 7) back # Due to problems with different clients they will get a not-available-reply # but the next ADVERTISE will offer them the last known and still active # lease. This makes sense in case of fixed MAC-based, addresses, ranges and # ID-based addresses, Random addresses will be recalculated elif Transactions[transaction_id].LastMessageReceivedType == 4: # the RFC 3315 is a little bit confusing regarding CONFIRM # messages so it won't hurt to simply let the client # solicit addresses again via answering "NoBinding" self.build_response(7, transaction_id, [13], status=3) # RENEW # if last request was a RENEW (type 5) send a REPLY (type 7) back elif Transactions[transaction_id].LastMessageReceivedType == 5: self.build_response(7, transaction_id, [3] + [7] + Transactions[transaction_id].OptionsRequest) # store leases for addresses volatilestore.store_lease(transaction_id) if cfg.DNS_UPDATE: DNSUpdate(transaction_id) # REBIND # if last request was a REBIND (type 6) send a REPLY (type 7) back elif Transactions[transaction_id].LastMessageReceivedType == 6: self.build_response(7, transaction_id, [3] + [7] + Transactions[transaction_id].OptionsRequest) # store leases for addresses volatilestore.store_lease(transaction_id) # RELEASE # if last request was a RELEASE (type 8) send a REPLY (type 7) back elif Transactions[transaction_id].LastMessageReceivedType == 8: if cfg.DNS_UPDATE: # build client to be able to delete it from DNS if Transactions[transaction_id].Client == None: Transactions[transaction_id].Client = BuildClient(transaction_id) for a in Transactions[transaction_id].Addresses: DNSDelete(transaction_id, address=a, action="release") for a in Transactions[transaction_id].Addresses: # free lease volatilestore.release_lease(a) # send status code option (type 13) with success (type 0) self.build_response(7, transaction_id, [13], status=0) # DECLINE # if last request was a DECLINE (type 9) send a REPLY (type 7) back elif Transactions[transaction_id].LastMessageReceivedType == 9: # maybe has to be refined - now only a status code "NoBinding" is answered self.build_response(7, transaction_id, [13], status=3) # INFORMATION-REQUEST # if last request was an INFORMATION-REQUEST (type 11) send a REPLY (type 7) back elif Transactions[transaction_id].LastMessageReceivedType == 11: self.build_response(7, transaction_id, Transactions[transaction_id].OptionsRequest) # general error - statuscode 1 "Failure" else: # send Status Code Option (type 13) with status code "UnspecFail" self.build_response(7, transaction_id, [13], status=1) # count requests of transaction # if there will be too much something went wrong # may be evaluated to reset the whole transaction Transactions[transaction_id].Counter += 1 except Exception,err: import traceback traceback.print_exc(file=sys.stdout) log.error("handle(): " + str(err)) print "Caused by:", self.client_address[0] return None def build_response(self, response_type, transaction_id, options_request, status=0): """ creates answer and puts it into self.response arguments: response_type - mostly 2 or 7 transaction_id option_request status -mostly 0 (OK) response will be sent by self.finish() """ try: # Header # response type + transaction id response_ascii = "%02x" % (response_type) response_ascii += transaction_id # these options are always useful # Option 1 client identifier response_ascii += BuildOption(1, Transactions[transaction_id].DUID) # Option 2 server identifier response_ascii += BuildOption(2, cfg.SERVERDUID) # list of options in answer to be logged options_answer = [] # IA_NA non-temporary addresses # Option 3 + 5 Identity Association for Non-temporary Address if 3 in options_request: # check if MAC of LLIP is really known if Transactions[transaction_id].ClientLLIP in CollectedMACs: # collect client information if Transactions[transaction_id].Client == None: Transactions[transaction_id].Client = BuildClient(transaction_id) # check if only a short NoAddrAvail answer or none at all ist t be returned if not Transactions[transaction_id].Answer == "normal": if Transactions[transaction_id].Answer == "noaddress": # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) # clean client addresses which not be deployed anyway Transactions[transaction_id].Client.Addresses[:] = [] # options in answer to be logged options_answer.append(13) else: # clean response as there is nothing to respond in case of answer = none self.response = "" return None else: # if client could not be built because of database problems send # status message back if Transactions[transaction_id].Client: # embed option 5 into option 3 - several if necessary ia_addresses = "" try: for address in Transactions[transaction_id].Client.Addresses: if address.IA_TYPE == "na": ipv6_address = binascii.b2a_hex(socket.inet_pton(socket.AF_INET6, ColonifyIP6(address.ADDRESS))) # if a transaction consists of too many requests from client - # - might be caused by going wild Windows clients - # reset all addresses with lifetime 0 # lets start with maximal transaction count of 10 if Transactions[transaction_id].Counter < 10: preferred_lifetime = "%08x" % (int(address.PREFERRED_LIFETIME)) valid_lifetime = "%08x" % (int(address.VALID_LIFETIME)) else: preferred_lifetime = "%08x" % (0) valid_lifetime = "%08x" % (0) ia_address = BuildOption(5, ipv6_address + preferred_lifetime + valid_lifetime) ia_addresses += ia_address if not ia_addresses == "": # # todo: default clients sometimes seem to have class "" # if Transactions[transaction_id].Client.Class != "": t1 = "%08x" % (int(cfg.CLASSES[Transactions[transaction_id].Client.Class].T1)) t2 = "%08x" % (int(cfg.CLASSES[Transactions[transaction_id].Client.Class].T2)) else: t1 = "%08x" % (int(cfg.T1)) t2 = "%08x" % (int(cfg.T2)) ia_na = BuildOption(3, Transactions[transaction_id].IAID + t1 + t2 + ia_addresses) response_ascii += ia_na # options in answer to be logged options_answer.append(3) except: # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) # options in answer to be logged options_answer.append(3) else: # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) # options in answer to be logged options_answer.append(3) # IA_TA temporary addresses if 4 in options_request: # check if MAC of LLIP is really known if Transactions[transaction_id].ClientLLIP in CollectedMACs: # collect client information if Transactions[transaction_id].Client == None: Transactions[transaction_id].Client = BuildClient(transaction_id) # check if only a short NoAddrAvail answer or none at all ist t be returned if not Transactions[transaction_id].Answer == "normal": if Transactions[transaction_id].Answer == "noaddress": # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) # clean client addresses which not be deployed anyway Transactions[transaction_id].Client.Addresses[:] = [] # options in answer to be logged options_answer.append(13) else: # clean response as there is nothing to respond in case of answer = none self.response = "" return None else: # if client could not be built because of database problems send # status message back if Transactions[transaction_id].Client: # embed option 5 into option 4 - several if necessary ia_addresses = "" try: for address in Transactions[transaction_id].Client.Addresses: if address.IA_TYPE == "ta": ipv6_address = binascii.b2a_hex(socket.inet_pton(socket.AF_INET6, ColonifyIP6(address.ADDRESS))) # if a transaction consists of too many requests from client - # - might be caused by going wild Windows clients - # reset all addresses with lifetime 0 # lets start with maximal transaction count of 10 if Transactions[transaction_id].Counter < 10: preferred_lifetime = "%08x" % (int(address.PREFERRED_LIFETIME)) valid_lifetime = "%08x" % (int(address.VALID_LIFETIME)) else: preferred_lifetime = "%08x" % (0) valid_lifetime = "%08x" % (0) ia_address = BuildOption(5, ipv6_address + preferred_lifetime + valid_lifetime) ia_addresses += ia_address if not ia_addresses == "": ia_ta = BuildOption(4, Transactions[transaction_id].IAID + ia_addresses) response_ascii += ia_ta # options in answer to be logged options_answer.append(4) except: # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) # options in answer to be logged options_answer.append(13) else: # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) # options in answer to be logged options_answer.append(13) # Option 7 Server Preference if 7 in options_request: response_ascii += BuildOption(7, "%02x" % (int(cfg.SERVER_PREFERENCE))) # options in answer to be logged options_answer.append(7) # Option 11 Authentication Option # seems to be pretty unused at the moment - to be done if 11 in options_request: # "3" for Reconfigure Key Authentication Protocol protocol = "%02x" % (3) # "1" for algorithm algorithm = "%02x" % (1) # assuming "0" as valid Replay Detection method rdm = "%02x" % (0) # Replay Detection - current time for example replay_detection = "%016x" % (int(datetime.datetime.now().strftime("%s"))) # Authentication Information Type # first 1, later with HMAC-MD5 2 ai_type = "%02x" % (1) authentication_information = cfg.AUTHENTICATION_INFORMATION # stuffed together response_ascii += BuildOption(11, protocol + algorithm + rdm + replay_detection + ai_type + authentication_information) # options in answer to be logged options_answer.append(11) # Option 12 Server Unicast Option if 12 in options_request: response_ascii += BuildOption(12, binascii.b2a_hex(socket.inet_pton(socket.AF_INET6, cfg.ADDRESS))) # options in answer to be logged options_answer.append(12) # Option 13 Status Code Option - statuscode is taken from dictionary if 13 in options_request: response_ascii += BuildOption(13, "%04x" % (status)) # options in answer to be logged options_answer.append(13) # Option 14 Rapid Commit Option - necessary for REPLY to SOLICIT message with Rapid Commit if 14 in options_request: response_ascii += BuildOption(14, "") # options in answer to be logged options_answer.append(14) # Option 23 DNS recursive name server if 23 in options_request and Transactions[transaction_id].Client: if len(cfg.NAMESERVER) > 0 or cfg.CLASSES[Transactions[transaction_id].Client.Class].NAMESERVER: # in case several nameservers are given convert them all and add them nameserver = "" # if the class has its own nameserver use them, otherwise the general ones if cfg.CLASSES[Transactions[transaction_id].Client.Class].NAMESERVER: for ns in cfg.CLASSES[Transactions[transaction_id].Client.Class].NAMESERVER: nameserver += socket.inet_pton(socket.AF_INET6, ns) else: for ns in cfg.NAMESERVER: nameserver += socket.inet_pton(socket.AF_INET6, ns) response_ascii += BuildOption(23, binascii.b2a_hex(nameserver)) # options in answer to be logged options_answer.append(23) # Option 24 Domain Search List if 24 in options_request: converted_domain_search_list = "" for d in cfg.DOMAIN_SEARCH_LIST: converted_domain_search_list += ConvertDNS2Binary(d) response_ascii += BuildOption(24, converted_domain_search_list) # options in answer to be logged options_answer.append(24) # Option 31 OPTION_SNTP_SERVERS #if 31 in options_request and cfg.SNTP_SERVERS != "": # sntp_servers = "" # for s in cfg.SNTP_SERVERS: # sntp_server = binascii.b2a_hex(socket.inet_pton(socket.AF_INET6, s)) # sntp_servers += sntp_server # response_ascii += BuildOption(31, sntp_servers) # Option 32 Information Refresh Time if 32 in options_request: response_ascii += BuildOption(32, "%08x" % int(cfg.INFORMATION_REFRESH_TIME)) # options in answer to be logged options_answer.append(32) # Option 39 FQDN # http://tools.ietf.org/html/rfc4704#page-5 # regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: # - client wants to update DNS itself -> sends 0 0 0 # - client wants server to update DNS -> sends 0 0 1 # - client wants no server DNS update -> sends 1 0 0 if 39 in options_request and Transactions[transaction_id].Client: # flags for answer N, O, S = 0, 0, 0 # use hostname supplied by client if cfg.DNS_USE_CLIENT_HOSTNAME and not cfg.DNS_IGNORE_CLIENT: hostname = Transactions[transaction_id].Hostname # use hostname from config else: hostname = Transactions[transaction_id].Client.Hostname if not hostname == "": if cfg.DNS_UPDATE == 1: # DNS update done by server - don't care what client wants if cfg.DNS_IGNORE_CLIENT: S = 1 O = 1 else: # honor the client's request for the server to initiate DNS updates if Transactions[transaction_id].DNS_S == 1: S = 1 # honor the client's request for no server-initiated DNS update elif Transactions[transaction_id].DNS_N == 1: N = 1 else: # no DNS update at all, not for server and not for client if Transactions[transaction_id].DNS_N == 1 or\ Transactions[transaction_id].DNS_S == 1: O = 1 # sum of flags nos_flags = N*4 + O*2 + S*1 response_ascii += BuildOption(39, "%02x" % (nos_flags) + ConvertDNS2Binary(hostname+"."+cfg.DOMAIN)) else: # if no hostname given put something in and force client override response_ascii += BuildOption(39, "%02x" % (3) + ConvertDNS2Binary("invalid-hostname")) # options in answer to be logged options_answer.append(39) # if databases are not connected send error to client if not (configstore.connected == volatilestore.connected == True): # mark database errors - every database may add its error dberror = [] if not configstore.connected: dberror.append("config") configstore.DBConnect() if not volatilestore.connected: dberror.append("volatile") volatilestore.DBConnect() # create error response - headers have to be recreated because # problems may have arisen while processing and these information # is not valid anymore # response type + transaction id response_ascii = "%02x" % (7) response_ascii += transaction_id # always of interest # option 1 client identifier response_ascii += BuildOption(1, Transactions[transaction_id].DUID) # option 2 server identifier response_ascii += BuildOption(2, cfg.SERVERDUID) # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) log.error("%s| TransactionID: %s | DatabaseError: %s" % (MESSAGE_TYPES[response_type], transaction_id, " ".join(dberror))) else: # log response if not Transactions[transaction_id].Client is None: if len(Transactions[transaction_id].Client.Addresses) == 0 and\ Transactions[transaction_id].Answer == "normal" and\ Transactions[transaction_id].LastMessageReceivedType in [1, 3, 5, 6]: # create error response - headers have to be recreated because # problems may have arisen while processing and these information # is not valid anymore # response type + transaction id response_ascii = "%02x" % (7) response_ascii += transaction_id # always of interest # option 1 client identifier response_ascii += BuildOption(1, Transactions[transaction_id].DUID) # option 2 server identifier response_ascii += BuildOption(2, cfg.SERVERDUID) # Option 13 Status Code Option - statuscode is 2: "No Addresses available" response_ascii += BuildOption(13, "%04x" % (2)) # options in answer to be logged options_answer.append(3) # log warning message about unavailable addresses log.warning("REPLY | No addresses available | TransactionID: %s | ClientLLIP: %s" %\ (transaction_id, ColonifyIP6(Transactions[transaction_id].ClientLLIP))) elif 3 in options_request or 4 in options_request: options_answer.sort() log.info("%s | TransactionID: %s | Options: %s%s" % (MESSAGE_TYPES[response_type], transaction_id, options_answer, Transactions[transaction_id].Client._getOptionsString())) else: log.info("what else should I do?") else: options_answer.sort() log.info("%s | TransactionID: %s | Options: %s" % (MESSAGE_TYPES[response_type], transaction_id, options_answer)) # response self.response = binascii.a2b_hex(response_ascii) except Exception, err: import traceback traceback.print_exc(file=sys.stdout) log.error("Response(): " + str(err)) print transaction_id print Transactions[transaction_id].Client.__dict__ # clear any response self.response = "" return None def finish(self): """ send response from self.response """ # send only if there is anything to send if cfg.REALLY_DO_IT: if len(self.response) > 0: self.socket.sendto(self.response, self.client_address) else: log.error("Nothing sent - please set 'really_do_it = yes' in config file.") ### MAIN ### if __name__ == "__main__": log.info("Starting dhcpy6d daemon...") log.info("Server DUID: %s" % (cfg.SERVERDUID)) # configure SocketServer UDPMulticastIPv6.address_family = socket.AF_INET6 server = UDPMulticastIPv6(("", 547), Handler) # start query queue watcher configqueryqueuewatcher = QueryQueue(cfg, configstore, configqueryqueue, configanswerqueue) configqueryqueuewatcher.start() volatilequeryqueuewatcher = QueryQueue(cfg, volatilestore, volatilequeryqueue, volatileanswerqueue) volatilequeryqueuewatcher.start() # adjust old data to match newer versions of dhcpy6d LegacyAdjustments() # collect all known MAC addresses from database if cfg.CACHE_MAC_LLIP == True: volatilestore.CollectMACsFromDB() # start TidyUp thread for cleaning in background tidyup = TidyUpThread() tidyup.start() # start DNS query queue to care for DNS in background dnsquery = DNSQueryThread(dnsqueue) dnsquery.start() # set user and group log.info("Running as user %s (UID %s) and group %s (GID %s)" % (cfg.USER, pwd.getpwnam(cfg.USER).pw_uid, cfg.GROUP, grp.getgrnam(cfg.GROUP).gr_gid)) # first set group because otherwise the freshly unprivileged user could not modify its groups itself os.setgid(grp.getgrnam(cfg.GROUP).gr_gid) os.setuid(pwd.getpwnam(cfg.USER).pw_uid) # log interfaces log.info("Listening on interfaces: %s" % (" ".join(IF_NAME))) # serve forever try: server.serve_forever() except KeyboardInterrupt: sys.exit(0) dhcpy6d/doc/000077500000000000000000000000001242200350000131505ustar00rootroot00000000000000dhcpy6d/doc/LICENSE000066400000000000000000000431031242200350000141560ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. dhcpy6d/doc/clients-example.conf000066400000000000000000000011571242200350000171150ustar00rootroot00000000000000# These are some example clients. Every section is a client. # Every client has to have a hostname, a class and at least # one of mac or duid to be identified depending on class definition. # # The option attribute "id" can be used for address definitions of # category "id". [client1] hostname = client1 mac = 01:01:01:01:01:01 class = valid_client [client2] hostname = client2 mac = 02:02:02:02:02:02 class = invalid_client [client3] hostname = client3 mac= 03:03:03:03:03:03 duid = 000100011234567890abcdef1234 class = valid_client [client4] hostname = client4 mac = 04:04:04:04:04:04 id = 4 class = valid_client dhcpy6d/doc/config.sql000066400000000000000000000002331242200350000151340ustar00rootroot00000000000000CREATE TABLE "hosts" ("hostname" VARCHAR PRIMARY KEY NOT NULL UNIQUE , "mac" VARCHAR, "class" VARCHAR, "address" VARCHAR, "id" VARCHAR, "duid" VARCHAR); dhcpy6d/doc/dhcpy6d-example.conf000066400000000000000000000231101242200350000170060ustar00rootroot00000000000000# dhcpy6d example configuration # # The first section [dhcpy6d] contains general options. # All sections whose name starts with "address_" are address # definitions. These are used in the sections named something # like "class_". These contain definitions for classes of clients. # The membership of clients of a class is defined in the client # configuration from client config file or config database. # Addresses contain various properties best seen on examples # down below. Classes contain extra properties like nameservers # for clients and filters. # There is one predefined class: "default". If not set in a # [class_default] section all clients which have no configuration # or match no filter are automatically of this class. If # [class_default] is not set the address "default" is used which # also can be defined in an [address_default] section. [dhcpy6d] # GENERAL OPTIONS # Server interface - multiple interfaces have to be separated by spaces. interface = eth0 # Multicast address to listen at # sensible default for local subnet is ff02::1:2. #mcast = ff02::1:2 # Port to listen at - like multicast address also defined by RFC. #port = 547 # Not used yet. #address = ::1 # Server DUID - if not set there will be one generated every # time dhcpy6d starts. This might cause trouble for Windows # clients because they go crazy about the changed server DUID. # Please note that the commandline argument --duid overrides this # setting. This is the case in Debian /etc/init.d/dhcpy6d script # which uses the generated DUID value from /etc/default/dhcpy6d. #serverduid = 0001000100000000000000000000 # Server preference is 255 as default. #server_preference = 255 # non-privileged user/group user = dhcpy6d group = dhcpy6d # Nameserver for option 23 - there can be several specified # separated by spaces. nameserver = fd01:db8::53 # Domain to be used for option 39 - host FQDN domain = local # Domain search list for option 24 - domain search list. If omited the value # of option "domain" above is taken as default domain_search_list = foo.com bar.com # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = dhcpy6d.log # Log to syslog daemon log_syslog = no # Syslog facility log_syslog_facility = daemon # A remote server syslog socket or a local unix socket log_syslog_destination = remote-server:514 # Log discovered MAC/LLIP pairs of clients log_mac_llip = no # Configuration of clients can be stored in text file or in MySQL or # SQLite database. See delivered config.sql and volatile.sql for # database schemes. # Use for small environment could be to get config from text file # and store leases in SQLite database. Larger setups might have use # for config and volatile data in MySQL database. # Store config type is one of "file", "mysql", "sqlite" or "none". # if "none" no client configuration is used. store_config = file # Dito for store volatile data like leases and MAC-LLIPs-mapping - # one of "mysql" or "sqlite". store_volatile = mysql # Path to file used for configuration of clients. store_file_config = clients.conf # Data used for MySQL storage # host store_mysql_host = localhost # database store_mysql_db = dhcpy6d # user store_mysql_user = dhcpy6d # password store_mysql_password = dhcpy6d # Paths to SQLite database files. # config.sqlite and volatile.sqlite are included in source folder. store_sqlite_config = config.sqlite store_sqlite_volatile = volatile.sqlite # Authentication information needed for reconfigure requests does # not work so it can safely be ignored. # If it would work it had to be some 128 bit key. #authentication_information = 00000000000000000000000000000000 # Flag to let dhcpy6d really answer to client requests - # might be of use for debugging and testing. really_do_it = yes # Declare which attributes of a requesting client should be checked # to prove its identity. Default is "mac", but "duid" and "hostname" # are allowed too. It is even possible to mix them, separated by # spaces. #identification = mac duid hostname identification = mac # Declare if all checked attributes have to match or is it enough if # some do. Options are "match_all" and "match_some". The latter # might be interesting if there are some dualboot clients whose MAC # addresses match but their DUIDs don't. identification_mode = match_all # DYNAMIC DNS UPDATES # This works at the moment only for ISC Bind nameservers. # Do dynamic DNS updates. Default is "no". dns_update = no # RNDC key name for DNS Update. dns_rndc_key = rndc-key # RNDC secret - mostly some MD5-hash. Take it from # nameservers' /etc/rndc.key. dns_rndc_secret = 0000000000000000000 # Nameserver to talk to. dns_update_nameserver = ::1 # Regarding RFC 4704 5. there are 3 kinds of client behaviour # for N O S bits: # - client wants to update DNS itself -> sends 0 0 0 # - client wants server to update DNS -> sends 0 0 1 # - client wants no server DNS update -> sends 1 0 0 # Ignore client ideas about DNS (if at all, what name to use, # self-updating...) dns_ignore_client = yes # Use client supplied hostname - yes or no. It is no problem to # override client desires. dns_use_client_hostname = no # IA_NA/IA_TA OPTIONS # These lifetimes are also used as default for addresses which # have no extra defined lifetimes. # Lifetimes can be defined in address definitions. # RENEW (T1) and REBIND (T2) timers can be defined in # class definitions. # Default preferred lifetime in seconds preferred_lifetime = 43200 # default valid lifetime in seconds valid_lifetime = 64800 # T1 t1 = 21600 # T2 t2 = 32400 # information refresh time for option 32 information_refresh_time = 3600 # DEFINITION OF AVAILABLE ADDRESSES # Addresses are defined by patterns of static and variable parts. # # There are different categories: "random", "range", "id", "mac": # # $random64$ - calculate random 64 bit interface identifier address # part. maybe future a version will allow shorter random # $range$ - use range addresses - only in the last octet of address # $id$ - if configuration of clients contain some kind of ID # it can be used for one octet # $mac$ - puts MAC address into 3 octets - works only on local subnet # # Categories and variables used in pattern must match! # The two options every address definition must have are category # and pattern. # 1. Example: definition of a normal locally and globally connected # valid client # a globally unique address [address_global] # Prefix length can be up to 64. prefix_length = 64 # For privacy a global address might better be randomly created. category = random # This pattern results in an address like this: # 2001:0db8:0000:0000:d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # IA type is mostly non-temporary as default so it is not necessary # to declare here. ia_type = na # Lifetimes can be set in seconds for every defined address. preferred_lifetime = 32400 valid_lifetime = 43200 # A unique local address [address_local_valid] # Prefix length can be up to 64. prefix_length = 64 # For easier internal management put MAC address into address. category = mac # Given MAC 01:02:03:04:05:06 this pattern results in an address # like this: fd01:db8:0000:0000:babe:0102:0304:0506. pattern = fd01:db8::babe:$mac$ # Update these addresses in Bind DNS - defaults to "no" dns_update = yes # Zone to update. dns_zone = example.com # Reverse zone to update dns_rev_zone = 1.0.d.f.ip6.arpa # Define a class for normal valid clients. [class_valid_client] # These clients get 2 Addresses, one internal ULA and one global. # Different addresses should be separated by spaces. # Note that "address_" from address definition section is omitted # here! addresses = global local_valid # Some internal example nameserver. nameserver = fd01:db8::53 # 2. Example: definition of a class for invalid clients [address_local_invalid] # Prefix length can be up to 64. This is the default. prefix_length = 64 # Invalid clients will get addresses of a range. category = range # Definition of range. range = 1000-1fff # Local address for invalid clients will get another prefix # Resulting addresses look like # fd01:0db8:0bad:0000:0000:0000:0000:1000 pattern = fd01:db8:bad::$range$ # Lifetimes of address are shorter for faster reaction to status # changes. preferred_lifetime = 2700 valid_lifetime = 3600 # Class for invalid clients [class_invalid_client] addresses = local_invalid # Extra nameserver for invalid clients. nameserver = fd01:db8:bad::53 # Short interval of address refresh attempts. t1 = 600 t2 = 900 # 3. Example: definition of filtered clients [address_filtered] # Prefix length can be up to 64. prefix_length = 64 # Filtered clients will get addresses of a range. category = range # Definition of range. range = 1000-1fff # Local address for filtered clients will get another prefix # Resulting addresses look like # fd01:0db8:0000:0000:babe:0000:0000:1000 pattern = fd01:db8::babe:0:0:$range$ [class_filtered_clients] addresses = filtered # Filters are regular expessions. # See http://docs.python.org/howto/regex.html # There are three types of filters allowed: # filter_hostname # filter_mac # filter_duid # With this setting all clients which transmit a hostname starting # with "windows" will get an address of range # fd01:db8::beef:0:0:1000 to fd01:db8::beef:0:01fff filter_hostname = windows.* # 4. Example: default addresses for all unknown clients # It should be enough if address_default is defined, only if # unknown clients should get # extra nameservers etc. a class_default has to be set. [address_default] # Prefix length can be up to 64. prefix_length = 64 category = mac # Given MAC 01:02:03:04:05:06 this pattern results in an # address like this: fd01:db8:dead:0bad:beef:0102:0304:0506. pattern = fd01:db8:dead:bad:beef:$mac$ dhcpy6d/doc/dhcpy6d-minimal.conf000066400000000000000000000010771242200350000170110ustar00rootroot00000000000000# dhcpy6d minimal example configuration [dhcpy6d] # Interface to listen to multicast ff02::1:2. interface = eth1 # Do not identify and configure clients. store_config = none # SQLite DB for leases and LLIP-MAC-mapping. store_volatile = sqlite store_sqlite_volatile = volatile.sqlite # Not really necessary but might help for debugging. log = on log_console = on # Special address type which applies to all not specially # configured clients. [address_default] # Choosing MAC-based addresses. category = mac # ULA-type address pattern. pattern = fd01:db8:dead:bad:beef:$mac$dhcpy6d/doc/volatile.sql000066400000000000000000000012031242200350000155040ustar00rootroot00000000000000CREATE TABLE "leases" ("address" VARCHAR PRIMARY KEY NOT NULL, "active" INTEGER NOT NULL, "last_message" INTEGER NOT NULL, "preferred_lifetime" INTEGER NOT NULL, "valid_lifetime" INTEGER NOT NULL, "hostname" VARCHAR NOT NULL, "type" VARCHAR NOT NULL, "category" VARCHAR NOT NULL, "ia_type" VARCHAR NOT NULL, "class" VARCHAR NOT NULL, "mac" VARCHAR NOT NULL,"duid" VARCHAR NOT NULL, "iaid" VARCHAR NOT NULL, "last_update" DATETIME NOT NULL,"preferred_until" DATETIME NOT NULL, "valid_until" DATETIME NOT NULL); CREATE TABLE "macs_llips" ("mac" VARCHAR PRIMARY KEY NOT NULL, "link_local_ip" VARCHAR NOT NULL, "last_update" DATETIME NOT NULL); dhcpy6d/etc/000077500000000000000000000000001242200350000131565ustar00rootroot00000000000000dhcpy6d/etc/default/000077500000000000000000000000001242200350000146025ustar00rootroot00000000000000dhcpy6d/etc/default/dhcpy6d000077500000000000000000000000521242200350000160660ustar00rootroot00000000000000# dhcpy6d is disabled by default #RUN=yes dhcpy6d/etc/dhcpy6d.conf000066400000000000000000000014711242200350000153710ustar00rootroot00000000000000# dhcpy6d default configuration # # Please see the examples in /usr/share/doc/dhcpy6d and # http://dhcpy6d.ifw-dresden.de/documentation for more information. [dhcpy6d] # Interface to listen to multicast ff02::1:2. interface = eth0 # Do not identify and configure clients. store_config = none # SQLite DB for leases and LLIP-MAC-mapping. store_volatile = sqlite store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite log = on log_file = /var/log/dhcpy6d.log # set to yes to really answer to clients # not necessary in Debian where it comes from /etc/default/dhcpy6d and /etc/init.d/dhcpy6 #really_do_it = no # Special address type which applies to all not specially # configured clients. [address_default] # Choosing MAC-based addresses. category = mac # ULA-type address pattern. pattern = fd01:db8:dead:bad:beef:$mac$ dhcpy6d/etc/init.d/000077500000000000000000000000001242200350000143435ustar00rootroot00000000000000dhcpy6d/etc/init.d/dhcpy6d000077500000000000000000000044321242200350000156350ustar00rootroot00000000000000#!/bin/sh ### BEGIN INIT INFO # Provides: dhcpy6d # Required-Start: $syslog $network $remote_fs # Required-Stop: $syslog $remote_fs # Should-Start: $local_fs # Should-Stop: $local_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start/Stop dhcpy6d DHCPv6 server # Description: (empty) ### END INIT INFO set -e PATH=/sbin:/bin:/usr/sbin:/usr/bin DHCPY6DBIN=/usr/sbin/dhcpy6d DHCPY6DCONF=/etc/dhcpy6d.conf DHCPY6DPID=/var/run/dhcpy6d.pid NAME="dhcpy6d" DESC="dhcpy6d DHCPv6 server" USER=dhcpy6d GROUP=dhcpy6d RUN=no DEFAULTFILE=/etc/default/dhcpy6d if [ -f $DEFAULTFILE ]; then . $DEFAULTFILE fi . /lib/lsb/init-functions check_status() { if [ ! -r "$DHCPY6DPID" ]; then test "$1" != -v || echo "$NAME is not running." return 3 fi if read pid < "$DHCPY6DPID" && ps -p "$pid" > /dev/null 2>&1; then test "$1" != -v || echo "$NAME is running." return 0 else test "$1" != -v || echo "$NAME is not running but $DHCPY6DPID exists." return 1 fi } test -x $DHCPY6DBIN || exit 0 case "$1" in start) if [ "$RUN" = "no" ]; then echo "dhcpy6d is disabled in /etc/default/dhcpy6d. Set RUN=yes to get it running." exit 0 fi log_daemon_msg "Starting $DESC $NAME" if ! check_status; then start-stop-daemon --start --make-pidfile --pidfile ${DHCPY6DPID} \ --background --oknodo --no-close --exec $DHCPY6DBIN -- --config $DHCPY6DCONF \ --user $USER \ --group $GROUP \ --duid $DUID \ --really-do-it $RUN sleep 2 if check_status -q; then log_end_msg 0 else log_end_msg 1 exit 1 fi else log_end_msg 1 exit 1 fi ;; stop) log_daemon_msg "Stopping $DESC $NAME" start-stop-daemon --stop --quiet --pidfile ${DHCPY6DPID} --oknodo log_end_msg $? rm -f $DHCPY6DPID ;; restart|force-reload) $0 stop sleep 2 $0 start if [ "$?" != "0" ]; then exit 1 fi ;; status) echo "Status of $NAME: " check_status -v exit "$?" ;; *) echo "Usage: $0 (start|stop|restart|force-reload|status)" exit 1 esac exit 0 dhcpy6d/etc/logrotate.d/000077500000000000000000000000001242200350000154005ustar00rootroot00000000000000dhcpy6d/etc/logrotate.d/dhcpy6d000066400000000000000000000001511242200350000166610ustar00rootroot00000000000000/var/log/dhcpy6d.log { weekly missingok rotate 4 compress notifempty create 770 dhcpy6d dhcpy6d } dhcpy6d/man/000077500000000000000000000000001242200350000131565ustar00rootroot00000000000000dhcpy6d/man/man5/000077500000000000000000000000001242200350000140165ustar00rootroot00000000000000dhcpy6d/man/man5/dhcpy6d-clients.conf.5000066400000000000000000000076031242200350000200360ustar00rootroot00000000000000.TH dhcpy6d-clients.conf 5 "Jul 17, 2014" "Henri Wahl" "dhcpy6d-clients.conf" .SH "NAME" dhcpy6d-clients.conf \- clients configuration file for DHCPv6 server dhcpy6d .SH "DESCRIPTION" This file contains all client configuration data if these options are set in .IR dhcpy6d.conf ": .B store_config = file and .B store_file_config = /path/to/dhcpy6d-clients.conf An alternative method to store client configuration is using database storage with SQLite or MySQL databases. Further details are available at .IR https://dhcpy6d.ifw\-dresden.de/documentation/config/. This file follows RFC 822 style parsed by Python ConfigParser module. Some options allow multiple values. These have to be separated by spaces. .SH CLIENT SECTIONS .TP .BR [host_name] Every client is configured in one section. It might have multiple attributes which are necessary depending on the configured .BR identification and general address settings from .IR dhcpy6d.conf ". .SH CLIENT ATTRIBUTES Every client section contains several attributes. .BR hostname " and " class are mandatory. A third one should match at least one of the identification attributes configured in .IR dhcpy6d.conf ". .SS MANDATORY CLIENT ATTRIBUTE class Both of the following 2 attributes are necessary. .TP .BR class\ =\ Every client needs a class. If a client is identified, it depends from its class, which addresses it will get. This relation is configured in .IR dhcpy6d.conf ". .SS SEMI-MANDATORY CLIENT ATTRIBUTES Depending on .B identification in .I dhcpy6d.conf clients need to have the corresponding attributes. At least one of them is needed. .TP .B mac = The MAC address of the Link Local Address of the client DHCPv6 request. .TP .B duid = The DUID of the client which comes with the DHCPv6 request message. No hex and \\ needed, just like for example .BR 000100011234567890abcdef1234 . .TP .B hostname = The client hostname. It will be used for dynamic DNS update. .SS EXTRA ATTRIBUTES These attributes do not serve for identification of a client but for appropriate address generation. .TP .B id = .B id has to be a hex number in the range 0-FFFF. The client ID from this directive will be inserted in the address pattern of category .B id instead of the .B $id$ placeholder. .TP .B address =
[
...] Addresses configured here will be sent to a client in addition to the ones it gets due to its class. Might be useful for some extra static address definitions. .SH EXAMPLES The next lines contain some example client definitions: .nf [client1] hostname = client1 mac = 01:01:01:01:01:01 class = valid_client [client2] hostname = client2 mac = 02:02:02:02:02:02 class = invalid_client [client3] hostname = client3 duid = 000100011234567890abcdef1234 class = valid_client address = 2001:cb8::babe:1 [client4] hostname = client4 mac = 04:04:04:04:04:04 id = 1234 class = valid_client .fi .SH AUTHOR Copyright (C) 2012-2014 Henri Wahl <\fBh.wahl@ifw-dresden.de\fP> .SH LICENSE This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. .SH SEE ALSO .nf .BR dhcpy6d (8) .BR dhcpy6d.conf (5) https://dhcpy6d.ifw-dresden.de https://github.com/HenriWahl/dhcpy6d dhcpy6d/man/man5/dhcpy6d.conf.5000066400000000000000000000645231242200350000164030ustar00rootroot00000000000000.TH "dhcpy6d.conf" "5" "Jul 17, 2014" "Henri Wahl" "dhcpy6d.conf" .SH "NAME" dhcpy6d.conf \- configuration file for DHCPv6 server dhcpy6d .SH "DESCRIPTION" This file contains the general settings for DHCPv6 server daemon dhcpy6d. It follows RFC 822 style parsed by Python ConfigParser module. It contains several sections which will be discussed in detail here. An online documentation is also available at .I https://dhcpy6d.ifw\-dresden.de/documentation/config/. Boolean settings can be done with 1/0, on/off or yes/no values. Some options allow multiple values. These have to be separated by spaces. There are 3 types of sections: .TP .B [dhcpy6d] This section contains general options like interfaces, storage and logging. Only one [dhcpy6d] section is allowed. .TP .B [address_] There can be various [address_] sections. In address sections severall address ranges and types can be defind according to you needs. Addresses are organized in classes. For details read further down. .TP .B [class_] Class definitions allow to apply different addresses, time limits et al. to different types of clients. .SH "GENERAL CONFIGURATION IN SECTION [dhcpy6d]" This section contains important general options. Values are sometimes examples and not meant to be used in production environments. .TP .B really_do_it = yes | no Let dhcpy6d really_do_it and respond to client requests \- might be of use for debugging and testing. .TP .B interface = [ ...] The interfaces the server listens on is defined with keyword interface. Multiple interfaces have to be separated by spaces. .TP .B mcast = The multicast address to listen at is ff02::1:2. Due to the facts that dhcpy6d at the moment works in local network segments only and to the restriction of MAC addresses only being usable there it will always have this value. .TP .B port = Exactly the same applies to the port dhcpy6d listens on. Default is 547. Probably senseless to change it but who knows. .TP .B serverduid = The server DUID should be configured with serverduid. If there is none dhcpy6d creates a new one at every startup. Windows clients might run a little bit wild when server DUID changed. You are free to compose your own as long as it follows RFC 3315. Please note that it has to be in hexadecimal format \- no octals, no "\-", just like in the example below. The example here is a DUID\-LLT (Link\-layer Address Plus Time) even if it should be a DUID\-TLL as timestamp comes first. It is composed of DUID\-type(LLT=1) + Hardware\-type(Ethernet=1) + Unixtime\-in\-hex + MAC\-address what makes a 0001 + 0001 + 11fb5dc9 + 01023472a6c5 = 0001000111fb5dc901023472a6c5. .TP .B server_preference = <0-255> The server_preference determines the priority of the server. The maximum value is 255 which means highest priority. .TP .B user = For security reasons dhcpy6d can be run as non\-root user. .TP .B group = For security reasons dhcpy6d can be run as non\-root group. .TP .B nameserver = [ ...] Nameservers to be replied to request option 23 are defined with nameserver. If more than one is needed they have to be separated by spaces. .TP .B domain = The domain to be used with FQDN hostnames for option 39. .TP .B domain_search_list = [ ...] Domain search lists to be used with option 24t. If none is given the value of domain above is used. Multiple domains have to be separated by space or comma. .TP .B log = yes | no Enable logging. .TP .B log_console = yes | no Log to the console where .B dhcpy6d has been started. .TP .B log_file = Defines the file used for logging. Will be created if it does not yet exist. .TP .B log_syslog = yes | no If logs should go to syslog it is set here. .TP .B log_syslog_destination = syslogserver An UDP syslog server may be used if log_syslog_destination points to it. Optionally a port other than default 514 can be set when adding ":port" to the destination. .TP .B log_syslog_facility = The default syslog facility is "daemon" but can be changed here. .TP .B log_mac_llip = yes | no Log discovered MAC/LLIP pairs of clients. Might be pretty verbose in larger setups and with disabled MAC/LLIP pair caching. .TP .B store_config = file | sqlite | mysql | none Configuration of clients can be stored in a file or in a database. Databases MySQL and SQLite are supported at the moment, thus possible values are "file", "sqlite" or "mysql". To disable any client configuration source it has to be "none". .TP .B store_file_config = File which contains the clients configuration. Default is .B /etc/dhcpy6d\-clients.conf. For details see .IR dhcpy6d\-clients.conf (5) ". .TP .B store_sqlite_config = /path/to/sqlite/config/file SQLite database file which contains the clients configuration. .TP .B store_volatile = sqlite | mysql Volatile data like leases and the mapping between Link Local Addresses and MAC addresses can be stored in MySQL or SQLite database, so the possible values for store_volatile are "mysql" and "sqlite". Default is /var/lib/dhcpy6d/volatile.sqlite. .TP .B store_sqlite_volatile = /path/to/sqlite/volatile/file If set store_volatile is set to "sqlite" a SQLite database file must be defined. .TP .B store_mysql_host = .TP .B store_mysql_db = .TP .B store_mysql_user = .TP .B store_mysql_password = If .B store_config and/or .B store_volatile use a MySQL database to store information it has to be set with these self\-explanatory options. The same database is used for config and volatile data. .TP .B cache_mac_llip = yes | no Cache discovered MAC/LLIP pairs in database. If enabled reduces response and open dhcpy6d to MAC/LLIP poisoning. If disabled might increase system load. .TP .B identification = Clients can be set to be identified by several attributes \- MAC address, DUID or hostname. At least one of mac, duid or hostname is necessary. Hostname is the one sent in client request with DHCPv6 option 39. Identification is used to get the correct settings for the client from config file or database. Same MAC and different DUIDs might be interesting for clients with multiple OS. .TP .B identification_mode = match_all | match_some If more than one identification attribute has been set, identification_mode can be one of "match_all" or "match_some". The first means that all attributes have to match to identify a client and the latter is more tolerant. .TP .B dns_update = yes | no Dynamically update DNS. This works at the moment only with Bind DNS, but might be extended to others, maybe via call of an external command. .TP .B dns_update_nameserver = [ ...] .TP .B dns_rndc_key = .TP .BR dns_rndc_secret\ =\ When connecting to a Bind DNS server its address and the necessary RNDC data must be set. .TP .BR dns_ignore_client\ =\ yes\ |\ no Clients may request that they update the DNS record theirself. If their wishes shall be ignored this option has to be true. .TP .BR dns_use_client_hostname\ =\ yes\ |\ no The client hostname either comes from configuration of dhcpy6d or in the client request. .TP .B preferred_lifetime = .TP .B valid_lifetime = .TP .B t1 = .TP .B t2 = Preferred lifetime, valid lifetime, T1 and T2 in seconds are configured with the corresponding options. .TP .B information_refresh_time = The lifetime of information given to clients as response to an information-request message. .SH ADDRESS DEFINITIONS IN MULTIPLE [address_] SECTIONS The .B part of an .B [address_] section is an arbitrarily chosen identifier like "clients" or "invalid_clients". There can be many address definitions which will be used by classes. Every address definition may include several properties: .TP .B category = mac | id | range | random Categories play an important role when defining patterns for addresses. An address belongs to a certain category: .BR mac " - uses MAC address from client request as part of address .BR id " - uses ID given to client in configuration file or database as one octet of address, should be in range 0-FFFF .BR range " - generate addresses of given ranges .BR random " - randomly created 64 bit values .TP .B pattern = 2001:db8::$mac$|$id$|$range$|$random$ Patterns allow to design the addresses according to their category. See examples section below to make it more clear. .B $mac$ - The MAC address from the DHCPv6 request's Link Local Address found in the neighbor cache will be inserted instead of the placeholder. It will be stretched over 3 octets like 00:11:22:33:44:55 becomes 0011:2233:4455. .B $id$ - If clients get an ID in client configuration file or in client configuration database this ID will fill one octet. Thus the ID has to be in the range of 0000-FFFF. .B $range$ - If address is of category range the range defined with extra keyword " range " will be used here in place of one octet. This is why the range can span from 0000-FFFF. Clients will get an address out of the given range. .B $random64$ - A 64 bit random address will be generated in place of this variable. Clients get a random address just like they would if privacy extensions were used. The random part will span over 4 octets. .TP .B prefix_length = <0-128> Default prefix length for addresses is 64 but it can be customized here. .TP .B ia_type = na | ta IA (Identity Association) types can be one of non-temporary address "na" or temporary address "ta". Default and probably most used is "na". .TP .B preferred_lifetime = .TP .B valid_lifetime = As default preferred and valid lifetime are set in general settings, but it is configurable individually for every address setting. .TP .B dns_update = yes | no .TP .B dns_zone = .TP .B dns_rev_zone = If these addresses should be synchronized with Bind DNS, these three settings have to be set accordingly. The nameserver for updates is set in general settings. .SS DEFAULT\ ADDRESS The address scheme used for the default class "class_default" is by default named "address_default". It should be enough if address_default is defined, only if unknown clients should get extra nameservers etc. a class_default has to be set. .TP .B [address_default] Address scheme used as default for clients which do not match any other class than "class_default". .SH CLASS DEFINITIONS IN MULTIPLE [class_] SECTIONS The .B part of an .B [class_] section is an arbitrarily chosen identifier like "clients" or "invalid_clients". Clients can be grouped in classes. Different classes can have different properties, different address sets and different numbers of addresses. Classes also might have different name servers, time intervals, filters and interfaces. A client gets the addresses, nameserver and T1/T2 values of the class which it is configured for in client configuration database or file. .TP .B addresses = [ ...] A class can contain as many addresses as needed. Their names have to be separated by spaces. .TP .B answer = normal | noaddress | none Normally a client will get an answer, but if for whatever reason is a need to give it an NoAddrAvail message back or completely ignore the client it can be set here. .TP .B nameserver = [ ...] Each class can have its own nameservers. If this option is used it replaces the nameservers from general settings. .TP .B t1 = .TP .B t2 = Each class can have its own .B t1 and .B t2 values. The ones from general settings will be overridden. Might be of use for some invalid-but-about-to-become-valid-somehow-soon class. .TP .B filter_hostname = .TP .B filter_mac = .TP .B filter_duid = Filters allow to apply a class to a client not by configuration but by a matching regular expression filter. Most useful might be the filtering by hostname, but maybe there is some use for DUID and MAC address based filtering too. The regular expressions are meant to by Python Regular Expressions. See .I https://docs.python.org/2/howto/regex.html and examples section below for details. .TP .B interface = [ ...] It is possible to let a class only apply on specific interfaces. These have to be separated by spaces. .SS DEFAULT\ CLASS At the moment every client which does not match any other class by client configuration or filter automatically matches the class "default". This class could get an address scheme too. It should be enough if address_default is defined, only if unknown clients should get extra nameservers etc. a class_default has to be set. .TP .B [class_default] Default class for all clients that do not match any other class. Like any other class it could contain all options that appyl to a class. .TP .B [class_default_] If dhcpy6d listens at multiple interfaces, one can define a default class for every interface. .SH "EXAMPLES" The following paragraphs contain some hopefully helpful examples. .SS 1. MINIMAL CONFIGURATION Here in this minimalistic example the server daemon listens on interface eth0. It does not use any client configuration source but answers requests with default addresses. These are made of the pattern fd01:db8:dead:bad:beef:$mac$ and result in addresses like fd01:db8:deaf:bad:beef:1020:3040:5060 if the MAC address of the requesting client was 10:20:30:40:50:60. .nf [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Some server DUID. serverduid = 0001000134824528134567366121 # Do not identify and configure clients. store_config = none # SQLite DB for leases and LLIP-MAC-mapping. store_volatile = sqlite store_sqlite_volatile = volatile.sqlite # Special address type which applies to all not specially. # configured clients. [address_default] # Choosing MAC-based addresses. category = mac # ULA-type address pattern. pattern = fd01:db8:dead:bad:beef:$mac$ .fi .SS 2. CONFIGURATION WITH VALID AND UNKNOWN CLIENTS This example shows some more complexity. Here only valid hosts will get a random global address from 2001:db8::/64. Unknown clients get a default ULA range address from fc00::/7. .nf [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Server DUID - if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Non-privileged user/group. user = dhcpy6d group = dhcpy6d # Nameservers for option 23 - there can be several specified separated by spaces. nameserver = fd00:db8::53 # Domain to be used for option 39 - host FQDN. domain = example.com # Domain search list for option 24 - domain search list. # If omited the value of option "domain" above is taken as default. domain_search_list = example.com # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = /var/log/dhcpy6d.log # Use SQLite for client configuration. store_config = sqlite # Use SQLite for volatile data. store_volatile = sqlite # Paths to SQLite database files. store_sqlite_config = config.sqlite store_sqlite_volatile = volatile.sqlite # Declare which attributes of a requesting client should be checked # to prove its identity. It is possible to mix them, separated by spaces. identification = mac # Declare if all checked attributes have to match or is it enough if # some do. Kind of senseless with just one attribute. identification_mode = match_all # These lifetimes are also used as default for addresses which # have no extra defined lifetimes. preferred_lifetime = 43200 valid_lifetime = 64800 t1 = 21600 t2 = 32400 # ADDRESS DEFINITION # Addresses for proper valid clients. [address_valid_clients] # Better privacy for global addresses with category random. category = random # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # Default addresses for unknown invalid clients. [address_default] # Unknown clients will get an internal ULA range-based address. category = range # The keyword "range" sets the range used in pattern. range = 1000-1FFF # This pattern results in addresses like fd00::1234. pattern = fd00::$range$ # CLASS DEFINITION # Class for proper valid client. [class_valid_clients] # At least one of the above address schemes has to be set. addresses = valid_clients # Valid clients get a different nameserver. nameserver = 2001:db8::53 # Default class for unknown hosts - only necessary here because of time interval settings. [class_default] addresses = default # Short interval of address refresh attempts so that a client's status # change will be reflected in IPv6 address soon. t1 = 600 t2 = 900 .fi .SS 3. CONFIGURATION WITH 2 NETWORK SEGMENTS, SERVERS, VALID AND UNKNOWN CLIENTS This example uses 2 network segments, one for servers and one for clients. Servers here only get local ULA addresses. Valid clients get 2 addresses, one local ULA and one global GUA address. This feature of DHCPv6 is at the moment only well supported by Windows clients. Unknown clients will get a local ULA address. Only valid clients and servers will get information about nameservers. .nf [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interfaces to listen to multicast ff02::1:2. # eth1 - client network # eth2 - server network interface = eth1 eth2 # Server DUID - if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Non-privileged user/group. user = dhcpy6d group = dhcpy6d # Domain to be used for option 39 - host FQDN. domain = example.com # Domain search list for option 24 - domain search list. # If omited the value of option "domain" above is taken as default. domain_search_list = example.com # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = /var/log/dhcpy6d.log # Use MySQL for client configuration. store_config = mysql # Use MySQL for volatile data. store_volatile = mysql # Data used for MySQL storage. store_mysql_host = localhost store_mysql_db = dhcpy6d store_mysql_user = dhcpy6d store_mysql_password = dhcpy6d # Declare which attributes of a requesting client should be checked # to prove its identity. It is possible to mix them, separated by spaces. identification = mac # Declare if all checked attributes have to match or is it enough if # some do. Kind of senseless with just one attribute. identification_mode = match_all # These lifetimes are also used as default for addresses which # have no extra defined lifetimes. preferred_lifetime = 43200 valid_lifetime = 64800 t1 = 21600 t2 = 32400 # ADDRESS DEFINITION # Global addresses for proper valid clients (GUA). [address_valid_clients_global] # Better privacy for global addresses with category random. category = random # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # Local addresses for proper valid clients (ULA). [address_valid_clients_local] # Local addresses need no privacy, so they will be based of range. category = range range = 2000-2FFF # Valid clients will get local ULA addresses from fd01::/64. pattern = fd01::$range$ # Servers in servers network will get local addresses based on IDs from client configuration. [address_servers] # IDs are set in client configuration database in range of 0-FFFF. category = id # Servers will get local ULA addresses from fd02::/64. pattern = fd02::$id$ # Default addresses for unknown invalid clients [address_default] # Unknown clients will get an internal ULA range-based address. category = range # The keyword "range" sets the range used in pattern. range = 1000-1FFF # This pattern results in addresses like fd00::1234. pattern = fd00::$range$ # CLASS DEFINITION # Class for proper valid client. [class_valid_clients] # Clients only exist in network linked with eth1. interface = eth1 # Valid clients get 2 addresses, one local ULA and one global GUA # (only works reliably with Windows clients). addresses = valid_clients_global valid_clients_local # Only valid clients get a nameserver from server network. nameserver = fd02::53 # Class for servers in network on eth2 [class_servers] # Servers only exist in network linked with eth2. interface = eth2 # Only local addresses for servers. addresses = servers # Nameserver from server network. nameserver = fd02::53 # Default class for unknown hosts - only necessary here because of time interval settings [class_default] addresses = default # Short interval of address refresh attempts so that a client's status # change will be reflected in IPv6 address soon. t1 = 600 t2 = 900 .fi .SS 4. CONFIGURATION WITH DYNAMIC DNS UPDATES In this example the hostnames of valid clients will be registered in the Bind DNS server. The zones to be updated are configured for every address definition. Here only the global GUA addresses for valid clients will be updated in DNS. The hostnames will be taken from client configuration data - the ones supplied by the clients are ignored. .nf [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Server DUID - if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Non-privileged user/group. user = dhcpy6d group = dhcpy6d # Nameservers for option 23 - there can be several specified separated by spaces. nameserver = fd00:db8::53 # Domain to be used for option 39 - host FQDN. domain = example.com # Domain search list for option 24 - domain search list. # If omited the value of option "domain" above is taken as default. domain_search_list = example.com # This works at the moment only for ISC Bind nameservers. dns_update = yes # RNDC key name for DNS Update. dns_rndc_key = rndc-key # RNDC secret - mostly some MD5-hash. Take it from # nameservers' /etc/rndc.key. dns_rndc_secret = 0123456789012345679 # Nameserver to talk to. dns_update_nameserver = ::1 # Regarding RFC 4704 5. there are 3 kinds of client behaviour # for N O S bits: # - client wants to update DNS itself -> sends 0 0 0 # - client wants server to update DNS -> sends 0 0 1 # - client wants no server DNS update -> sends 1 0 0 # Ignore client ideas about DNS (if at all, what name to use, self-updating...) # Here client hostname is taken from client configuration dns_ignore_client = yes # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = /var/log/dhcpy6d.log # Use SQLite for client configuration. store_config = sqlite # Use SQLite for volatile data. store_volatile = sqlite # Paths to SQLite database files. store_sqlite_config = config.sqlite store_sqlite_volatile = volatile.sqlite # Declare which attributes of a requesting client should be checked # to prove its identity. It is possible to mix them, separated by spaces. identification = mac # ADDRESS DEFINITION # Addresses for proper valid clients. [address_valid_clients] # Better privacy for global addresses with category random. category = random # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # Update these addresses in Bind DNS dns_update = yes # Zone to update. dns_zone = example.com # Reverse zone to update dns_rev_zone = 8.b.d.0.1.0.0.2.ip6.arpa # Default addresses for unknown invalid clients. [address_default] # Unknown clients will get an internal ULA range-based address. category = range # The keyword "range" sets the range used in pattern. range = 1000-1FFF # This pattern results in addresses like fd00::1234. pattern = fd00::$range$ # CLASS DEFINITION # Class for proper valid client. [class_valid_clients] # At least one of the above address schemes has to be set. addresses = valid_clients # Valid clients get a different nameserver. nameserver = 2001:db8::53 .fi .SS 5. CONFIGURATION WITH FILTER In this example the membership of a client to a class is defined by a filter for hostnames. All Windows machines have win*-names here and when requesting an address this hostname gets filtered. .nf [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Server DUID - if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Use no client configuration. store_config = none # Use SQLite for volatile data. store_volatile = sqlite # Paths to SQLite database file. store_sqlite_volatile = volatile.sqlite # ADDRESS DEFINITION [address_local] category = range range = 1000-1FFF pattern = fd00::$range$ [address_global] category = random pattern = 2001:638::$random64$ # CLASS DEFINITION [class_windows] addresses = local # Python regular expressions to be used here filter_hostname = win.* [class_default] addresses = global .fi .SH AUTHOR Copyright (C) 2012-2014 Henri Wahl <\fBh.wahl@ifw-dresden.de\fP> .SH LICENSE This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. .SH "SEE ALSO" .nf .BR dhcpy6d (8) .BR dhcpy6d\-clients.conf (5) https://dhcpy6d.ifw\-dresden.de https://github.com/HenriWahl/dhcpy6d dhcpy6d/man/man8/000077500000000000000000000000001242200350000140215ustar00rootroot00000000000000dhcpy6d/man/man8/dhcpy6d.8000066400000000000000000000064421242200350000154610ustar00rootroot00000000000000.TH dhcpy6d 8 "Jun 27, 2014" "" "dhcpy6d" .SH NAME dhcpy6d - MAC address aware DHCPv6 server .SH SYNOPSIS \fBdhcpy6d\fP [\fB\-c\fR \fIfile\fR] [\fB\-u\fR \fIuser\fR] [\fB\-g\fR \fIgroup\fR] [\fB\-r\fR \fIyes|no\fR] [\fB\-d\fR \fIduid\fR] [\fB\-G\fR] .SH DESCRIPTION .B dhcpy6d is an open source server for DHCPv6, the DHCP protocol for IPv6. .PP Its development is driven by the need to be able to use the existing IPv4 infrastructure in coexistence with IPv6. In a dualstack scenario, the existing DHCPv4 most probably uses MAC addresses of clients to identify them. This is not intended by RFC 3315 for DHCPv6, but also not forbidden. Dhcpy6d is able to do so in local network segments and therefore offers a pragmatical method for parallel use of DHCPv4 and DHCPv6, because existing client management solutions could be used further. .PP .B dhcpy6d comes with the following features: .br .I \fR * identifies clients by MAC address, DUID or hostname .br .I \fR * generates addresses randomly, by MAC address, by range or by given ID .br .I \fR * filters clients by MAC, DUID or hostname .br .I \fR * assigns multiple addresses per client .br .I \fR * allows one to organize clients in different classes .br .I \fR * stores leases in MySQL or SQLite database .br .I \fR * client information can be retrieved from database or textfile .br .I \fR * dynamically updates DNS (Bind) .br .I \fR * supports rapid commit .br .I \fR * listens on multiple interfaces .br .SH OPTIONS Most configuration is done via the configuration file. .TP .BR \-c, " \-\-config=\fIconfigfile\fR Set the configuration file to use. Default is /etc/dhcpy6d.conf. .TP .BR \-u, " \-\-user=\fIuser\fR Set the unprivileged user to be used. .TP .BR \-g, " \-\-group=\fIgroup\fR Set the unprivileged group to be used. .TP .BR \-r, " \-\-really\-do\-it=\fIyes|no\fR Really activate the DHCPv6 server. This is a precaution to prevent larger network trouble. .TP .BR \-d, " \-\-duid=\fIgroup\fR Set the DUID for the server. This argument is used by /etc/init.d/dhcpy6d. .TP .BR \-G, " \-\-generate-duid Generate DUID to be used in config file. This argument is used to generate a DUID for /etc/default/dhcpy6d. After generation dhcpy6d exits. .SH FILES .nf /etc/dhcpy6d.conf /etc/dhcpy6d-clients.conf /var/lib/dhcpy6d/ /var/log/dhcpy6d.log .SH AUTHOR Copyright (C) 2012-2014 Henri Wahl <\fBh.wahl@ifw-dresden.de\fP> .SH LICENSE This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. .SH SEE ALSO .nf .BR dhcpy6d.conf (5) .BR dhcpy6d-clients.conf (5) https://dhcpy6d.ifw-dresden.de https://github.com/HenriWahl/dhcpy6d dhcpy6d/redhat/000077500000000000000000000000001242200350000136525ustar00rootroot00000000000000dhcpy6d/redhat/dhcpy6d.spec000066400000000000000000000110401242200350000160630ustar00rootroot00000000000000%{?!dhcpy6d_uid: %define dhcpy6d_uid dhcpy6d} %{?!dhcpy6d_gid: %define dhcpy6d_gid %dhcpy6d_uid} %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} Name: dhcpy6d Version: 0.3.99 Release: 1%{?dist} Summary: DHCPv6 server daemon %if 0%{?suse_version} Group: Productivity/Networking/Boot/Servers %else Group: System Environment/Daemons %endif License: GPLv2 URL: http://dhcpy6d.ifw-dresden.de Source0: http://%{name}.ifw-dresden.de/files-%{name}/%{name}-%{version}.tar.gz # in order to build from tarball # tar -zxvf dhcpy6d-%%{version}.tar.gz -C ~/ dhcpy6d-%%{version}/redhat/init.d/dhcpy6d --strip-components=4&& rpmbuild -ta dhcpy6d-%%{version}.tar.gz&& rm -f ~/dhcpy6d Source1: %{name} BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) BuildArch: noarch BuildRequires: python Requires: python %if 0%{?suse_version} Requires: python-mysql Requires: python-dnspython %else Requires: MySQL-python Requires: python-dns %endif Requires: coreutils Requires: filesystem Requires(pre): /usr/sbin/useradd, /usr/sbin/groupadd Requires(post): coreutils, filesystem, /sbin/chkconfig Requires(preun): /sbin/service, coreutils, /sbin/chkconfig, /usr/sbin/userdel, /usr/sbin/groupdel Requires(postun): /sbin/service Requires: /etc/init.d, logrotate %description Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack transistion, addresses may be generated randomly, by range, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically. %prep %setup -q %build CFLAGS="%{optflags}" %{__python} setup.py build %install %{__python} setup.py install --skip-build --prefix=%{_prefix} --install-scripts=%{_sbindir} --root=%{buildroot} install -p -D -m 555 %{S:1} %{buildroot}%{_sysconfdir}/init.d/%{name} install -p -D -m 644 etc/logrotate.d/%{name} %{buildroot}%{_sysconfdir}/logrotate.d/%{name} /bin/chmod 0550 %{buildroot}%{_sbindir}/%{name} %pre # enable that only for non-root user! %if "%{dhcpy6d_uid}" != "root" /usr/sbin/groupadd -f -r %{dhcpy6d_gid} > /dev/null 2>&1 || : /usr/sbin/useradd -r -s /sbin/nologin -d /var/lib/%{name} -M \ -g %{dhcpy6d_gid} %{dhcpy6d_uid} > /dev/null 2>&1 || : %endif %post file=/var/log/%{name}.log if [ ! -f ${file} ] then /bin/touch ${file} fi /bin/chown %{dhcpy6d_uid}:%{dhcpy6d_gid} ${file} /bin/chmod 0640 ${file} # proper service handling http://en.opensuse.org/openSUSE:Cron_rename %{?fillup_and_insserv: %{fillup_and_insserv -y %{name}} } %{!?fillup_and_insserv: # undefined /sbin/chkconfig --add %{name} #/sbin/chkconfig %{name} on } %preun if [ "$1" = "0" ]; then /sbin/service %{name} stop > /dev/null 2>&1 || : /bin/rm -f /var/lib/%{name}/pid > /dev/null 2>&1 || : %{?stop_on_removal: %{stop_on_removal %{name}} } %{!?stop_on_removal: # undefined /sbin/chkconfig %{name} off /sbin/chkconfig --del %{name} } # enable that only for non-root user! %if "%{dhcpy6d_uid}" != "root" /usr/sbin/userdel %{dhcpy6d_uid} if [ ! `grep %{dhcpy6d_gid} /etc/group` = "" ]; then /usr/sbin/groupdel %{dhcpy6d_uid} fi %endif fi %postun if [ $1 -ge 1 ]; then %{?restart_on_update: %{restart_on_update %{name}} %insserv_cleanup } %{!?restart_on_update: # undefined /sbin/service %{name} condrestart > /dev/null 2>&1 || : } fi %files %doc %{_defaultdocdir}/* %{_mandir}/man?/* #%{_mandir}/man5/dhcpy6d-clients.conf.5 #%{_mandir}/man8/dhcpy6d.8 %{_sbindir}/%{name} %{python_sitelib}/*dhcpy6* %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} %config(noreplace) %{_sysconfdir}/%{name}.conf %exclude %{_localstatedir}/log/%{name}.log %{_sysconfdir}/init.d/%{name} %dir %attr(0775,%{dhcpy6d_uid},%{dhcpy6d_gid}) %{_localstatedir}/lib/%{name} %config(noreplace) %attr(0644,%{dhcpy6d_uid},%{dhcpy6d_gid}) %{_localstatedir}/lib/%{name}/volatile.sqlite %changelog * Tue Oct 21 2014 Henri Wahl - 0.4-1 - New upstream release * Sun Jun 09 2013 Marcin Dulak - 0.2-1 - RHEL and openSUSE versions based on Christopher Meng's spec * Tue Jun 04 2013 Christopher Meng - 0.2-1 - New upstream release. * Thu May 09 2013 Christopher Meng - 0.1.3-1 - Initial Package. dhcpy6d/redhat/init.d/000077500000000000000000000000001242200350000150375ustar00rootroot00000000000000dhcpy6d/redhat/init.d/dhcpy6d000077500000000000000000000035401242200350000163300ustar00rootroot00000000000000#!/bin/sh # ### BEGIN INIT INFO # Provides: dhcpy6d # Required-Start: $syslog $network $remote_fs # Required-Stop: $syslog $remote_fs # Should-Start: $local_fs # Should-Stop: $local_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start and stop the DHCPv6 server dhcpy6d # Description: dhcpy6d is a DHCPv6 server ### END INIT INFO # chkconfig: - 65 35 # description: Start and stop the DHCPv6 server dhcpy6d # processname: dhcpy6d # config: /etc/dhcpy6d.conf # pidfile: /var/run/dhcpy6d.pid # this init.d file derived from /etc/init.d/dhcpd6 . /etc/rc.d/init.d/functions RETVAL=0 prog=dhcpy6d exec=/usr/sbin/dhcpy6d lockfile=/var/lock/subsys/dhcpy6d pidfile=/var/run/dhcpy6d.pid config=/etc/dhcpy6d.conf user=dhcpy6d group=dhcpy6d rh_status() { status -p $pidfile -l $(basename $lockfile) $exec } rh_status_q() { rh_status >/dev/null 2>&1 } start() { [ `id -u` -eq 0 ] || return 4 [ -x $exec ] || return 5 [ -f $config ] || return 6 rh_status_q && return 0 echo -n $"Starting $prog: " $exec --config $config --user $user --group $group & RETVAL=$? pid=$! echo $pid > $pidfile [ $RETVAL -eq 0 ] && touch $lockfile [ $RETVAL -eq 0 ] && success $"$base startup" || failure $"$base startup" echo return $RETVAL } stop() { [ `id -u` -eq 0 ] || return 4 rh_status_q || return 0 echo -n $"Shutting down $prog: " killproc -p $pidfile $prog RETVAL=$? echo [ $RETVAL -eq 0 ] && rm -f $lockfile return $RETVAL } usage() { echo $"Usage: $0 {start|stop|restart|force-reload|status}" } if [ $# -gt 1 ]; then exit 2 fi case "$1" in start) start ;; stop) stop ;; restart|force-reload) stop ; start ;; status) rh_status ;; *) usage exit 2 ;; esac exit $? dhcpy6d/setup.py000077500000000000000000000064741242200350000141330ustar00rootroot00000000000000#!/usr/bin/env python # encoding: utf-8 # dhcpy6d - DHCPv6 server # Copyright (C) 2012 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the # # Free Software Foundation # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301 # USA from distutils.core import setup import os.path CLASSIFIERS = [ 'Intended Audience :: System Administrators', 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX', 'Natural Language :: English', 'Programming Language :: Python', 'Topic :: System :: Networking' ] data_files_custom = [('/var/lib/dhcpy6d', ['var/lib/volatile.sqlite']),\ ('/var/log', ['var/log/dhcpy6d.log']),\ ('/usr/share/doc/dhcpy6d', ['doc/clients-example.conf',\ 'doc/config.sql',\ 'doc/dhcpy6d-example.conf',\ 'doc/dhcpy6d-minimal.conf',\ 'doc/LICENSE',\ 'doc/volatile.sql']),\ ('/usr/share/man/man5', ['man/man5/dhcpy6d.conf.5',\ 'man/man5/dhcpy6d-clients.conf.5']), ('/usr/share/man/man8', ['man/man8/dhcpy6d.8']),\ ('/etc', ['etc/dhcpy6d.conf']),] # RPM creation uses more files as data_files which on Debian are # installed via debhelpers # /tmp/DHCPY6D_BUILDING_RPM has been touched by build.sh if os.path.exists("/tmp/DHCPY6D_BUILDING_RPM"): scripts_custom = '' data_files_custom.append(('/usr/sbin', ['dhcpy6d'])) data_files_custom.append(('/etc/logrotate.d', ['etc/logrotate.d/dhcpy6d'])) data_files_custom.append(('/etc/init.d', ['redhat/init.d/dhcpy6d'])) else: scripts_custom = ['dhcpy6d'] setup(name = 'dhcpy6d', version = '0.4', license = 'GNU GPL v2', description = 'DHCPv6 server daemon', long_description = 'Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack transistion, addresses may be generated randomly, by range, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically.', classifiers = CLASSIFIERS, author = 'Henri Wahl', author_email = 'h.wahl@ifw-dresden.de', url = 'http://dhcpy6d.ifw-dresden.de', download_url = 'http://dhcpy6d.ifw-dresden.de/download', py_modules = ['dhcpy6.Helpers', 'dhcpy6.Constants',\ 'dhcpy6.Config', 'dhcpy6.Storage'], data_files = data_files_custom,\ scripts = scripts_custom\ ) dhcpy6d/var/000077500000000000000000000000001242200350000131735ustar00rootroot00000000000000dhcpy6d/var/lib/000077500000000000000000000000001242200350000137415ustar00rootroot00000000000000dhcpy6d/var/lib/volatile.sqlite000066400000000000000000000120001242200350000167740ustar00rootroot00000000000000SQLite format 3@ -% #!!tablemacs_llipsmacs_llipsCREATE TABLE "macs_llips" ("mac" VARCHAR PRIMARY KEY NOT NULL, "link_local_ip" VARCHAR NOT NULL, "last_update" DATETIME NOT NULL)3G!indexsqlite_autoindex_macs_llips_1macs_llipstableleasesleasesCREATE TABLE "leases" ("address" VARCHAR PRIMARY KEY NOT NULL, "active" INTEGER NOT NULL, "last_message" INTEGER NOT NULL, "preferred_lifetime" INTEGER NOT NULL, "valid_lifetime" INTEGER NOT NULL, "hostname" VARCHAR NOT NULL, "type" VARCHAR NOT NULL, "category" VARCHAR NOT NULL, "ia_type" VARCHAR NOT NULL, "class" VARCHAR NOT NULL, "mac" VARCHAR NOT NULL,"duid" VARCHAR NOT NULL, "iaid" VARCHAR NOT NULL, "last_update" DATETIME NOT NULL,"preferred_until" DATETIME NOT NULL, "valid_until" DATETIME NOT NULL)+?indexsqlite_autoindex_leases_1leases    dhcpy6d/var/log/000077500000000000000000000000001242200350000137545ustar00rootroot00000000000000dhcpy6d/var/log/dhcpy6d.log000066400000000000000000000000001242200350000160060ustar00rootroot00000000000000