-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
dhcpv6-pd_ia: initial import of a DHCPv6 server bootstrapper
- Loading branch information
Showing
9 changed files
with
590 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# DHCPv6 PD_IA server | ||
|
||
This provides tooling to bootstrap a [DHCPv6] server with [prefix delegation] | ||
support ([Kea]) to provide a prefix for the [`gnrc_border_router` example]'s | ||
or similar applications' subnets. | ||
|
||
# Usage | ||
Just run the script `dhcpv6-pd_ia.py` with sudo, e.g. | ||
|
||
```sh | ||
sudo ./dhcpv6-pd_ia.py tap0 2001:db8::/32 | ||
``` | ||
|
||
For more information on the arguments, have a look at the usage information of | ||
the script | ||
|
||
```sh | ||
./dhcpv6-pd_ia.py -h | ||
``` | ||
|
||
|
||
[DHCPv6]: https://tools.ietf.org/html/rfc8415 | ||
[prefix delegation]: https://en.wikipedia.org/wiki/Prefix_delegation | ||
[Kea]: http://kea.isc.org | ||
[`gnrc_border_router` example]: ../../../examples/gnrc_border_router |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
# -*- coding: utf-8 -*- | ||
# vim:fenc=utf-8 | ||
# | ||
# Copyright (C) 2020 Freie Universität Berlin | ||
# | ||
# This file is subject to the terms and conditions of the GNU Lesser | ||
# General Public License v2.1. See the file LICENSE in the top level | ||
# directory for more details. | ||
|
||
|
||
import atexit | ||
import os | ||
import shutil | ||
import subprocess | ||
import time | ||
import threading | ||
import signal | ||
import sys | ||
|
||
import pkg | ||
|
||
|
||
__author__ = "Martine S. Lenders" | ||
__copyright__ = "Copyright (C) 2020 Freie Universität Berlin" | ||
__credits__ = ["Martine S. Lenders"] | ||
__license__ = "LGPLv2.1" | ||
__maintainer__ = "Martine S. Lenders" | ||
__email__ = "m.lenders@fu-berlin.de" | ||
|
||
|
||
# see https://refactoring.guru/design-patterns/singleton/python/example | ||
class _SingletonMeta(type): | ||
""" | ||
This is a thread-safe implementation of Singleton. | ||
""" | ||
|
||
_instance = None | ||
|
||
_lock = threading.Lock() | ||
""" | ||
We now have a lock object that will be used to synchronize threads | ||
during first access to the Singleton. | ||
""" | ||
|
||
def __call__(cls, *args, **kwargs): | ||
# Now, imagine that the program has just been launched. Since | ||
# there's no Singleton instance yet, multiple threads can | ||
# simultaneously pass the previous conditional and reach this point | ||
# almost at the same time. The first of them will acquire lock and | ||
# will proceed further, while the rest will wait here. | ||
with cls._lock: | ||
# The first thread to acquire the lock, reaches this | ||
# conditional, goes inside and creates the Singleton instance. | ||
# Once it leaves the lock block, a thread that might have been | ||
# waiting for the lock release may then enter this section. But | ||
# since the Singleton field is already initialized, the thread | ||
# won't create a new object. | ||
if not cls._instance: | ||
cls._instance = super().__call__(*args, **kwargs) | ||
return cls._instance | ||
|
||
|
||
class DHCPv6Server(metaclass=_SingletonMeta): | ||
""" | ||
Inspired by Daemon class | ||
https://web.archive.org/web/20160305151936/http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ | ||
""" | ||
PORT = 547 | ||
command = None | ||
package = None | ||
installer = None | ||
|
||
def __init__(self, daemonized=False, pidfile=None, stdin=None, stdout=None, | ||
stderr=None): | ||
if self.command is None or self.package is None: | ||
raise NotImplementedError("Please inherit from {} and set the " | ||
"the static attributes `command` and " | ||
"`packet`".format(type(self)), file=sys) | ||
assert (not daemonized) or (pidfile is not None) | ||
self.daemonized = daemonized | ||
self.pidfile = pidfile | ||
self.stdin = stdin | ||
self.stdout = stdout | ||
self.stderr = stderr | ||
|
||
@classmethod | ||
def is_installed(cls): | ||
return shutil.which(cls.command[0]) is not None | ||
|
||
def install(self): | ||
self.installer = pkg.PackageManagerFactory.get_installer() | ||
self.installer.install(self.package) | ||
|
||
def daemonize(self): | ||
""" | ||
do the UNIX double-fork magic, see Stevens' "Advanced | ||
Programming in the UNIX Environment" for details (ISBN 0201563177) | ||
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 | ||
""" | ||
try: | ||
pid = os.fork() | ||
if pid > 0: | ||
# exit first parent | ||
sys.exit(0) | ||
except OSError as e: | ||
sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) | ||
sys.exit(1) | ||
|
||
# decouple from parent environment | ||
os.chdir("/") | ||
os.setsid() | ||
os.umask(0) | ||
|
||
# do second fork | ||
try: | ||
pid = os.fork() | ||
if pid > 0: | ||
# exit from second parent | ||
sys.exit(0) | ||
except OSError as e: | ||
sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) | ||
sys.exit(1) | ||
|
||
# redirect standard file descriptors | ||
if self.stdin: | ||
si = open(self.stdin, 'r') | ||
os.dup2(si.fileno(), sys.stdin.fileno()) | ||
if self.stdout: | ||
sys.stdout.flush() | ||
so = open(self.stdout, 'a+') | ||
os.dup2(so.fileno(), sys.stdout.fileno()) | ||
if self.stderr: | ||
sys.stderr.flush() | ||
se = open(self.stderr, 'a+', 0) | ||
os.dup2(se.fileno(), sys.stderr.fileno()) | ||
|
||
atexit.register(self.delpid) | ||
# write pidfile | ||
with open(self.pidfile, "w+") as f: | ||
f.write("{}\n".format(os.getpid())) | ||
|
||
def delpid(self): | ||
os.remove(self.pidfile) | ||
|
||
def stop(self): | ||
""" | ||
Stop the daemon | ||
""" | ||
# Get the pid from the pidfile | ||
try: | ||
pf = open(self.pidfile, 'r') | ||
pid = int(pf.read().strip()) | ||
pf.close() | ||
except IOError: | ||
pid = None | ||
if not pid: | ||
message = "pidfile %s does not exist. Daemon not running?\n" | ||
sys.stderr.write(message % self.pidfile) | ||
return # not an error in a restart | ||
|
||
# Try killing the daemon process | ||
try: | ||
while 1: | ||
os.kill(pid, signal.SIGTERM) | ||
time.sleep(0.1) | ||
except OSError as err: | ||
err = str(err) | ||
if err.find("No such process") > 0: | ||
if os.path.exists(self.pidfile): | ||
os.remove(self.pidfile) | ||
else: | ||
print(str(err), file=sys.stderr) | ||
sys.exit(1) | ||
|
||
def pre_run(self): | ||
# may be overridden by implementation to do things before running | ||
# the daemon or script | ||
pass | ||
|
||
def run(self): | ||
if not self.is_installed(): | ||
self.install() | ||
if self.daemonized: | ||
""" | ||
Start the daemon | ||
""" | ||
# Check for a pidfile to see if the daemon already runs | ||
try: | ||
pf = open(self.pidfile, 'r') | ||
pid = int(pf.read().strip()) | ||
pf.close() | ||
except (IOError, ValueError): | ||
pid = None | ||
if pid: | ||
message = "pidfile %s already exist. Daemon already running?\n" | ||
sys.stderr.write(message % self.pidfile) | ||
sys.exit(1) | ||
# Start the daemon | ||
self.daemonize() | ||
self.pre_run() | ||
subprocess.run(self.command) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
# vim:fenc=utf-8 | ||
# | ||
# Copyright (C) 2020 Freie Universität Berlin | ||
# | ||
# This file is subject to the terms and conditions of the GNU Lesser | ||
# General Public License v2.1. See the file LICENSE in the top level | ||
# directory for more details. | ||
|
||
import argparse | ||
import os | ||
|
||
import kea | ||
import util | ||
|
||
__author__ = "Martine S. Lenders" | ||
__copyright__ = "Copyright (C) 2020 Freie Universität Berlin" | ||
__credits__ = ["Martine S. Lenders"] | ||
__license__ = "LGPLv2.1" | ||
__version__ = "0.0.1" | ||
__maintainer__ = "Martine S. Lenders" | ||
__email__ = "m.lenders@fu-berlin.de" | ||
__status__ = "Experimental" | ||
|
||
|
||
DEFAULT_NEXT_HOP = "fe80::2" | ||
DEFAULT_DELEGATED_LEN = 64 | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument("-d", "--daemonized", action="store_true", | ||
help="Run server in background") | ||
parser.add_argument("-p", "--pidfile", nargs="?", | ||
help="PID file for the server. Required with -d.") | ||
parser.add_argument( | ||
"-n", "--next-hop", default=DEFAULT_NEXT_HOP, nargs="?", | ||
help="Next hop address for application (default: fe80::2)" | ||
) | ||
parser.add_argument( | ||
"-g", "--delegated-len", default=DEFAULT_DELEGATED_LEN, nargs="?", | ||
type=int, | ||
help="The prefix length delegated by the DHCPv6 server. " | ||
"Must be greater or equal to the prefix length of the subnet. " | ||
"This may differ from the prefix length provided in subnet more " | ||
"to understand as a template from which to generate the " | ||
"delegated prefixes from. " | ||
"(default: 64)" | ||
) | ||
parser.add_argument( | ||
"interface", help="Interface to bind DHCPv6 server to" | ||
) | ||
parser.add_argument( | ||
"subnet", type=util.split_prefix, | ||
help="Subnet to delegate (must have format <prefix>/<prefix_len>)" | ||
) | ||
args = parser.parse_args() | ||
if "SUDO_USER" not in os.environ: | ||
raise PermissionError("Must be run with sudo") | ||
if args.delegated_len < args.subnet[1]: | ||
raise ValueError("delegated_len {} is lesser than prefix length {}" | ||
.format(args.delegated_len, args.subnet[1])) | ||
config = kea.KeaConfig(args.interface, args.subnet[0], args.subnet[1], | ||
args.delegated_len) | ||
server = kea.KeaServer(config, args.next_hop, daemonized=args.daemonized, | ||
pidfile=args.pidfile) | ||
server.run() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.