├── docs ├── source │ ├── README.rst │ ├── images │ │ ├── dhcpcanon.ico │ │ ├── dhcpcanon_logo.png │ │ ├── packages_dhcpcanon.svg │ │ ├── dhcpcapfsm.svg │ │ └── classes_dhcpcanon.svg │ ├── download.rst │ ├── todo.rst │ ├── diagrams.rst │ ├── index.rst │ ├── running.rst │ ├── install_run_cases.rst │ ├── api.rst │ ├── questions.rst │ ├── implementation.rst │ ├── contributing.rst │ ├── soa.rst │ ├── privileges.rst │ ├── install.rst │ ├── integration.rst │ └── specification.rst ├── make.bat └── Makefile ├── AUTHORS ├── systemd ├── network │ └── 90-dhcpcanon.link └── dhcpcanon.service ├── requirements_docs.txt ├── setup.cfg ├── requirements.txt ├── tmpfiles.d └── dhcpcanon.conf ├── MANIFEST.in ├── dhcpcanon ├── _version.py ├── dhcpcaputils.py ├── __init__.py ├── conflog.py ├── clientscript.py ├── dhcpcanon.py ├── dhcpcaplease.py ├── netutils.py ├── timers.py ├── constants.py └── dhcpcap.py ├── .readthedocs.yml ├── requirements_dev.txt ├── .gitignore ├── .coveragerc ├── .editorconfig ├── CHANGELOG.rst ├── tests ├── conftest.py ├── test_dhcpcap.py ├── dhcpcap_leases.py ├── dhcpcap_objs.py ├── dhcpcap_pkts.py └── test_dhcpcapfsm.py ├── man ├── dhcpcanon.8 └── dhcpcanon-script.8 ├── tox.ini ├── LICENSE ├── LICENSES └── MIT.txt ├── install.sh ├── .travis.yml ├── CONTRIBUTING.md ├── setup.py ├── apparmor.d └── sbin.dhcpcanon ├── Makefile └── README.rst /docs/source/README.rst: -------------------------------------------------------------------------------- 1 | ../../README.rst -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | juga (juga at riseup dot net) 2 | -------------------------------------------------------------------------------- /systemd/network/90-dhcpcanon.link: -------------------------------------------------------------------------------- 1 | [Link] 2 | MacAddressPolicy=random 3 | -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | sphinxcontrib-plantuml 2 | sphinx-bootstrap-theme 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [aliases] 5 | # test=pytest 6 | -------------------------------------------------------------------------------- /docs/source/images/dhcpcanon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juga0/dhcpcanon/HEAD/docs/source/images/dhcpcanon.ico -------------------------------------------------------------------------------- /docs/source/images/dhcpcanon_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juga0/dhcpcanon/HEAD/docs/source/images/dhcpcanon_logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # to do not maintain this file, use setup.py 2 | --index-url https://pypi.python.org/simple/ 3 | 4 | -e . 5 | -------------------------------------------------------------------------------- /tmpfiles.d/dhcpcanon.conf: -------------------------------------------------------------------------------- 1 | # see tmpfiles.d(5) 2 | #Type Path Mode UID GID Age Argument 3 | d /run/dhcpcanon 0755 root root - - 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.rst 3 | include AUTHORS LICENSE requirements.txt 4 | recursive-include docs * 5 | prune docs/build 6 | recursive-include man * 7 | -------------------------------------------------------------------------------- /dhcpcanon/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | 5 | version = "0.8.5" 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.5 6 | # pip_install: true 7 | # extra_requirements: 8 | # - doc 9 | requirements_file: requirements_docs.txt 10 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | # to do not maintain this file, use setup.py 2 | --index-url https://pypi.python.org/simple/ 3 | 4 | -e . 5 | # from setup.py extras_require 6 | dhcpcanon[test] 7 | dhcpcanon[dev] 8 | dhcpcanon[doc] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | .*swp 4 | *.egg-info/ 5 | deb_dist 6 | dist 7 | build 8 | __pycache__ 9 | docs/build 10 | docs/source/images/*.dot 11 | docs/source/images/*.pdf 12 | docs/source/images/*.dia 13 | .cache 14 | .tox 15 | .coverage* 16 | htmlcov 17 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = dhcpcanon 4 | parallel = True 5 | 6 | [paths] 7 | source = 8 | dhcpcanon 9 | .tox/*/lib/python*/site-packages/dhcpcanon 10 | .tox/*/lib/site-packages/dhcpcanon 11 | .tox/pypy*/site-packages/dhcpcanon 12 | 13 | [report] 14 | omit = 15 | */tests/* 16 | */python?.?/* 17 | *__init__* 18 | -------------------------------------------------------------------------------- /systemd/dhcpcanon.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=dhcpcanon DHCP client Anonymity Profile 3 | Documentation=man:dhcpcanon(8) 4 | 5 | [Service] 6 | RuntimeDirectory=dhcpcanon 7 | RuntimeDirectoryMode=0775 8 | ExecStart=/sbin/dhcpcanon $ARGS 9 | AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW 10 | User=dhcpcanon 11 | 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /docs/source/download.rst: -------------------------------------------------------------------------------- 1 | .. _download: 2 | 3 | Download dhcpcanon 4 | =================== 5 | 6 | You can download this project in either 7 | `zip `__ 8 | or `tar `__ formats. 9 | 10 | You can also clone the project with Git by running: 11 | git clone git://github.com/juga0/dhcpcanon 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # this should work for all editors that support .editorconfig! 2 | # 3 | # on debian, emacs users should install elpa-editorconfig and vim 4 | # users should install vim-editorconfig. 5 | 6 | root = true 7 | 8 | [*] 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | end_of_line = lf 13 | charset = utf-8 14 | indent_size = 4 15 | max_line_length = 78 16 | 17 | [Makefile] 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /docs/source/todo.rst: -------------------------------------------------------------------------------- 1 | .. _todo: 2 | 3 | TODO 4 | ===== 5 | 6 | [X] create debian package 7 | 8 | [X] create documentation 9 | 10 | [X] calculate retransmission times for DISCOVER 11 | 12 | [X] create tests 13 | 14 | [ ] (WIP) integrate with Network Manager 15 | 16 | [ ] listen in several interfaces 17 | 18 | [X] create systemd service 19 | 20 | [X] create init.d daemon: Won't fix. 21 | 22 | [X] limit privileges 23 | 24 | [X] include MAC anonymization module: Debian package suggest it. 25 | 26 | [X] create apparmor profile 27 | 28 | [ ] implement IPv6 29 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | =========== 3 | 4 | 0.3.1 5 | ------ 6 | * Restore self.my_send for sendp: my_send uses L3Socket, my_send L2Socket 7 | * conf.checkIPaddr does not have effect 8 | * Remove forgotten GPL license text 9 | * Add changelog 10 | * Fix travis style and dependencies for documentation 11 | * Fix running documentation 12 | 13 | 0.3.0 14 | ------ 15 | 16 | Multiple changes and refactoring. 17 | 18 | * Python 3 compatible 19 | * Add tests 20 | * Improve docstrings 21 | * Update documentation and diagrams 22 | 23 | 24 | v0.1.8.2 25 | --------- 26 | -------------------------------------------------------------------------------- /docs/source/diagrams.rst: -------------------------------------------------------------------------------- 1 | .. _diagrams: 2 | 3 | ``dhcpcanon`` diagrams 4 | ========================= 5 | 6 | Finite State Machine diagram 7 | -------------------------------- 8 | 9 | .. image:: ./images/dhcpcapfsm.* 10 | 11 | Classes diagram 12 | --------------- 13 | 14 | .. image:: ./images/classes_dhcpcanon.* 15 | 16 | Packages diagram 17 | -------------------- 18 | 19 | .. image:: ./images/packages_dhcpcanon.* 20 | 21 | Calls diagram 22 | --------------- 23 | 24 | .. image:: ./images/calls_dhcpcanon.* 25 | 26 | Organigram 27 | ----------- 28 | 29 | This organigram does not reflect the current status of ``dhcpcanon``, 30 | but as it should be changed 31 | 32 | .. image:: ./images/organigram_dhcpcanon.* 33 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 3 | # SPDX-FileCopyrightText: 2016, juga 4 | # SPDX-License-Identifier: MIT 5 | """.""" 6 | import pytest 7 | from dhcpcanon.dhcpcap import DHCPCAP 8 | 9 | 10 | @pytest.fixture 11 | def dhcpcap_maker(request): 12 | """ return a function which creates initialized dhcpcap instances. """ 13 | 14 | def maker(): 15 | dhcpcap = DHCPCAP(client_mac="00:01:02:03:04:05", iface='eth0', 16 | xid=900000000) 17 | return dhcpcap 18 | return maker 19 | 20 | 21 | @pytest.fixture 22 | def dhcpcap(dhcpcap_maker): 23 | """ return an initialized dhcpcap instance. """ 24 | return dhcpcap_maker() 25 | -------------------------------------------------------------------------------- /man/dhcpcanon.8: -------------------------------------------------------------------------------- 1 | .\" Manpage for dhcpcanon. 2 | .\" Contact ju@riseup.net to correct errors or typos. 3 | .TH man 8 "25 Nov 2016" "1.0" "dhcpcanon man page" 4 | .SH NAME 5 | dhcpcanon \- DHCP Client Anonymity profile 6 | .SH SYNOPSIS 7 | dhcpcanon [-h] [-d] [-l LEASE] [-v] [interface] 8 | .SH DESCRIPTION 9 | dhcpcanon is a DCHP client implementation of the DHCP Anonymity Profiles (RFC7844). 10 | using Scapy Automaton. 11 | .SH OPTIONS 12 | The options which apply to the dhcpcanon command are: 13 | 14 | -h, --help 15 | Show help message and exit. 16 | 17 | -d, --debug 18 | Show debug messages. 19 | 20 | -l, --lease LEASE 21 | Custom lease time. 22 | 23 | -v, --version 24 | Show version. 25 | .SH AUTHOR 26 | ju xor (ju@riseup.net) 27 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. DHCP anonymity profile documentation master file, created by 2 | sphinx-quickstart on Sat Jul 23 19:57:52 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: README.rst 7 | 8 | Contents: 9 | ========= 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | install 15 | download 16 | running 17 | install_run_cases 18 | todo 19 | contributing 20 | soa 21 | specification 22 | questions 23 | implementation 24 | privileges 25 | integration 26 | api 27 | diagrams 28 | 29 | Indices and tables 30 | ================== 31 | 32 | * :ref:`genindex` 33 | * :ref:`modindex` 34 | * :ref:`search` 35 | -------------------------------------------------------------------------------- /docs/source/running.rst: -------------------------------------------------------------------------------- 1 | .. _running: 2 | 3 | Running dhcpcanon 4 | ================== 5 | 6 | If ``dhcpcanon`` has be installed with systemd, it can be started with:: 7 | 8 | sudo systemctl start dhcpcanon 9 | 10 | After installing, it can also be run manually:: 11 | 12 | sudo dhcpcanon 13 | 14 | There is no need to pass any argument, most of the arguments are only used when 15 | ``dhcpcanon`` is called by other program (``systemd`` or 16 | ``gnome network manager``) and mimic the ``dhclient`` arguments. 17 | 18 | You can specify which network interface to use passing it as an argument. 19 | Without specificying the network interface, it will use the active interface. 20 | 21 | An useful argument when reporting bugs is ``-v``. 22 | 23 | An updated command line usage description can be obtained with:: 24 | 25 | dhcpcanon -h 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = clean, lint, py35, stats 3 | #doc{py27,py35}-{pinned,unpinned}, stats 4 | 5 | [testenv:clean] 6 | skip_install = True 7 | changedir={toxinidir} 8 | deps = 9 | coverage 10 | commands= 11 | coverage erase 12 | 13 | [testenv:lint] 14 | skip_install = True 15 | deps = .[dev] 16 | commands = 17 | flake8 --max-line-length 79 --ignore=E402,E123 dhcpcanon scripts tests 18 | 19 | [testenv:stats] 20 | skip_install = True 21 | changedir={toxinidir} 22 | deps = 23 | coverage 24 | commands= 25 | coverage combine 26 | coverage report 27 | coverage html 28 | 29 | [testenv] 30 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 31 | deps = .[test] 32 | commands = 33 | coverage run --rcfile={toxinidir}/.coveragerc --source=dhcpcanon -m pytest -svx {toxinidir}/tests 34 | 35 | [testenv:doc] 36 | deps = .[doc] 37 | whitelist_externals = make 38 | changedir = docs 39 | commands = 40 | make html 41 | # this requires build the pdf images 42 | # make latexpdf 43 | # this requires network 44 | # make linkcheck 45 | -------------------------------------------------------------------------------- /docs/source/install_run_cases.rst: -------------------------------------------------------------------------------- 1 | .. _install_run_cases: 2 | 3 | Installation and running cases 4 | =================================== 5 | 6 | system files 7 | ------------- 8 | 9 | sbin/dhcpcanon-script 10 | systemd/dhcpcanon.service 11 | tmpfiles.d/dhcpcanon.conf 12 | systemd/network/90-dhcpcanon.link 13 | console_scripts -> /sbin/dhcpcanon 14 | 15 | run cases 16 | ---------- 17 | 18 | 1. standalone without systemd, using -sp sbin/dhcpcanon-script 19 | 2. standalone without systemd, using resolvconf 20 | 3. standalone without systemd, using resolvconf-admin 21 | 4. launched with a wrapper, using -sp sbin/dhcpcanon-script 22 | 5. launched with a wrapper, sing resolvconf 23 | 6. launched with a wrapper, using resolvconf-admin 24 | 7. launched as systemd service, using systemd-resolved 25 | 26 | install from 27 | ------------- 28 | 29 | * setup.py: dhcpcanon-scriptresolvconf, resolvconf-admin and/or systemd 30 | need to be installed manually 31 | * pip: dhcpcanon-scriptresolvconf, resolvconf-admin and/or systemd 32 | need to be installed manually 33 | * Makefile 34 | * Debian 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016, 2017, juga 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016, 2017, juga 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: 2016, juga 4 | # SPDX-License-Identifier: MIT 5 | 6 | mkdir -p /sbin 7 | for i in sbin/dhcpcanon-script; do install "$i" /sbin; done 8 | mkdir -p /share/doc/dhcpcanon 9 | for i in README.md LICENSE; do install -m 644 "$i" /share/doc/dhcpcanon; done 10 | mkdir -p /share/man/man8 11 | for i in man/dhcpcanon.8; do install -m 644 "$i" /share/man/man8; done 12 | python3 setup.py install --record installed.txt --install-scripts=/sbin 13 | adduser --system dhcpcanon 14 | mkdir -p /lib/systemd/system 15 | cp systemd/dhcpcanon.service /lib/systemd/system/dhcpcanon.service 16 | mkdir -p /lib/tmpfiles.d 17 | for i in tmpfiles.d/dhcpcanon.conf; do install -m 644 "$i" /lib/tmpfiles.d; done 18 | systemctl enable /lib/systemd/system/dhcpcanon.service 19 | systemd-tmpfiles --create --root=/lib/tmpfiles.d/dhcpcanon.conf 20 | 21 | mkdir -p /lib/systemd/network 22 | for i in systemd/network/90-dhcpcanon.link; do install -m 644 "$i" /lib/systemd/network; done 23 | mkdir -p /etc/apparmor.d 24 | for i in apparmor.d/sbin.dhcpcanon; do install -m 644 "$i" /etc/apparmor.d; done 25 | for i in apparmor.d/sbin.dhcpcanon; do aa-complain /etc/apparmor.d/"$i"; done 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: required 3 | dist: trusty 4 | python: 5 | - '3.5' 6 | 7 | before_install: 8 | - sudo apt update && sudo apt install -y inkscape libdbus-glib-1-dev 9 | # - echo "Python diagnostic Information" 10 | # - env 11 | # - sudo dpkg -l|grep python|grep dev 12 | # - 'which -a python' 13 | # - python --version 14 | # - python-config --includes 15 | # - ls -l ~/virtualenv 16 | 17 | install: 18 | - pip install "dhcpcanon[dev, test, doc]" 19 | 20 | env: 21 | global: 22 | # The dbus-python maintainer hit these bugs on travis, see his comments on the 23 | # travis bug report: https://github.com/travis-ci/travis-ci/issues/6530 24 | # See https://github.com/mitya57/secretstorage/blob/2636a47b45aff51a21dfaf1cbd9f1f3c1347f7f4/.travis.yml for an example workaround 25 | # This is also a known issue and filed on the python bugtracker: https://bugs.python.org/issue7352 26 | - 'PYTHON_LIBS="-L$(python-config --prefix)/lib $(python-config --libs)"' 27 | - PY_ENABLE_SHARD=0 28 | - TOX_ENV=lint 29 | - TOX_ENV=py35,stats 30 | - TOX_ENV=doc 31 | 32 | script: 33 | - tox -c tox.ini -e $TOX_ENV 34 | # this is already in tox 35 | # - coverage run --source=dhcpcanon setup.py test 36 | 37 | after_success: 38 | - coveralls 39 | - codecov 40 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | dhcpcanon Python API Reference 4 | =============================== 5 | 6 | .. autosummary:: 7 | 8 | dhcpcanon.dhcpcapfsm 9 | dhcpcanon.dhcpcap 10 | dhcpcanon.dhcpcaplease 11 | dhcpcanon.clientscript 12 | dhcpcanon.timers 13 | dhcpcanon.dhcpcaputils 14 | dhcpcanon.constants 15 | dhcpcanon.conflog 16 | 17 | dhcpcapfsm module 18 | ------------------- 19 | 20 | .. automodule:: dhcpcanon.dhcpcapfsm 21 | :members: 22 | :undoc-members: 23 | 24 | dhcpcap module 25 | ------------------- 26 | 27 | .. automodule:: dhcpcanon.dhcpcap 28 | :members: 29 | :undoc-members: 30 | 31 | dhcpcaplease module 32 | ------------------- 33 | 34 | .. automodule:: dhcpcanon.dhcpcaplease 35 | :members: 36 | :undoc-members: 37 | 38 | clientscript module 39 | -------------------- 40 | 41 | .. automodule:: dhcpcanon.clientscript 42 | :members: 43 | :undoc-members: 44 | 45 | timers module 46 | ------------------- 47 | 48 | .. automodule:: dhcpcanon.timers 49 | :members: 50 | :undoc-members: 51 | 52 | dhcpcaputils module 53 | -------------------- 54 | 55 | .. automodule:: dhcpcanon.dhcpcaputils 56 | :members: 57 | :undoc-members: 58 | 59 | constants module 60 | ------------------- 61 | 62 | .. automodule:: dhcpcanon.constants 63 | :members: 64 | :undoc-members: 65 | 66 | conflog module 67 | ------------------- 68 | 69 | .. automodule:: dhcpcanon.conflog 70 | :members: 71 | :undoc-members: 72 | -------------------------------------------------------------------------------- /dhcpcanon/dhcpcaputils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Util functions for the DHCP client implementation of the Anonymity Profile 5 | ([:rfc:`7844`]).""" 6 | from __future__ import absolute_import 7 | 8 | import logging 9 | import random 10 | 11 | from scapy.arch.linux import get_if_list 12 | from scapy.layers.dhcp import DHCP, DHCPTypes 13 | 14 | from .constants import XID_MIN, XID_MAX 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def isoffer(packet): 20 | """.""" 21 | if DHCP in packet and (DHCPTypes.get(packet[DHCP].options[0][1]) == 22 | 'offer' or packet[DHCP].options[0][1] == "offer"): 23 | logger.debug('Packet is Offer.') 24 | return True 25 | return False 26 | 27 | 28 | def isnak(packet): 29 | """.""" 30 | if DHCP in packet and (DHCPTypes.get(packet[DHCP].options[0][1]) == 31 | 'nak' or packet[DHCP].options[0][1] == 'nak'): 32 | logger.debug('Packet is NAK.') 33 | return True 34 | return False 35 | 36 | 37 | def isack(packet): 38 | """.""" 39 | if DHCP in packet and (DHCPTypes.get(packet[DHCP].options[0][1]) == 40 | 'ack' or packet[DHCP].options[0][1] == 'ack'): 41 | logger.debug('Packet is ACK.') 42 | return True 43 | return False 44 | 45 | 46 | def gen_xid(): 47 | return random.randint(XID_MIN, XID_MAX) 48 | 49 | 50 | def discover_ifaces(): 51 | ifaces = get_if_list() 52 | ifaces.remove('lo') 53 | logger.debug('Disovered interfaces %s.', ifaces) 54 | return ifaces 55 | 56 | 57 | def detect_speed_network(): 58 | # 100 Mbps = 100 Mb/s 59 | with open('/sys/class/net/eth0/speed') as fd: 60 | speed = fd.read() 61 | logger.debug('Net speed %s', speed) 62 | return speed 63 | 64 | 65 | # TODO 66 | def detect_initial_network(): 67 | pass 68 | -------------------------------------------------------------------------------- /tests/test_dhcpcap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # SPDX-FileCopyrightText: 2016, juga 4 | # SPDX-License-Identifier: MIT 5 | """""" 6 | import logging 7 | from datetime import datetime 8 | 9 | from dhcpcap_leases import LEASE_ACK, LEASE_REQUEST 10 | from dhcpcap_pkts import dhcp_ack, dhcp_discover, dhcp_offer, dhcp_request 11 | 12 | FORMAT = "%(levelname)s: %(filename)s:%(lineno)s - %(funcName)s - " + \ 13 | "%(message)s" 14 | logging.basicConfig(format=FORMAT, level=logging.DEBUG) 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class TestDHCPCAP: 19 | def test_intialize(self, dhcpcap): 20 | assert dhcpcap.client_mac == "00:01:02:03:04:05" 21 | assert dhcpcap.iface == "eth0" 22 | # assert dhcpcap.xid == 900000000 23 | # assert dhcpcap.prl == \ 24 | # b"\x01\x03\x06\x0f\x1f\x21\x2b\x2c\x2e\x2f\x79\xf9\xfc" 25 | # assert dhcpcap.client_ip == "0.0.0.0" 26 | # assert dhcpcap.client_port == 68 27 | # assert dhcpcap.server_mac == "ff:ff:ff:ff:ff:ff" 28 | # assert dhcpcap.server_ip == "255.255.255.255" 29 | # assert dhcpcap.server_port == 67 30 | # assert dhcpcap.lease is None 31 | # assert dhcpcap.event is None 32 | 33 | def test_gen_discover(self, dhcpcap): 34 | discover = dhcpcap.gen_discover() 35 | logger.debug(discover) 36 | logger.debug(dhcp_discover) 37 | assert discover == dhcp_discover 38 | 39 | def test_handle_offer(self, dhcpcap): 40 | dhcpcap.handle_offer(dhcp_offer) 41 | lease = dhcpcap.lease 42 | assert lease == LEASE_REQUEST 43 | 44 | def test_gen_request(self, dhcpcap): 45 | dhcpcap.lease = LEASE_REQUEST 46 | request = dhcpcap.gen_request() 47 | assert request == dhcp_request 48 | 49 | def test_handle_ack(self, dhcpcap): 50 | dhcpcap.lease = LEASE_REQUEST 51 | dhcpcap.handle_ack(dhcp_ack, datetime(2017, 6, 23)) 52 | lease = dhcpcap.lease 53 | assert lease == LEASE_ACK 54 | -------------------------------------------------------------------------------- /tests/dhcpcap_leases.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 3 | # SPDX-FileCopyrightText: 2016, juga 4 | # SPDX-License-Identifier: MIT 5 | """.""" 6 | from dhcpcanon.dhcpcaplease import DHCPCAPLease 7 | 8 | LEASE_INIT = DHCPCAPLease(interface='enp0s25', address='', server_id='', 9 | next_server='', router='', subnet_mask='', 10 | broadcast_address='', domain='', name_server='', 11 | lease_time='', renewal_time='', rebinding_time='', 12 | subnet_mask_cidr='', subnet='', expiry='', renew='', 13 | rebind='') 14 | 15 | 16 | LEASE_REQUEST = DHCPCAPLease(interface='eth0', address='192.168.1.23', 17 | server_id='192.168.1.1', 18 | next_server='192.168.1.1', 19 | router='192.168.1.1', subnet_mask='255.255.255.0', 20 | broadcast_address='192.168.1.255', 21 | domain='localdomain', 22 | name_server='192.168.1.1 8.8.8.8', 23 | lease_time='43200', 24 | renewal_time='21600', rebinding_time='37800', 25 | subnet_mask_cidr='24', subnet='192.168.1.0', 26 | expiry='', renew='', rebind='') 27 | 28 | 29 | LEASE_ACK = DHCPCAPLease(interface='eth0', address='192.168.1.23', 30 | server_id='192.168.1.1', next_server='192.168.1.1', 31 | router='192.168.1.1', subnet_mask='255.255.255.0', 32 | broadcast_address='192.168.1.255', 33 | domain='localdomain', 34 | name_server='192.168.1.1 8.8.8.8', lease_time='43200', 35 | renewal_time='21600', rebinding_time='37800', 36 | subnet_mask_cidr='24', subnet='192.168.1.0', 37 | expiry='17-06-23 12:00:00', renew='17-06-23 06:00:00', 38 | rebind='17-06-23 10:30:00') 39 | -------------------------------------------------------------------------------- /dhcpcanon/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """__init__ for the DHCP client implementation of the Anonymity Profile 5 | ([:rfc:`7844`]).""" 6 | from __future__ import absolute_import 7 | 8 | try: 9 | from ._version import version 10 | except ImportError: 11 | try: 12 | from setuptools_scm import get_version 13 | version = get_version() 14 | except (ImportError, LookupError): 15 | version = "0.8.5" 16 | 17 | __version__ = version 18 | __author__ = "juga" 19 | __author_mail__ = "juga@riseup.net" 20 | __description__ = "DHCP client disclosing less identifying information" 21 | __long_description__ = "Python implmentation of the DHCP Anonymity Profiles \ 22 | (RFC7844) designed for users that \ 23 | wish to remain anonymous to the visited network \ 24 | minimizing disclosure of identifying information." 25 | __website__ = 'https://github.com/juga0/dhcpcanon' 26 | __documentation__ = 'http://dhcpcanon.readthedocs.io/en/' + __version__ 27 | __authors__ = [] 28 | __copyright__ = """Copyright (C) 2016 29 | This program comes with ABSOLUTELY NO WARRANTY. 30 | This is free software, and you are welcome to redistribute it 31 | under certain conditions. 32 | For details see the COPYRIGHT file distributed along this program.""" 33 | 34 | __license__ = """ 35 | This package is free software; you can redistribute it and/or modify 36 | it under the terms of the GNU General Public License as published by 37 | the Free Software Foundation; either version 3 of the License, or 38 | any later version. 39 | 40 | This package is distributed in the hope that it will be useful, 41 | but WITHOUT ANY WARRANTY; without even the implied warranty of 42 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 43 | GNU General Public License for more details. 44 | 45 | You should have received a copy of the GNU General Public License 46 | along with this package. If not, see . 47 | """ 48 | __all__ = ('clientscript', 'conflog', 'dhcpcapfsm', 'dhcpcaplease', 49 | 'dhcpcaputils', 'timers', 'constants', 'dhcpcap') 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to `dhcpcanon` 2 | =========================== 3 | 4 | We welcome contributions of any kind (ideas, code, tests, documentation, 5 | examples, ...). 6 | 7 | General contribution guidelines 8 | ------------------------------- 9 | 10 | - Any non-trivial change should contain tests. 11 | - All the functions and methods should contain Sphinx docstrings which 12 | are used to generate the API documentation. 13 | 14 | Code style guide 15 | ---------------- 16 | 17 | - We follow [PEP8 Python Style 18 | Guide](http://www.python.org/dev/peps/pep-0008/) 19 | - Use 4 spaces for a tab 20 | - Use 79 characters in a line 21 | - Make sure edited file doesn't contain any trailing whitespace 22 | - You can verify that your modifications don't break any rules by 23 | running the `flake8` script - e.g. `flake8 dhcpcanon/edited_file.py` 24 | or `tox -e style`. Second command will run flake8 on all the files 25 | in the repository. 26 | 27 | And most importantly, follow the existing style in the file you are 28 | editing and **be consistent**. 29 | 30 | Docstring conventions 31 | --------------------- 32 | 33 | For documenting the API we we use Sphinx and reStructuredText syntax. 34 | 35 | Contribution workflow 36 | --------------------- 37 | 38 | ### 1. Open a new issue on our issue tracker 39 | 40 | Go to our [issue tracker](https://github.com/juga0/dhcpcanon/issues) and 41 | open a new issue for your changes there. 42 | 43 | ### 2. Fork our Github repository 44 | 45 | Fork our [Github git repository](https://github.com/juga0/dhcpcanon). 46 | Your fork will be used to hold your changes. 47 | 48 | ### 3. Create a new branch for your changes 49 | 50 | For example: 51 | 52 | ### 4. Make your changes 53 | 54 | Commit often and rebase master 55 | 56 | ### 5. Write tests for your changes and make sure all the tests pass 57 | 58 | Make sure that all the code you have added or modified has appropriate 59 | test coverage. Also make sure all the tests including the existing ones 60 | still pass using `tox` 61 | 62 | ### 6. Open a Pull request 63 | 64 | You can then push your feature branch to your remote and open a pull 65 | request. 66 | 67 | > **note** 68 | > 69 | > Partly copied from [libcloud 70 | > contributing](https://libcloud.readthedocs.io/en/latest/development.html#contributing) 71 | -------------------------------------------------------------------------------- /dhcpcanon/conflog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Logging configuration.""" 5 | import logging 6 | import sys 7 | 8 | LOGGING = { 9 | 'version': 1, 10 | 'disable_existing_loggers': True, 11 | 'formatters': { 12 | 'verbose': { 13 | 'format': '%(asctime)s %(levelname)s' 14 | ' %(filename)s:%(lineno)s -' 15 | ' %(funcName)s - %(message)s', 16 | 'datefmt': '%Y-%m-%d %H:%M:%S' 17 | }, 18 | 'simple': { 19 | 'format': "%(message)s", 20 | }, 21 | 'sys': { 22 | 'format': "%(module)s[%(process)s]: " 23 | "%(message)s" 24 | } 25 | }, 26 | 'handlers': { 27 | 'stdout': { 28 | 'class': 'logging.StreamHandler', 29 | 'stream': sys.stdout, 30 | 'formatter': 'verbose', 31 | 'level': 'DEBUG', 32 | }, 33 | 'stdoutscapy': { 34 | 'class': 'logging.StreamHandler', 35 | 'stream': sys.stdout, 36 | 'formatter': 'simple', 37 | 'level': 'DEBUG', 38 | }, 39 | 'syslog': { 40 | 'class': 'logging.handlers.SysLogHandler', 41 | 'address': '/dev/log', 42 | 'formatter': 'sys', 43 | 'level': 'INFO', 44 | }, 45 | }, 46 | 'loggers': { 47 | 'dhcpcanon': { 48 | 'handlers': ['syslog', 'stdout'], 49 | 'level': logging.INFO, 50 | 'propagate': False 51 | }, 52 | "scapy": { 53 | 'handlers': ['stdoutscapy'], 54 | 'level': logging.DEBUG, 55 | 'propagate': False 56 | }, 57 | # next ones set to ERROR to disable 58 | # WARNING: Failed to execute tcpdump. 59 | "scapy.interactive": { 60 | 'handlers': ['stdoutscapy'], 61 | 'level': logging.ERROR, 62 | 'propagate': False 63 | }, 64 | "scapy.runtime": { 65 | 'handlers': ['stdoutscapy'], 66 | 'level': logging.ERROR, 67 | 'propagate': False 68 | }, 69 | "scapy.loading": { 70 | 'handlers': ['stdoutscapy'], 71 | 'level': logging.ERROR, 72 | 'propagate': False 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/source/questions.rst: -------------------------------------------------------------------------------- 1 | .. _questions: 2 | 3 | Summary of questions regarding the RFCs and the implementations 4 | =============================================================== 5 | 6 | This is a summary of the questions stated in `RFC7844 DHCPv4 restricted version summary `_ 7 | 8 | Message Options 9 | ----------------- 10 | 11 | Requested IP Address Option (code 50) 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | [:rfc:`7844#section-3.3`] 14 | 15 | - Is there a way to know ``if`` the link-layer address changed without leaking the link-layer? 16 | 17 | 18 | Not specified in RFC7844, but in RFC2131 19 | ----------------------------------------- 20 | 21 | Probe the offered IP 22 | ~~~~~~~~~~~~~~~~~~~~~ 23 | [:rfc:`2131#section-2.2`] 24 | 25 | - does any implementation issue an ARP request to probe the offered address? 26 | - is it issued after DHCPOFFER and before DHCPREQUEST, 27 | or after DHCPACK and before passing to BOUND state? 28 | 29 | Retransmission delays 30 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 31 | 32 | Sending DHCPDISCOVER [:rfc:`2131#section-4.4.1`] 33 | 34 | - is the DISCOVER retranmitted in the same way as the REQUEST 35 | 36 | [:rfc:`2131#section-3.1`], [:rfc:`2131#section-4.4.5`], [:rfc:`2131#section-4.1`] 37 | 38 | - the delay for the next retransmission is calculated with respect to the type 39 | of DHCP message or for the total of DHCP messages sent indendent of the type? 40 | - without this algorithm being mandatory, **it'd be possible to fingerprint the 41 | the implementation depending on the delay of the retransmission** 42 | - how does other implementations do? 43 | 44 | Selecting offer algorithm 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | [:rfc:`2131#section-4.2`] 47 | 48 | - what is a "no acceptable offer"? 49 | - which are the "strategies" to select OFFER implemented? 50 | - how many offers to wait for? 51 | - different algorithms to select an OFFER **could fingerprint the implementation** 52 | 53 | [:rfc:`2131#section-4.4.1`] 54 | 55 | - Is it different the retransmission delays waiting for offer or ack/nak?, 56 | in all states? 57 | 58 | Timers 59 | ~~~~~~~ 60 | [:rfc:`2131#section-4.4.5`] 61 | 62 | - what's the fixed value for the fuzz and how is it calculated? 63 | - The "fuzz" range is not specified, the fuzz chosen **could fingerprint** the 64 | implementation. 65 | 66 | 67 | Leases 68 | ~~~~~~~ 69 | 70 | [:rfc:`7844#section-3.3`] 71 | 72 | - is there a way to know if the network the client is connected to is the same to which it was connected previously? 73 | 74 | Not specified in any RFC 75 | ------------------------- 76 | 77 | - is it needed to check that the ACK options match with the OFFER ones? 78 | - is it needed to check that all options make sense?, which ones? 79 | -------------------------------------------------------------------------------- /docs/source/implementation.rst: -------------------------------------------------------------------------------- 1 | .. _implementation: 2 | 3 | Message types and options details in all layers 4 | ------------------------------------------------ 5 | 6 | DHCPDISCOVER 7 | ~~~~~~~~~~~~~ 8 | 9 | Always broadcast in AP:: 10 | 11 | Ehter: src=client_mac, dst="ff:ff:ff:ff:ff:ff" 12 | IP: src="0.0.0.0", dst="255.255.255.255" 13 | UDP: sport=68, dport=67 14 | BOOTP: Client Hardware address (chaddr in scapy) 15 | DHCP: Message Type option (message-type in scapy) 16 | 17 | DHCPREQUEST 18 | ~~~~~~~~~~~~~ 19 | 20 | In SELECTING state: Broadcast in AP:: 21 | 22 | Ehter: src=client_mac, dst="ff:ff:ff:ff:ff:ff" 23 | IP: src="0.0.0.0", dst="255.255.255.255" 24 | UDP: sport=68, dport=67 25 | BOOTP: Client Hardware address (chaddr in scapy) 26 | DHCP: Message Type option (message-type in scapy) 27 | DHCP: Server Identifier option (server_id in scapy, siaddr in server BOOTP offer) 28 | DHCP: Requested IP option (requested_addr in scapy, yiaddr in server BOOTP offer) 29 | 30 | In RENEWING state: Unicast to server id:: 31 | 32 | Ehter: src=client_mac, dst=server_mac 33 | IP: src=client_ip, dst=server_ip 34 | UDP: sport=68, dport=67 35 | BOOTP: Client Hardware address (chaddr in scapy) 36 | DHCP: Message Type option (message-type in scapy) 37 | Client IP address (ciaddr=client_ip)? 38 | 39 | In REBINDING state: broadcast:: 40 | 41 | Ehter: src=client_mac, dst="ff:ff:ff:ff:ff:ff" 42 | IP: src="0.0.0.0", dst="255.255.255.255" 43 | UDP: sport=68, dport=67 44 | BOOTP: Client Hardware address (chaddr in scapy) 45 | DHCP: Message Type option (message-type in scapy) 46 | Client IP address (ciaddr=client_ip)? 47 | 48 | 49 | DHCPDECLINE 50 | ~~~~~~~~~~~~~ 51 | Always broadcast?:: 52 | 53 | Ehter: src=client_mac, dst="ff:ff:ff:ff:ff:ff" 54 | IP: src="0.0.0.0", dst="255.255.255.255" 55 | UDP: sport=68, dport=67 56 | BOOTP: Client Hardware address (chaddr in scapy) 57 | DHCP: Message Type option (message-type in scapy) 58 | DHCP: Server Identifier option (server_id in scapy, siaddr in server BOOTP offer) 59 | DHCP: Requested IP option (requested_addr in scapy, yiaddr in server BOOTP offer) 60 | 61 | DHCPRELEASE 62 | ~~~~~~~~~~~~~ 63 | 64 | Always unicast, is not being used:: 65 | 66 | Ehter: src=client_mac, dst=server_mac 67 | IP: src=client_ip, dst=server_ip 68 | UDP: sport=68, dport=67 69 | BOOTP: Client Hardware address (chaddr in scapy) 70 | DHCP: Message Type option (message-type in scapy) 71 | DHCP: Server Identifier option (server_id in scapy, siaddr in server BOOTP offer) 72 | 73 | DHCPINFORM 74 | ~~~~~~~~~~~~~ 75 | 76 | Always broadcast in Anonymity Profile, is not being used:: 77 | 78 | Ehter: src=client_mac, dst="ff:ff:ff:ff:ff:ff" 79 | IP: src=client_ip, dst="255.255.255.255" 80 | UDP: sport=68, dport=67 81 | BOOTP: Client Hardware address (chaddr in scapy) 82 | BOOTP: Client IP address (ciaddr=client_ip) 83 | DHCP: Message Type option (message-type in scapy) 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | Contributing to ``dhcpcanon`` 4 | ============================== 5 | 6 | We welcome contributions of any kind (ideas, code, tests, documentation, examples, ...). 7 | 8 | General contribution guidelines 9 | ------------------------------- 10 | 11 | * Any non-trivial change should contain tests. 12 | * All the functions and methods should contain Sphinx docstrings which are used 13 | to generate the API documentation. 14 | 15 | Code style guide 16 | ----------------- 17 | 18 | * We follow `PEP8 Python Style Guide`_ 19 | * Use 4 spaces for a tab 20 | * Use 79 characters in a line 21 | * Make sure edited file doesn't contain any trailing whitespace 22 | * You can verify that your modifications don't break any rules by running the 23 | ``flake8`` script - e.g. ``flake8 dhcpcanon/edited_file.py`` or 24 | ``tox -e style``. 25 | Second command will run flake8 on all the files in the repository. 26 | 27 | And most importantly, follow the existing style in the file you are editing and 28 | **be consistent**. 29 | 30 | Docstring conventions 31 | --------------------- 32 | 33 | For documenting the API we we use Sphinx and reStructuredText syntax. 34 | 35 | 36 | Contribution workflow 37 | --------------------- 38 | 39 | 1. Open a new issue on our issue tracker 40 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | 42 | Go to our `issue tracker`_ and open a new issue for your changes there. 43 | 44 | 2. Fork our Github repository 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | Fork our `Github git repository`_. Your fork will be used to hold your changes. 48 | 49 | 3. Create a new branch for your changes 50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | 52 | For example: 53 | 54 | .. sourcecode:: bash 55 | 56 | git checkout -b 57 | 58 | 4. Make your changes 59 | ~~~~~~~~~~~~~~~~~~~~ 60 | 61 | Commit often and rebase master 62 | 63 | 5. Write tests for your changes and make sure all the tests pass 64 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | 66 | Make sure that all the code you have added or modified has appropriate test 67 | coverage. Also make sure all the tests including the existing ones still pass 68 | using ``tox`` 69 | 70 | .. sourcecode:: bash 71 | 72 | tox 73 | 74 | 6. Open a Pull request 75 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 76 | 77 | You can then push your feature branch to your remote and open a pull request. 78 | 79 | Reporting a Vulnerability 80 | ---------------------------- 81 | 82 | Please do not report security issues using the public `Issue tracker`_. 83 | Send a description of it to juga at riseup dot net. 84 | You are also encouraged to encrypt this email using GPG. 85 | The key can be found in the public servers. 86 | 87 | This docummentation is partly copied from `libcloud contributing`_ 88 | 89 | .. _`PEP8 Python Style Guide`: http://www.python.org/dev/peps/pep-0008/ 90 | .. _`Issue tracker`: https://github.com/juga0/dhcpcanon/issues 91 | .. _`Github git repository`: https://github.com/juga0/dhcpcanon 92 | .. _`libcloud contributing`: https://libcloud.readthedocs.io/en/latest/development.html#contributing 93 | -------------------------------------------------------------------------------- /dhcpcanon/clientscript.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Class to Initialize and call external script.""" 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import logging 8 | import os 9 | import subprocess 10 | 11 | import attr 12 | 13 | from .constants import (ENV_OPTIONS_REQ, LEASEATTRS2ENVKEYS, 14 | LEASEATTRS_SAMEAS_ENVKEYS, SCRIPT_ENV_KEYS, 15 | SCRIPT_PATH, STATES2REASONS) 16 | 17 | logger = logging.getLogger('dhcpcanon') 18 | 19 | 20 | @attr.s 21 | class ClientScript(object): 22 | """Simulates the behaviour of the isc-dhcp client-script or nm-dhcp-helper. 23 | 24 | `client-script 25 | `_ 26 | or `nm-dhcp-helper 27 | `_. 28 | 29 | """ 30 | 31 | scriptname = attr.ib(default=None) 32 | env = attr.ib(default=attr.Factory(dict)) 33 | 34 | def __attrs_post_init__(self, scriptfile=None, env=None): 35 | """.""" 36 | logger.debug('Modifying ClientScript obj after creating it.') 37 | self.scriptname = self.scriptname or scriptfile or SCRIPT_PATH 38 | if env is None: 39 | self.env = dict.fromkeys(SCRIPT_ENV_KEYS, str('')) 40 | else: 41 | self.env = env 42 | self.env['medium'] = str() 43 | self.env['pid'] = str(os.getpid()) 44 | 45 | def script_init(self, lease, state, prefix='', medium=''): 46 | """Initialize environment to pass to the external script.""" 47 | logger.debug('self.scriptname %s', self.scriptname) 48 | if self.scriptname is not None: 49 | logger.debug('Modifying ClientScript obj, setting env.') 50 | if isinstance(state, int): 51 | reason = STATES2REASONS[state] 52 | else: 53 | reason = state 54 | self.env['reason'] = str(reason) 55 | self.env['medium'] = self.env.get('medium') or str(medium) 56 | self.env['client'] = str('dhcpcanon') 57 | self.env['pid'] = str(os.getpid()) 58 | for k in LEASEATTRS_SAMEAS_ENVKEYS: 59 | self.env[k] = str(lease.__getattribute__(k)) 60 | for k, v in LEASEATTRS2ENVKEYS.items(): 61 | self.env[v] = str(lease.__getattribute__(k)) 62 | self.env.update(ENV_OPTIONS_REQ) 63 | else: 64 | logger.debug('There is not script path.') 65 | 66 | def script_go(self, scriptname=None, env=None): 67 | """Run the external script.""" 68 | scriptname = self.scriptname or scriptname 69 | if scriptname is not None: 70 | env = self.env or env 71 | logger.info('Calling script %s', scriptname) 72 | logger.info('with env %s', env) 73 | proc = subprocess.Popen([scriptname], env=env, 74 | stderr=subprocess.STDOUT) 75 | try: 76 | (stdout, stderr) = proc.communicate() 77 | return True 78 | except TypeError as e: 79 | logger.error(e) 80 | return False 81 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim:ts=4:sw=4:expandtab 4 | # SPDX-FileCopyrightText: 2016, juga 5 | # SPDX-License-Identifier: MIT 6 | 7 | """Setup.""" 8 | import subprocess 9 | 10 | from setuptools import find_packages, setup 11 | 12 | import dhcpcanon 13 | 14 | 15 | def systemd_unit_dir(): 16 | cmd = ['pkg-config', '--variable', 'systemdsystemunitdir', 'systemd'] 17 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, 18 | stderr=subprocess.PIPE) 19 | (stdout, stderr) = proc.communicate() 20 | if proc.returncode != 0 or stdout is None: 21 | return None # systemd not found 22 | if isinstance(stdout.strip(), bytes): 23 | return stdout.strip().decode('utf-8') 24 | return stdout.strip() 25 | 26 | 27 | def systemd_tmpfiles_dir(): 28 | # There doesn't seem to be a specific pkg-config variable for this 29 | cmd = ['pkg-config', '--variable', 'prefix', 'systemd'] 30 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, 31 | stderr=subprocess.PIPE) 32 | (stdout, stderr) = proc.communicate() 33 | if proc.returncode != 0 or stdout is None: 34 | return None # systemd not found 35 | d = stdout.strip() 36 | if isinstance(d, bytes): 37 | d = d.decode('utf-8') 38 | return d + '/lib/tmpfiles.d' 39 | 40 | 41 | if systemd_unit_dir(): 42 | data_files = [ 43 | (systemd_unit_dir(), ['systemd/dhcpcanon.service']), 44 | (systemd_tmpfiles_dir(), ['tmpfiles.d/dhcpcanon.conf']), 45 | ('sbin', ["sbin/dhcpcanon-script"]) 46 | ] 47 | else: 48 | data_files = [] 49 | 50 | test_requirements = ['coverage', 'tox', 'pytest'] 51 | 52 | setup( 53 | name='dhcpcanon', 54 | version=dhcpcanon.__version__, 55 | description=dhcpcanon.__description__, 56 | long_description=dhcpcanon.__long_description__, 57 | author=dhcpcanon.__author__, 58 | author_email=dhcpcanon.__author_mail__, 59 | license='MIT', 60 | url=dhcpcanon.__website__, 61 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 62 | install_requires=[ 63 | "attrs>=16.3", 64 | "dbus-python>=1.2", 65 | "netaddr>=0.7", 66 | "lockfile>=0.12", 67 | "pip>=8.1", 68 | "pyroute2>=0.3", 69 | 'scapy-python3>=0.20', 70 | ], 71 | python_requires=">=3.5", 72 | extras_require={ 73 | 'dev': ['flake8'], 74 | 'test': test_requirements, 75 | 'doc': ['sphinx', 'sphinx-bootstrap-theme', 'pylint'] 76 | }, 77 | tests_require=test_requirements, 78 | entry_points={ 79 | 'console_scripts': [ 80 | 'dhcpcanon = dhcpcanon.dhcpcanon:main', 81 | ] 82 | }, 83 | # NOTE: not installing system files as the user might want to install them 84 | # in a custom prefix or without systemd 85 | # data_files=data_files, 86 | zip_safe=False, 87 | include_package_data=True, 88 | keywords='python scapy dhcp RFC7844 RFC2131 anonymity', 89 | classifiers=[ 90 | 'Development Status :: 3 - Alpha', 91 | "Environment :: Console", 92 | 'Intended Audience :: End Users/Desktop', 93 | 'Intended Audience :: Developers', 94 | 'License :: OSI Approved :: MIT License', 95 | 'Operating System :: OS Independent', 96 | 'Programming Language :: Python :: 3', 97 | 'Topic :: System :: Networking', 98 | ], 99 | ) 100 | -------------------------------------------------------------------------------- /docs/source/soa.rst: -------------------------------------------------------------------------------- 1 | .. _soa: 2 | 3 | ================== 4 | State of the Art 5 | ================== 6 | 7 | on DHCP clients, network managers and libraries in Debian/Ubuntu 8 | 9 | ISC-DHCP 10 | ----------- 11 | 12 | Reference ISC implementation 13 | `ISC License `__ 14 | 15 | `homepage `__ 16 | `tar.gz `__ 17 | 18 | 19 | Debian DHCP clients 20 | ====================== 21 | 22 | isc-dhcp-client 23 | ------------------ 24 | 25 | Debian default 26 | 27 | `debian `__ 28 | `debian source `__ 29 | 30 | network-manager built-in 31 | -------------------------- 32 | 33 | 34 | systemd-networkd 35 | -------------------- 36 | 37 | ``man 5 systemd.network`` => DHCP options 38 | 39 | udhcpc 40 | ----------- 41 | 42 | Busybox implementation 43 | 44 | `debian `__ 45 | 46 | Debian network managers 47 | ======================== 48 | 49 | Gnome Network Manager 50 | ------------------------ 51 | 52 | Can use 3 DHCP clients: 53 | - ISC DHCP client: package `isc-dhpc-client`, binarry `dhclient` 54 | - systemd DHCP client 55 | - built-in DHCP client 56 | 57 | `debian `__ 58 | 59 | wicd 60 | ----- 61 | 62 | `debian `__ 63 | 64 | 65 | 66 | Python DHCP libraries/tools 67 | =============================== 68 | 69 | python-isc-dhcp-leases 70 | -------------------------- 71 | 72 | Python module for reading dhcp leases files 73 | 74 | `debian `__ 75 | 76 | pydhcplib 77 | ------------------- 78 | 79 | Pure Python library. 80 | 81 | GPL. Last updated XX. Commiters: 1. 82 | 83 | `pypi `__, 84 | `repo `__, 85 | `wiki `__ 86 | `debian `__ 87 | 88 | pydhcpd 89 | ----------- 90 | 91 | DHCP command-line query and testing tool. Uses pydhcplib 92 | 93 | GPL. Last updated: 2009 94 | 95 | `code `__ 96 | 97 | staticdhcpd 98 | ---------------- 99 | 100 | is an all-Python, RFC 2131-compliant DHCP server, 101 | with support for most common DHCP extensions and 102 | extensive site-specific customisation. 103 | 104 | GPL. Last updated 12/03/2017. Commiters: +3 105 | 106 | `repo `__ 107 | 108 | dhquery 109 | ---------- 110 | 111 | DHCP command line query and testing tool 112 | 113 | `code `__ 114 | `one github fork `__ (updated 2016) 115 | 116 | dhcpy6d 117 | ------------ 118 | 119 | MAC address aware DHCPv6 server written in Python 120 | 121 | Last updated 28/06/2017. Commiters: 2? 122 | 123 | `homepage `__ 124 | `repo `__ 125 | `doc `__ 126 | `debian `__ 127 | 128 | dhcpscapy 129 | ----------- 130 | 131 | Simple DCHP client and server implemented with scapy 132 | 133 | Last updated. 18/05/2014. Commiters: 1 134 | 135 | `repo `__ 136 | -------------------------------------------------------------------------------- /dhcpcanon/dhcpcanon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:ts=4:sw=4:expandtab 2 4 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 5 | """DCHP client implementation of the anonymity profile (RFC7844).""" 6 | 7 | import argparse 8 | import logging 9 | import logging.config 10 | 11 | from lockfile.pidlockfile import (PIDLockFile, AlreadyLocked, 12 | LockTimeout, LockFailed) 13 | from scapy.config import conf 14 | 15 | # in python3 this seems to be the only way to to disable: 16 | # WARNING: Failed to execute tcpdump. 17 | conf.logLevel = logging.ERROR 18 | 19 | from . import __version__ 20 | from .conflog import LOGGING 21 | from .constants import (CLIENT_PORT, SERVER_PORT, SCRIPT_PATH, PID_PATH) 22 | from .dhcpcapfsm import DHCPCAPFSM 23 | 24 | logging.config.dictConfig(LOGGING) 25 | logger = logging.getLogger('dhcpcanon') 26 | 27 | 28 | def main(): 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument('interface', nargs='?', 31 | help='interface to configure with DHCP') 32 | parser.add_argument('-v', '--verbose', 33 | help='Set logging level to debug', 34 | action='store_true') 35 | parser.add_argument('--version', action='version', 36 | help='version', 37 | version='%(prog)s ' + __version__) 38 | parser.add_argument('-s', '--delay_selecting', 39 | help='Selecting starts after a ramdon delay.', 40 | action='store_true') 41 | # options to looks like dhclient 42 | parser.add_argument( 43 | '-sf', metavar='script-file', nargs='?', 44 | const=SCRIPT_PATH, 45 | help='Path to the network configuration script invoked by ' 46 | 'dhcpcanon when it gets a lease. Without this option ' 47 | 'dhcpcanon will configure the network by itself.' 48 | 'If unspecified, the ' 49 | 'default /sbin/dhcpcanon-script is used, which is a copy of' 50 | 'dhclient-script(8) for a description of this file.' 51 | 'If dhcpcanon is running with NetworkManager, it will' 52 | 'be called with the script nm-dhcp-helper.') 53 | parser.add_argument( 54 | '-pf', metavar='pid-file', nargs='?', 55 | const=PID_PATH, 56 | help='Path to the process ID file. If unspecified, the' 57 | 'default /var/run/dhcpcanon.pid is used. ' 58 | 'This option is used by NetworkManager to check whether ' 59 | 'dhcpcanon is already running.') 60 | args = parser.parse_args() 61 | logger.debug('args %s', args) 62 | 63 | # do not put interfaces in promiscuous mode 64 | conf.sniff_promisc = conf.promisc = 0 65 | conf.checkIPaddr = 1 66 | 67 | if args.verbose: 68 | logger.setLevel(logging.DEBUG) 69 | logger.debug('args %s', args) 70 | if args.interface: 71 | conf.iface = args.interface 72 | logger.debug('interface %s' % conf.iface) 73 | if args.pf is not None: 74 | # This is only needed for nm 75 | pf = PIDLockFile(args.pf, timeout=5) 76 | try: 77 | pf.acquire() 78 | logger.debug('using pid file %s', pf) 79 | except AlreadyLocked as e: 80 | pf.break_lock() 81 | pf.acquire() 82 | except (LockTimeout, LockFailed) as e: 83 | logger.error(e) 84 | dhcpcap = DHCPCAPFSM(iface=conf.iface, 85 | server_port=SERVER_PORT, 86 | client_port=CLIENT_PORT, 87 | scriptfile=args.sf, 88 | delay_selecting=args.delay_selecting) 89 | dhcpcap.run() 90 | 91 | 92 | if __name__ == '__main__': 93 | main() 94 | -------------------------------------------------------------------------------- /apparmor.d/sbin.dhcpcanon: -------------------------------------------------------------------------------- 1 | # vim:syntax=apparmor 2 | # Last Modified: Fri Jul 17 11:46:19 2009 3 | # SPDX-FileCopyrightText: 2009, Jamie Strandboge 4 | # SPDX-FileCopyrightText: 2017, juga 5 | # SPDX-License-Identifier: MIT 6 | # Modified for dhcpcanon by: juga , 2017. 7 | #include 8 | 9 | /sbin/dhcpcanon flags=(attach_disconnected) { 10 | #include 11 | #include 12 | #include 13 | 14 | capability net_bind_service, 15 | capability net_raw, 16 | capability sys_module, 17 | capability dac_override, 18 | capability net_admin, 19 | 20 | network packet, 21 | network raw, 22 | 23 | @{PROC}/[0-9]*/net/ r, 24 | @{PROC}/[0-9]*/net/** r, 25 | 26 | /sbin/dhcpcanon mr, 27 | # LP: #1197484 and LP: #1202203 - why is this needed? :( 28 | /bin/bash mr, 29 | 30 | /etc/dhcpcanon.conf r, 31 | /etc/dhcp/ r, 32 | /etc/dhcp/** r, 33 | 34 | /var/lib/dhcp{,3}/dhcpcanon* lrw, 35 | /{,var/}run/dhcpcanon*.pid lrw, 36 | /{,var/}run/dhcpcanon*.lease* lrw, 37 | 38 | # NetworkManager 39 | /{,var/}run/nm*conf r, 40 | /{,var/}run/sendsigs.omit.d/network-manager.dhcpcanon*.pid lrw, 41 | /var/lib/NetworkManager/dhcpcanon*.conf lrw, 42 | /var/lib/NetworkManager/dhcpcanon*.lease* lrw, 43 | signal (receive) peer=/usr/sbin/NetworkManager, 44 | ptrace (readby) peer=/usr/sbin/NetworkManager, 45 | 46 | # connman 47 | /{,var/}run/connman/dhcpcanon*.pid lrw, 48 | /{,var/}run/connman/dhcpcanon*.leases lrw, 49 | 50 | # synce-hal 51 | /usr/share/synce-hal/dhcpcanon.conf r, 52 | 53 | # if there is a custom script, let it run unconfined 54 | /etc/dhcp/dhcpcanon-script Uxr, 55 | 56 | # The dhcpcanon-script shell script sources other shell scripts rather than 57 | # executing them, so we can't just use a separate profile for dhcpcanon-script 58 | # with 'Uxr' on the hook scripts. However, for the long-running dhcpcanon3 59 | # daemon to run arbitrary code via /sbin/dhcpcanon-script, it would need to be 60 | # able to subvert dhcpcanon-script or write to the hooks.d directories. As 61 | # such, if the dhcpcanon3 daemon is subverted, this effectively limits it to 62 | # only being able to run the hooks scripts. 63 | /sbin/dhcpcanon-script Uxr, 64 | 65 | # Run the ELF executables under their own unrestricted profiles 66 | /usr/lib/NetworkManager/nm-dhcp-client.action Pxrm, 67 | /usr/lib/connman/scripts/dhcpcanon-script Pxrm, 68 | 69 | # Support the new executable helper from NetworkManager. 70 | /usr/lib/NetworkManager/nm-dhcp-helper Pxrm, 71 | signal (receive) peer=/usr/lib/NetworkManager/nm-dhcp-helper, 72 | 73 | # Site-specific additions and overrides. See local/README for details. 74 | #include 75 | } 76 | 77 | /usr/lib/NetworkManager/nm-dhcp-client.action { 78 | #include 79 | #include 80 | /usr/lib/NetworkManager/nm-dhcp-client.action mr, 81 | 82 | /var/lib/NetworkManager/*lease r, 83 | signal (receive) peer=/usr/sbin/NetworkManager, 84 | ptrace (readby) peer=/usr/sbin/NetworkManager, 85 | network inet dgram, 86 | network inet6 dgram, 87 | } 88 | 89 | /usr/lib/NetworkManager/nm-dhcp-helper { 90 | #include 91 | #include 92 | /usr/lib/NetworkManager/nm-dhcp-helper mr, 93 | 94 | /run/NetworkManager/private-dhcp rw, 95 | signal (send) peer=/sbin/dhcpcanon, 96 | 97 | /var/lib/NetworkManager/*lease r, 98 | signal (receive) peer=/usr/sbin/NetworkManager, 99 | ptrace (readby) peer=/usr/sbin/NetworkManager, 100 | network inet dgram, 101 | network inet6 dgram, 102 | } 103 | 104 | /usr/lib/connman/scripts/dhcpcanon-script { 105 | #include 106 | #include 107 | /usr/lib/connman/scripts/dhcpcanon-script mr, 108 | network inet dgram, 109 | network inet6 dgram, 110 | } 111 | -------------------------------------------------------------------------------- /dhcpcanon/dhcpcaplease.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Lease class for the DHCP client implementation of the Anonymity Profile 5 | ([:rfc:`7844`])..""" 6 | from __future__ import absolute_import 7 | 8 | import logging 9 | 10 | import attr 11 | from attr.validators import instance_of 12 | 13 | from .timers import (future_dt_str, gen_rebinding_time, gen_renewing_time, 14 | nowutc) 15 | 16 | from.constants import (LEASE_ATTRS2LEASE_LOG, ENV_OPTIONS_REQ) 17 | 18 | logger = logging.getLogger('dhcpcanon') 19 | 20 | 21 | @attr.s 22 | class DHCPCAPLease(object): 23 | """.""" 24 | address = attr.ib(default='', validator=instance_of(str)) 25 | server_id = attr.ib(default='', validator=instance_of(str)) 26 | next_server = attr.ib(default='', validator=instance_of(str)) 27 | router = attr.ib(default='', validator=instance_of(str)) 28 | subnet_mask = attr.ib(default='', validator=instance_of(str)) 29 | broadcast_address = attr.ib(default='', validator=instance_of(str)) 30 | domain = attr.ib(default='', validator=instance_of(str)) 31 | name_server = attr.ib(default='', validator=instance_of(str)) 32 | subnet = attr.ib(default='', validator=instance_of(str)) 33 | lease_time = attr.ib(default='', validator=instance_of(str)) 34 | renewal_time = attr.ib(default='', validator=instance_of(str)) 35 | rebinding_time = attr.ib(default='', validator=instance_of(str)) 36 | # not given by the server 37 | interface = attr.ib(default='', validator=instance_of(str)) 38 | # not given by the server, calculated on previous 39 | subnet_mask_cidr = attr.ib(default='', validator=instance_of(str)) 40 | network = attr.ib(default='', validator=instance_of(str)) 41 | expiry = attr.ib(default='', validator=instance_of(str)) 42 | renew = attr.ib(default='', validator=instance_of(str)) 43 | rebind = attr.ib(default='', validator=instance_of(str)) 44 | 45 | # def __attrs_post_init__(self, sent_dt): 46 | # """Initializes attributes after attrs __init__.""" 47 | # self.set_times(sent_dt) 48 | 49 | def set_times(self, sent_dt): 50 | """ 51 | Set timers for the lease given the time in which the request was sent. 52 | 53 | [:rfc:`2131#section-4.4.1`]:: 54 | 55 | The client records the lease expiration time 56 | as the sum of the time at which the original request was 57 | sent and the duration of the lease from the DHCPACK message. 58 | 59 | """ 60 | logger.debug('Modifying Lease obj, setting timers.') 61 | elapsed = (nowutc() - sent_dt).seconds 62 | if self.renewal_time == '': 63 | self.renewal_time = gen_renewing_time(self.lease_time, elapsed) 64 | if self.rebinding_time == '': 65 | self.rebinding_time = gen_rebinding_time(self.lease_time, elapsed) 66 | self.expiry = future_dt_str(sent_dt, self.lease_time) 67 | self.renew = future_dt_str(sent_dt, self.renewal_time) 68 | self.rebind = future_dt_str(sent_dt, self.rebinding_time) 69 | logger.debug('lease time: %s, expires on %s', self.lease_time, 70 | self.expiry) 71 | logger.debug('renewal_time: %s, expires on %s', 72 | self.renewal_time, self.renew) 73 | logger.debug('rebinding time: %s, expires on %s', 74 | self.rebinding_time, self.rebind) 75 | 76 | def info_lease(self): 77 | """Print lease information.""" 78 | for k, v in LEASE_ATTRS2LEASE_LOG. items(): 79 | logger.debug("'%s'=>'%s'", v, getattr(self, k)) 80 | for k, v in ENV_OPTIONS_REQ.items(): 81 | logger.debug("option '%s'=>'1'", k) 82 | logger.info('address %s', self.address) 83 | logger.info('plen %s (%s)', self.subnet_mask_cidr, self.subnet_mask) 84 | logger.info('gateway %s', self.router) 85 | logger.info('server identifier %s', self.server_id) 86 | logger.info('nameserver %s', self.name_server) 87 | logger.info('domain name %s', self.domain) 88 | logger.info('lease time %s', self.lease_time) 89 | -------------------------------------------------------------------------------- /docs/source/privileges.rst: -------------------------------------------------------------------------------- 1 | .. _privileges: 2 | 3 | Minimising ``dhcpcanon`` privileges 4 | ==================================== 5 | 6 | Reasons why a DHCP client needs to run with root privileges: 7 | 8 | * open sockets in privilege ports (68) 9 | * open RAW sockets: to receive packets without having an IP set yet 10 | * to set the IP offered 11 | 12 | .. note:: 13 | 14 | ``dhcpcanon`` does not need privileges to set up the IP, as that is done 15 | by a separated script, as ``dhclient`` does. 16 | 17 | Possible solutions to minimise privileges and their associated problems: 18 | 19 | 1. drop privileges after BOUND DHCP state (sockets binded): 20 | * problem: if the client stays connected until the renewing/rebinding time, 21 | privileges would be needed again and dropping privileges `temporally` it is 22 | not recommended []. 23 | * possible solutions: do not implement RENEWING/REBINDING states. 24 | 25 | * problem: this would not be compliant with RFC 2131 nor 7844. 26 | * pro: in "usual" networks, if the client stays enough time 27 | connected to the network, the lease would expire it could just restart in the 28 | INIT state. 29 | 30 | .. todo:: 31 | 32 | which would be the associated problems to this solution? 33 | 34 | 2. wrapper with privileges to set linux network capabilities to the client, 35 | open sockets, then call the client inheriting the sockets: 36 | * problem: same as 1. 37 | 38 | .. note:: 39 | 40 | it's not possible to set net capabilities directly to a python script, 41 | they would need to be set to the python binary, but that would give the 42 | capabilities to any python script. 43 | Python binary could also be copied, set the capabilies, and that script call 44 | the client, but would have the same problem as giving the capabilities to 45 | the original python binary 46 | 47 | 3. ``dhcpcanon`` could call a binary with privileges to create the sockets 48 | every time it needs to do so. 49 | It's needed to change several parts of the current implementation. 50 | 51 | 4. to have the process be granted just the capabilities it needs, 52 | by the system-level process manager. 53 | 54 | This is already implemented with ``systemd`` 55 | 56 | 5. wrapper that does the same as in 4. without a system-level process 57 | manager. See section "wrapper to inherit capabilities" 58 | 59 | It could be solved with `infinity0's wrapper ` running:: 60 | 61 | RUST_BACKTRACE=1 ./target/debug/ambient -c NET_RAW,NET_ADMIN,NET_BIND_SERVICE /usr/bin/python3 -m dhcpcanon.dhcpcanon -v 62 | 63 | 6. wrapper with privileges to disable linux Remote Path (RP) filter, 64 | open sockets, then call the client: 65 | * problems: 66 | 67 | * it still needs root to change the default RP settings 68 | * it would only allow that the DHCP offers are received from other interfaces 69 | [], but still RAW sockets are needed to receive packets in the 70 | same interface that does not have an IP address yet 71 | * same as 1. 72 | 73 | Wrapper to inherit capabilities 74 | -------------------------------- 75 | 76 | With ``capsh``, ``dhcpcanon`` could be launched as another user and 77 | inherit only the required capabilities, in a similar way as 78 | ``systemd.service`` does:: 79 | 80 | capsh --caps=cap_net_raw,cap_net_bind_service,cap_net_admin+epi --keep=1 -- -c "mkdir -p /run/dhcpcanon && cd /run/dhcpcanon && su -c 'exec /sbin/dhcpcanon enp0s25' -s /bin/sh dhcpcanon" 81 | 82 | ``-s`` is needed cause dhcpcanon shell is ``/bin/false`` 83 | 84 | However this does not have capabilities to create the socket. 85 | 86 | To show the capabilities that are actually inherited:: 87 | 88 | capsh --keep=1 --secbits=0x1C --caps=cap_net_raw,cap_net_bind_service,cap_net_admin+epi -- -c "mkdir -p /run/dhcpcanon && cd /run/dhcpcanon && su -c '/sbin/capsh --print' -s /bin/sh dhcpcanon" 89 | 90 | In ``man capsh`` ``--securebits`` is not documented, ``securebits.h`` 91 | has some documentation, but it seems to be needed a newer version of 92 | ``libcap`` as commented in this `post `_ 93 | -------------------------------------------------------------------------------- /tests/dhcpcap_objs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 3 | # SPDX-FileCopyrightText: 2016, juga 4 | # SPDX-License-Identifier: MIT 5 | """.""" 6 | from dhcpcanon.dhcpcaplease import DHCPCAPLease 7 | from dhcpcanon.dhcpcap import DHCPCAP 8 | 9 | # init, before send discover 10 | 11 | # init after sent discover 12 | client_init = DHCPCAP(iface='enp0s25', client_mac='f0:de:f1:b8:9b:db', 13 | client_ip='0.0.0.0', client_port=68, 14 | server_mac='ff:ff:ff:ff:ff:ff', 15 | server_ip='255.255.255.255', server_port=67, 16 | lease=DHCPCAPLease(address='', server_id='', 17 | next_server='', router='', 18 | subnet_mask='', broadcast_address='', 19 | domain='', name_server='', subnet='', 20 | lease_time='', renewal_time='', 21 | rebinding_time='', 22 | interface='enp0s25', 23 | subnet_mask_cidr='', network='', 24 | expiry='', renew='', rebind=''), 25 | event=None) 26 | # selecting after received offer 27 | client_select = DHCPCAP(iface='enp0s25', client_mac='f0:de:f1:b8:9b:db', 28 | client_ip='0.0.0.0', client_port=68, 29 | server_mac='ff:ff:ff:ff:ff:ff', 30 | server_ip='255.255.255.255', server_port=67, 31 | lease=DHCPCAPLease(address='192.168.2.113', 32 | server_id='192.168.2.1', 33 | next_server='192.168.2.1', 34 | router='192.168.2.1', 35 | subnet_mask='255.255.255.0', 36 | broadcast_address='192.168.2.255', 37 | domain='localdomain', 38 | name_server='192.168.2.1 8.8.8.8', 39 | subnet='192.168.2.0', 40 | lease_time='43200', 41 | renewal_time='21600', 42 | rebinding_time='37800', 43 | interface='enp0s25', 44 | subnet_mask_cidr='24', 45 | network='', 46 | expiry='', renew='', rebind=''), 47 | event=None) 48 | 49 | 50 | # in requesting state after received ack 51 | client_request = DHCPCAP(iface='enp0s25', client_mac='f0:de:f1:b8:9b:db', 52 | client_ip='0.0.0.0', client_port=68, 53 | server_mac='1c:74:0d:b2:e5:10', 54 | server_ip='192.168.2.1', server_port=67, 55 | lease=DHCPCAPLease(address='192.168.2.113', 56 | server_id='192.168.2.1', 57 | next_server='192.168.2.1', 58 | router='192.168.2.1', 59 | subnet_mask='255.255.255.0', 60 | broadcast_address='192.168.2.255', 61 | domain='localdomain', 62 | name_server='192.168.2.1 8.8.8.8', 63 | subnet='192.168.2.0', 64 | lease_time='43200', 65 | renewal_time='21600', 66 | rebinding_time='37800', 67 | interface='enp0s25', 68 | subnet_mask_cidr='24', 69 | network='', 70 | expiry='17-06-15 23:00:32', 71 | renew='17-06-15 17:00:32', 72 | rebind='17-06-15 21:30:32'), 73 | event=4) 74 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Install dhcpcanon 4 | ================= 5 | 6 | The recommended way to install ``dhcpcanon`` is with your package source 7 | distribution, as it will also install other system files. 8 | 9 | Currently is availabe for Debian unstable/testing. 10 | It can be installed with a package manager or in command line:: 11 | 12 | sudo apt install dhcpcanon 13 | 14 | The main script will be installed in ``/sbin/dhcpcanon``, a systemd service 15 | will be enabled and run by default, so there is no need to run anything manually. 16 | 17 | **Important**: when running ``dhcpcanon`` the hardware address 18 | (`MAC `__) should be randomized. 19 | You can use `macchanger `__, 20 | `macouflage `__ or other. 21 | 22 | Installation from source code in Debian/Ubuntu 23 | =============================================== 24 | 25 | In case you would like to have a newer version or it is not packaged for your 26 | distribution, you can install it from the source code. 27 | 28 | Install system dependencies, in Debian/Ubuntu:: 29 | 30 | sudo apt install python3-dev 31 | 32 | Obtain the source code:: 33 | 34 | git clone https://github.com/juga0/dhcpcanon/ 35 | 36 | Install ``dhcpcanon`` and system files:: 37 | 38 | sudo ./install.sh 39 | 40 | 41 | for advanced users 42 | -------------------- 43 | 44 | Follow the two first steps in the previous paragraph. 45 | 46 | To install ``dhcpcanon`` and the ``systemd`` service:: 47 | 48 | sudo make install WITH_SYSTEMD=true 49 | 50 | In Debian this will install all the required files under ``/usr/local``. 51 | ``WITH_SYSTEMD`` will install a systemd service and enable it, to run it:: 52 | 53 | systemctl start dhcpcanon 54 | 55 | It's possible to also install support for udev:: 56 | 57 | sudo apt install sudo make install WITH_SYSTEMD=true 58 | sudo make install WITH_SYSTEMD=true WITH_SYSTEMD_UDEV=true 59 | 60 | And apparmor profile:: 61 | 62 | sudo apt install apparmor 63 | sudo make install WITH_APPARMOR=true 64 | 65 | In the case that you would like to install without root privileges, 66 | you can install it without the systemd service and you can specify 67 | an alternative location, for instance:: 68 | 69 | make --prefix=/home/user/.local install 70 | 71 | Note however that without systemd ``dhcpcanon`` will need to be run with root 72 | privileges, while the systemd service drop ``dhcpcanon`` root privileges and 73 | only keeps the required network capabilities. 74 | 75 | You would also need to install 76 | `resolvconf-admin `_ 77 | to be able to run it as non root user and set up DNS servers provided by the DHCP server. 78 | It will be possible to set up DNS servers with ``systemd`` too soon. 79 | 80 | An alternative to do not run ``dhcpcanon`` with root privileges nor systemd, 81 | is to use `ambient-rs wrapper `_ 82 | and run:: 83 | 84 | RUST_BACKTRACE=1 ./target/debug/ambient \ 85 | -c NET_RAW,NET_ADMIN,NET_BIND_SERVICE \ 86 | /usr/bin/python3 -m dhcpcanon.dhcpcanon -v 87 | 88 | Installation with pip 89 | ========================== 90 | 91 | The pip package does not install either system files and it can be installed 92 | without root, but it still needs to be run as root, as commented in the last 93 | section.:: 94 | 95 | pip3 install dhcpcanon 96 | 97 | In Debian this will install the files in ``/home/youruser/.local`` 98 | Note also that if you install it in a virtualenv, when executing ``dhcpcanon`` 99 | with ``sudo``, won't use the virtualenv. To keep the virtualenv run it with:: 100 | 101 | sudo /pathtovirtualenv/bin/dhcpcanon 102 | 103 | Installation for developers 104 | ============================= 105 | 106 | It is recommended to install ``dhcpcanon`` in a python virtual environment. 107 | 108 | Check https://virtualenv.pypa.io/en/latest/installation.html. In Debian:: 109 | 110 | sudo apt install python3-virtualenv 111 | 112 | Create a virtual environment:: 113 | 114 | mkdir ~/.virtualenvs 115 | virtualenv ~/.virtualenvs/dhcpcanonenv -p /usr/bin/python3 116 | source ~/.virtualenvs/dhcpcanonenv/bin/activate 117 | 118 | Get the sources:: 119 | git clone https://github.com/juga0/dhcpcanon 120 | 121 | Install it:: 122 | 123 | pip3 install -e . 124 | -------------------------------------------------------------------------------- /tests/dhcpcap_pkts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 3 | # SPDX-FileCopyrightText: 2016, juga 4 | # SPDX-License-Identifier: MIT 5 | """.""" 6 | from scapy.all import BOOTP, DHCP, IP, UDP, Ether 7 | 8 | # client packets 9 | 10 | dhcp_discover = ( 11 | Ether(src="00:01:02:03:04:05", dst="ff:ff:ff:ff:ff:ff") / 12 | IP(src="0.0.0.0", dst="255.255.255.255") / 13 | UDP(sport=68, dport=67) / 14 | BOOTP(chaddr=[b'\x00\x01\x02\x03\x04\x05'], xid=900000000) / 15 | DHCP(options=[ 16 | ('message-type', 'discover'), 17 | ("client_id", b'\x00\x01\x02\x03\x04\x05'), 18 | ("param_req_list", 19 | b"\x01\x03\x06\x0f\x1f\x21\x2b\x2c\x2e\x2f\x79\xf9\xfc"), 20 | 'end']) 21 | ) 22 | 23 | dhcp_request = ( 24 | Ether(src="00:01:02:03:04:05", dst="ff:ff:ff:ff:ff:ff") / 25 | IP(src="0.0.0.0", dst="255.255.255.255") / 26 | UDP(sport=68, dport=67) / 27 | BOOTP(chaddr=[b'\x00\x01\x02\x03\x04\x05'], xid=900000000) / 28 | DHCP(options=[ 29 | ('message-type', 'request'), 30 | ("client_id", b'\x00\x01\x02\x03\x04\x05'), 31 | ("param_req_list", 32 | b"\x01\x03\x06\x0f\x1f\x21\x2b\x2c\x2e\x2f\x79\xf9\xfc"), 33 | ("requested_addr", "192.168.1.23"), 34 | ("server_id", "192.168.1.1"), 35 | 'end']) 36 | ) 37 | 38 | dhcp_request_unicast = ( 39 | Ether(src="00:01:02:03:04:05", dst="00:0a:0b:0c:0d:0f") / 40 | IP(src="192.168.1.23", dst="192.168.1.1") / 41 | UDP(sport=68, dport=67) / 42 | BOOTP(chaddr=[b'\x00\x01\x02\x03\x04\x05'], xid=900000000, 43 | ciaddr="192.168.1.23") / 44 | DHCP(options=[ 45 | ('message-type', 'request'), 46 | ("client_id", b'\x00\x01\x02\x03\x04\x05'), 47 | ("param_req_list", 48 | b"\x01\x03\x06\x0f\x1f\x21\x2b\x2c\x2e\x2f\x79\xf9\xfc"), 49 | 'end']) 50 | ) 51 | 52 | dhcp_decline = ( 53 | Ether(src="00:01:02:03:04:05", dst="ff:ff:ff:ff:ff:ff") / 54 | IP(src="0.0.0.0", dst="255.255.255.255") / 55 | UDP(sport=68, dport=67) / 56 | BOOTP(chaddr=['\x00\x01\x02\x03\x04\x05'], options='c\x82Sc') / 57 | DHCP(options=[ 58 | ('message-type', 'decline'), 59 | ("requested_addr", "192.168.1.23"), 60 | ("server_id", "192.168.1.1"), 61 | 'end']) 62 | ) 63 | 64 | dhcp_inform = ( 65 | Ether(src="00:01:02:03:04:05", dst="ff:ff:ff:ff:ff:ff") / 66 | IP(src="192.168.1.23", dst="255.255.255.255") / 67 | UDP(sport=68, dport=67) / 68 | BOOTP(chaddr=['\x00\x01\x02\x03\x04\x05'], ciaddr="192.168.1.23", 69 | options='c\x82Sc') / 70 | DHCP(options=[ 71 | ('message-type', 'inform'), 72 | 'end']) 73 | ) 74 | 75 | # server packets 76 | ################# 77 | 78 | dhcp_offer = ( 79 | Ether(src="00:0a:0b:0c:0d:0f", dst="00:01:02:03:04:05") / 80 | IP(src="192.168.1.1", dst="192.168.1.23") / 81 | UDP(sport=67, dport=68) / 82 | BOOTP(op=2, yiaddr="192.168.1.23", siaddr="192.168.1.1", 83 | giaddr='0.0.0.0') / 84 | DHCP(options=[ 85 | ('message-type', 'offer'), 86 | ('server_id', "192.168.1.1"), 87 | ('lease_time', 43200), 88 | ('renewal_time', 21600), 89 | ('rebinding_time', 37800), 90 | ('subnet_mask', "255.255.255.0"), 91 | ('broadcast_address', "192.168.1.255"), 92 | ('router', "192.168.1.1"), 93 | ('name_server', "192.168.1.1", "8.8.8.8"), 94 | ('domain', b'localdomain'), 95 | 'end'] 96 | ) 97 | ) 98 | 99 | dhcp_ack = ( 100 | Ether(src="00:0a:0b:0c:0d:0f", dst="00:01:02:03:04:05") / 101 | IP(src="192.168.1.1", dst="192.168.1.23") / 102 | UDP(sport=67, dport=68) / 103 | BOOTP(op=2, yiaddr="192.168.1.23", siaddr="192.168.1.1", 104 | giaddr='0.0.0.0') / 105 | DHCP(options=[ 106 | ('message-type', 'ack'), 107 | ('server_id', "192.168.1.1"), 108 | ('lease_time', 43200), 109 | ('renewal_time', 21600), 110 | ('rebinding_time', 37800), 111 | ('subnet_mask', "255.255.255.0"), 112 | ('broadcast_address', "192.168.1.255"), 113 | ('router', "192.168.1.1"), 114 | ('name_server', "192.168.1.1", "8.8.8.8"), 115 | ('domain', b'localdomain'), 116 | 'end']) 117 | ) 118 | 119 | dhcp_nak = ( 120 | Ether(src="00:0a:0b:0c:0d:0f", dst="00:01:02:03:04:05") / 121 | IP(src="192.168.1.1", dst="192.168.1.23") / 122 | UDP(sport=67, dport=68) / 123 | BOOTP(op=2, yiaddr="192.168.1.23", siaddr="192.168.1.1", 124 | giaddr='0.0.0.0') / 125 | DHCP(options=[ 126 | ('message-type', 'nak'), 127 | ('server_id', "192.168.1.1"), 128 | 'end']) 129 | ) 130 | -------------------------------------------------------------------------------- /docs/source/images/packages_dhcpcanon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | packages_dhcpcanon 11 | 12 | 13 | 0 14 | 15 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/__init__.py 16 | 17 | 18 | 1 19 | 20 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/_version.py 21 | 22 | 23 | 2 24 | 25 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/clientscript.py 26 | 27 | 28 | 3 29 | 30 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/conflog.py 31 | 32 | 33 | 4 34 | 35 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/constants.py 36 | 37 | 38 | 5 39 | 40 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/dhcpcanon.py 41 | 42 | 43 | 6 44 | 45 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/dhcpcap.py 46 | 47 | 48 | 7 49 | 50 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/dhcpcapfsm.py 51 | 52 | 53 | 8 54 | 55 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/dhcpcaplease.py 56 | 57 | 58 | 9 59 | 60 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/dhcpcaputils.py 61 | 62 | 63 | 10 64 | 65 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/netutils.py 66 | 67 | 68 | 11 69 | 70 | /home/user/_my/code/dhcp-related/dhcpcanon/dhcpcanon/timers.py 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for a source distribution of dhcpcanon. 2 | # 3 | # This package is not self-contained and the build products may require other 4 | # dependencies to function; it is given as a reference for distro packagers. 5 | # 6 | # SPDX-FileCopyrightText: 2016, juga 7 | # SPDX-License-Identifier: MIT 8 | 9 | PACKAGE = dhcpcanon 10 | VERSION = $(shell sh version.sh) 11 | DESTDIR = 12 | 13 | THISFILE = $(lastword $(MAKEFILE_LIST)) 14 | PYTHON = python3 15 | 16 | # GNU command variables 17 | # see http://www.gnu.org/prep/standards/html_node/Command-Variables.html 18 | 19 | INSTALL = install 20 | INSTALL_DATA = $(INSTALL) -m 644 21 | INSTALL_PROGRAM = $(INSTALL) 22 | INSTALL_SCRIPT = $(INSTALL) 23 | 24 | # GNU directory variables 25 | # see http://www.gnu.org/prep/standards/html_node/Directory-Variables.html 26 | 27 | prefix = /usr/local 28 | exec_prefix = $(prefix) 29 | sbindir = $(exec_prefix)/sbin 30 | 31 | datarootdir = $(prefix)/share 32 | datadir = $(datarootdir) 33 | sysconfdir = $(prefix)/etc 34 | 35 | docdir = $(datarootdir)/doc/$(PACKAGE) 36 | mandir = $(datarootdir)/man 37 | man8dir = $(mandir)/man8 38 | 39 | # for systemd 40 | tmpfilesdir=/usr/lib/tmpfiles.d 41 | systemunitdir=/lib/systemd/system 42 | # for systemd udev 43 | networkdir=/lib/systemd/network 44 | 45 | # for apparmor 46 | apparmordir=/etc/apparmor.d 47 | 48 | srcdir = . 49 | 50 | SRC_MAN8 = man/dhcpcanon.8 51 | SRC_SCRIPT = sbin/dhcpcanon-script 52 | SRC_DOC = README.rst LICENSE 53 | SRC_TMPFILES = tmpfiles.d/dhcpcanon.conf 54 | SRC_UNITFILE = systemd/dhcpcanon.service 55 | SRC_APPARMOR = apparmor.d/sbin.dhcpcanon 56 | SRC_LINKFILE = systemd/network/90-dhcpcanon.link 57 | SRC_ALL = $(SRC_SCRIPT) $(SRC_DOC) $(SRC_MAN8) 58 | 59 | DST_MAN8 = $(SRC_MAN8) 60 | DST_SCRIPT = $(SRC_SCRIPT) 61 | DST_DOC = $(SRC_DOC) 62 | DST_TMPFILES = $(SRC_TMPFILES) 63 | DST_UNITFILE = $(SRC_UNITFILE) 64 | DST_APPARMOR = $(SRC_APPARMOR) 65 | DST_LINKFILE = $(SRC_LINKFILE) 66 | DST_ALL = $(DST_SCRIPT) $(DST_DOC) $(DST_MAN8) 67 | 68 | TEST_PY = dhcpcanon-test.py 69 | 70 | all: $(DST_ALL) $(THISFILE) 71 | 72 | install: all 73 | @echo $@ 74 | 75 | mkdir -p $(DESTDIR)$(sbindir) 76 | for i in $(DST_SCRIPT); do $(INSTALL_SCRIPT) "$$i" /sbin; done 77 | mkdir -p $(DESTDIR)$(docdir) 78 | for i in $(DST_DOC); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(docdir); done 79 | mkdir -p $(DESTDIR)$(man8dir) 80 | for i in $(DST_MAN8); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(man8dir); done 81 | 82 | $(PYTHON) setup.py install --record installed.txt $(if $(DESTDIR),--root=$(DESTDIR),--install-scripts=/sbin) 83 | 84 | if [ -n "$(WITH_SYSTEMD)" ]; then \ 85 | adduser --system dhcpcanon; \ 86 | mkdir -p $(DESTDIR)$(systemunitdir); \ 87 | for i in $(DST_UNITFILE); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(systemunitdir); done; \ 88 | mkdir -p $(DESTDIR)$(tmpfilesdir); \ 89 | for i in $(DST_TMPFILES); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(tmpfilesdir); done; \ 90 | systemctl enable $(DESTDIR)$(systemunitdir)/dhcpcanon.service; \ 91 | systemd-tmpfiles --create --root=$(DESTDIR)$(tmpfilesdir)/dhcpcanon.conf; \ 92 | systemctl start $(DESTDIR)$(systemunitdir)/dhcpcanon.service; \ 93 | systemctl status $(DESTDIR)$(systemunitdir)/dhcpcanon.service; \ 94 | fi 95 | 96 | if [ -n "$(WITH_SYSTEMD_UDEV)" ]; then \ 97 | mkdir -p $(DESTDIR)$(networkdir); \ 98 | for i in $(DST_LINKFILE); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(networkdir); done; \ 99 | fi 100 | 101 | if [ -n "$(WITH_APPARMOR)" ]; then \ 102 | mkdir -p $(DESTDIR)$(apparmordir); \ 103 | for i in $(DST_APPARMOR); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(apparmordir); done; \ 104 | for i in $(DST_APPARMOR); do aa-complain $(DESTDIR)$(apparmordir)/"$$i"; done; \ 105 | fi 106 | 107 | uninstall: 108 | @echo $@ 109 | for i in $(notdir $(DST_SCRIPT)); do rm -f $(DESTDIR)$(sbindir)/"$$i"; done 110 | for i in $(notdir $(DST_DOC)); do rm -f $(DESTDIR)$(docdir)/"$$i"; done 111 | for i in $(notdir $(DST_MAN8)); do rm -f $(DESTDIR)$(man8dir)/"$$i"; done 112 | # it will only work in the case that the file has not been removed 113 | cat installed.txt | xargs rm -rf 114 | # systemd files 115 | for i in $(notdir $(DST_UNITFILE)); do rm -f $(DESTDIR)$(systemunitdir)/"$$i"; done 116 | for i in $(notdir $(DST_TMPFILES)); do rm -f $(DESTDIR)$(tmpfilesdir)/"$$i"; done 117 | for i in $(notdir $(DST_APPARMOR)); do rm -f $(DESTDIR)$(apparmordir)/"$$i"; done 118 | for i in $(notdir $(DST_LINKFILE)); do rm -f $(DESTDIR)$(networkdir)/"$$i"; done 119 | 120 | clean: 121 | python setup.py clean 122 | rm -rf *.pyc build dist dhcpcanon.egg-info 123 | 124 | distclean: clean 125 | 126 | maintainer-clean: distclean 127 | rm -f $(DST_MAN8) 128 | 129 | pylint: $(SRC_SCRIPT) 130 | pylint -E $^ 131 | 132 | check: $(THISFILE) 133 | for i in $(TEST_PY); do $(PYTHON) "$$i"; done 134 | 135 | .PHONY: all install uninstall clean distclean maintainer-clean check pylint 136 | -------------------------------------------------------------------------------- /docs/source/integration.rst: -------------------------------------------------------------------------------- 1 | .. _integration: 2 | 3 | ``dhcpcanon`` integration with network managers 4 | ================================================ 5 | 6 | Integration with Gnome ``Network Manager`` 7 | ------------------------------------------- 8 | 9 | `Gnome Network Manager `_ 10 | has several components. 11 | 12 | In Debian the service ``NetworkManager`` by default 13 | calls `dhclient `_ 14 | which in turn calls ``nm-dhcp-helper``. 15 | Depending on the configuration, dhclient is called with the parameters:: 16 | 17 | /sbin/dhclient -d -q 18 | -sf /usr/lib/NetworkManager/nm-dhcp-helper 19 | -pf /var/run/dhclient-.pid 20 | -lf /var/lib/NetworkManager/dhclient--.lease 21 | -cf /var/lib/NetworkManager/dhclient-.conf 22 | 23 | 24 | Dclient calls ``nm-dhcp-helper`` via the ``-sf`` parameter, 25 | which seems to communicate back with ``NetworkManager`` via D-Bus. 26 | 27 | ``NetworkManager`` can be configured to use `dhcpcd `_ 28 | or ``internal``, as DHCP clients instead of ``dhclient``. 29 | 30 | .. parsed-literal:: 31 | 32 | FIXME: Configuring ``NetworkManager`` to use ``internal`` did not work 33 | (why?). Is it using systemd DHCP client code? (``libsystemd-network `_ 34 | is included in ``NetworkManager`` source code, which is in ``systemd`` 35 | `code `_). 36 | 37 | It does not work either with ``dhcpcd``: 38 | NetworkManager[12712]: [1493146345.7994] dhcp-init: DHCP client 'dhcpcd' not available 39 | 40 | 41 | Environment variables that ``dhclient`` returns 42 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | When ``dhclient`` call the script, by default ``/sbin/dhcpcanon-script``, 45 | or when called by ``NetworkManager``, ``nm-dhcp-helper``, it pass environment 46 | variables. 47 | 48 | .. parsed-literal:: 49 | 50 | FIXME: Are these variables documented somewhere?. 51 | 52 | In ``man dhclient-script`` there is the list of values that the variable ``reason`` can take:: 53 | 54 | The following reasons 55 | are currently defined: MEDIUM, PREINIT, BOUND, RENEW, REBIND, REBOOT, 56 | EXPIRE, FAIL, STOP, RELEASE, NBI and TIMEOUT. 57 | 58 | But there are more variables. 59 | By setting ``RUN=yes`` in ``/etc/dhcp/debug``, these variables are found 60 | in ``/tmp/dhclient-script.debug``:: 61 | 62 | reason='PREINIT' 63 | interface= 64 | -------------------------- 65 | reason='REBOOT' 66 | interface= 67 | new_ip_address= 68 | new_network_number= 69 | new_subnet_mask= 70 | new_broadcast_address= 71 | new_routers= 72 | new_domain_name= 73 | new_domain_name_servers= 74 | 75 | Looking at the code `dhclient v4.3.5 `_ 76 | there seem to be more variables. 77 | 78 | Environment variables that ``nm-dhcp-helper`` gets 79 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 80 | 81 | TBD 82 | 83 | ?? 84 | 85 | ``dhcpcanon`` required modifications 86 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 87 | 88 | If ``dhcpcanon`` accepts the same arguments as ``dhclient`` and calls 89 | the script ``nm-dhcp-helper`` with the same environment 90 | variables as ``dhclient``, it should be integrated. 91 | 92 | .. parsed-literal:: 93 | 94 | FIXME: however for some reason this generates D-Bus errors. 95 | 96 | ``dhcpcanon`` could also implement the D-Bus input/output that 97 | ``NetworkManager`` needs. 98 | 99 | There's a `NetworkManager D-Bus API `_ 100 | specification. 101 | 102 | There's also a Python API, `python-networkmanager `_, 103 | so ``dhcpcanon`` could communicate directly with ``NetworkManager`` instead 104 | communicating with ``nm-dhcp-helper``. 105 | 106 | 107 | nm notes 108 | --------- 109 | 110 | Debugging: 111 | 112 | [logging] 113 | level=DEBUG 114 | 115 | 116 | It is not possible to set ``dhcp-send-hostname`` 117 | (`Bug 768076 - No way to set dhcp-send-hostname globally `_) 118 | globally. 119 | 120 | To modify ``dhcp-send-hostname`` per interface: 121 | 122 | nmcli connection modify "Wired connection" ipv4.dhcp-send-hostname no 123 | nmcli connection show "Wired connection" 124 | 125 | Or the files: 126 | /etc/NetworkManager/system-connections/Wired\ connection 127 | 128 | There is currently no way that when a new device is create it defaults to a configuration. 129 | 130 | 131 | Integration with ``wicd`` 132 | --------------------------- 133 | 134 | TBD 135 | 136 | `wicd `_ 137 | 138 | `wicd documentation `_ 139 | -------------------------------------------------------------------------------- /dhcpcanon/netutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Netowrk utils for the DHCP client implementation of the Anonymity Profile 5 | ([:rfc:`7844`]).""" 6 | import logging 7 | import os.path 8 | import subprocess 9 | 10 | from dbus import SystemBus, Interface, DBusException 11 | from pyroute2 import IPRoute 12 | from pyroute2.netlink import NetlinkError 13 | 14 | from .constants import RESOLVCONF, RESOLVCONF_ADMIN 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def set_net(lease): 20 | ipr = IPRoute() 21 | try: 22 | index = ipr.link_lookup(ifname=lease.interface)[0] 23 | except IndexError as e: 24 | logger.error('Interface %s not found, can not set IP.', 25 | lease.interface) 26 | try: 27 | ipr.addr('add', index, address=lease.address, 28 | mask=int(lease.subnet_mask_cidr)) 29 | except NetlinkError as e: 30 | if ipr.get_addr(index=index)[0].\ 31 | get_attrs('IFA_ADDRESS')[0] == lease.address: 32 | logger.debug('Interface %s is already set to IP %s' % 33 | (lease.interface, lease.address)) 34 | else: 35 | logger.error(e) 36 | else: 37 | logger.debug('Interface %s set to IP %s' % 38 | (lease.interface, lease.address)) 39 | try: 40 | ipr.route('add', dst='0.0.0.0', gateway=lease.router, oif=index) 41 | except NetlinkError as e: 42 | if ipr.get_routes(table=254)[0].\ 43 | get_attrs('RTA_GATEWAY')[0] == lease.router: 44 | logger.debug('Default gateway is already set to %s' % 45 | (lease.router)) 46 | else: 47 | logger.error(e) 48 | else: 49 | logger.debug('Default gateway set to %s', lease.router) 50 | ipr.close() 51 | set_dns(lease) 52 | 53 | 54 | def set_dns(lease): 55 | if systemd_resolved_status() is True: 56 | set_dns_systemd_resolved(lease) 57 | elif os.path.exists(RESOLVCONF_ADMIN): 58 | set_dns_resolvconf_admin(lease) 59 | elif os.path.exists(RESOLVCONF): 60 | set_dns_resolvconf(lease) 61 | 62 | 63 | def set_dns_resolvconf_admin(lease): 64 | cmd = [RESOLVCONF_ADMIN, 'add', lease.interface, lease.name_server] 65 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, 66 | stderr=subprocess.PIPE) 67 | try: 68 | (stdout, stderr) = proc.communicate() 69 | return True 70 | except TypeError as e: 71 | logger.error(e) 72 | return False 73 | 74 | 75 | def set_dns_resolvconf(lease): 76 | cmd = [RESOLVCONF, '-a', lease.interface] 77 | proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, 78 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 79 | stdin = '\n'.join(['nameserver ' + nm for nm in 80 | lease.name_server.split()]) 81 | stdin = str.encode(stdin) 82 | try: 83 | (stdout, stderr) = proc.communicate(stdin) 84 | return True 85 | except TypeError as e: 86 | logger.error(e) 87 | return False 88 | 89 | 90 | def set_dns_systemd_resolved(lease): 91 | # NOTE: if systemd-resolved is not already running, we might not want to 92 | # run it in case there's specific system configuration for other resolvers 93 | ipr = IPRoute() 94 | index = ipr.link_lookup(ifname=lease.interface)[0] 95 | # Construct the argument to pass to DBUS. 96 | # the equivalent argument for: 97 | # busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 \ 98 | # org.freedesktop.resolve1.Manager SetLinkDNS 'ia(iay)' 2 1 2 4 1 2 3 4 99 | # is SetLinkDNS(2, [(2, [8, 8, 8, 8])]_ 100 | iay = [(2, [int(b) for b in ns.split('.')]) 101 | for ns in lease.name_server.split()] 102 | # if '.' in ns 103 | # else (10, [ord(x) for x in 104 | # socket.inet_pton(socket.AF_INET6, ns)]) 105 | bus = SystemBus() 106 | resolved = bus.get_object('org.freedesktop.resolve1', 107 | '/org/freedesktop/resolve1') 108 | manager = Interface(resolved, 109 | dbus_interface='org.freedesktop.resolve1.Manager') 110 | try: 111 | manager.SetLinkDNS(index, iay) 112 | return True 113 | except DBusException as e: 114 | logger.error(e) 115 | return False 116 | 117 | 118 | def systemd_resolved_status(): 119 | bus = SystemBus() 120 | systemd = bus.get_object('org.freedesktop.systemd1', 121 | '/org/freedesktop/systemd1') 122 | manager = Interface(systemd, 123 | dbus_interface='org.freedesktop.systemd1.Manager') 124 | unit = manager.LoadUnit('sytemd-resolved.service') 125 | proxy = bus.get_object('org.freedesktop.systemd1', str(unit)) 126 | r = proxy.Get('org.freedesktop.systemd1.Unit', 127 | 'ActiveState', 128 | dbus_interface='org.freedesktop.DBus.Properties') 129 | if str(r) == 'active': 130 | return True 131 | return False 132 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2016, juga 2 | .. SPDX-License-Identifier: CC-BY-SA-4.0 3 | 4 | dhcpcanon - DHCP anonymity profile 5 | ================================== 6 | 7 | |PyPI| |Build Status| |Coverage Status| |Documentation status| |CII Best 8 | Practices| 9 | 10 | DHCP client disclosing less identifying information. 11 | 12 | Python implementation of the DHCP Anonymity Profile 13 | (`RFC7844 `__) designed for users 14 | that wish to remain anonymous to the visited network minimizing 15 | disclosure of identifying information. 16 | 17 | Technologies 18 | ------------ 19 | 20 | This implementation uses the Python `Scapy 21 | Automata `__ 22 | 23 | What is the Anonymity Profile? 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | As the RFC7844 stats: 27 | 28 | Some DHCP options carry unique identifiers. These identifiers can 29 | enable device tracking even if the device administrator takes care 30 | of randomizing other potential identifications like link-layer 31 | addresses or IPv6 addresses. The anonymity profiles are designed for 32 | clients that wish to remain anonymous to the visited network. The 33 | profiles provide guidelines on the composition of DHCP or DHCPv6 34 | messages, designed to minimize disclosure of identifying 35 | information. 36 | 37 | What is DHCP? 38 | ~~~~~~~~~~~~~ 39 | 40 | From `Wikipedia `__: 41 | 42 | The **Dynamic Host Configuration Protocol** (**DHCP**) is a 43 | standardized `network 44 | protocol `__ used on 45 | `Internet 46 | Protocol `__ (IP) 47 | networks. The DHCP is controlled by a DHCP server that dynamically 48 | distributes network configuration parameters, such as `IP 49 | addresses `__, for 50 | interfaces and services. A 51 | `router `__ or 52 | a `residential 53 | gateway `__ can 54 | be enabled to act as a DHCP server. A DHCP server enables computers 55 | to request IP addresses and networking parameters automatically, 56 | reducing the need for a `network 57 | administrator `__ 58 | or a user to configure these settings manually. In the absence of a 59 | DHCP server, each computer or other device (eg., a printer) on the 60 | network needs to be statically (ie., manually) assigned to an IP 61 | address. 62 | 63 | Documentation 64 | ------------- 65 | 66 | A more extensive online documentation is available in `Read the 67 | docs `__. The documentation source is 68 | in `this repository `__. 69 | 70 | Visit `DHCPAP `__ for an overview of all the 71 | repositories related to the RFC7844 implementation work. 72 | 73 | Installation 74 | ------------ 75 | 76 | See `Installation `__ and 77 | `Running `__ 78 | 79 | Download 80 | -------- 81 | 82 | You can download this project in either 83 | `zip `__ or 84 | `tar `__ formats. 85 | 86 | You can also clone the project with Git by running: 87 | 88 | :: 89 | 90 | git clone https://github.com/juga0/dhcpcanon 91 | 92 | Bugs and features 93 | ----------------- 94 | 95 | If you wish to signal a bug or report a feature request, please fill-in 96 | an issue on the `dhcpcanon issue 97 | tracker `__. 98 | 99 | Current status 100 | -------------- 101 | 102 | WIP, still not recommended for end users. Testers welcomed. 103 | 104 | See `TODO <./docs/source/todo.rst>`__ 105 | 106 | License 107 | ------- 108 | 109 | ``dhcpcanon`` is copyright 2016-2018 by juga ( juga at riseup dot net) 110 | and is licensed by the terms of the MIT license. The documentation is under CC-BY-SA-4.0. 111 | 112 | Acknowledgments 113 | --------------- 114 | 115 | To all the persons that have given suggestions and comments about this 116 | implementation, the authors of the `RFC 117 | 7844 `__, the `Prototype Fund 118 | Project `__ of the `Open Knowledge Foundation 119 | Germany `__ and the `Federal Ministry of Education and 120 | Research `__ who partially funds this work. 121 | 122 | .. |PyPI| image:: https://img.shields.io/pypi/v/dhcpcanon.svg 123 | :target: https://pypi.python.org/pypi/dhcpcanon 124 | .. |Build Status| image:: https://www.travis-ci.org/juga0/dhcpcanon.svg?branch=master 125 | :target: https://www.travis-ci.org/juga0/dhcpcanon 126 | .. |Coverage Status| image:: https://coveralls.io/repos/github/juga0/dhcpcanon/badge.svg?branch=master 127 | :target: https://coveralls.io/github/juga0/dhcpcanon?branch=master 128 | .. |Documentation Status| image:: https://readthedocs.org/projects/dhcpcanon/badge/?version=latest 129 | :target: http://dhcpcanon.readthedocs.io/en/latest/?badge=latest 130 | .. |CII Best Practices| image:: https://bestpractices.coreinfrastructure.org/projects/1020/badge 131 | :target: https://bestpractices.coreinfrastructure.org/projects/1020 132 | -------------------------------------------------------------------------------- /dhcpcanon/timers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Timers for the DHCP client implementation of the Anonymity Profile 5 | ([:rfc:`7844`]).""" 6 | from __future__ import absolute_import 7 | 8 | import logging 9 | import random 10 | from datetime import datetime, timedelta 11 | 12 | from .constants import (DT_PRINT_FORMAT, MAX_DELAY_SELECTING, REBIND_PERC, 13 | RENEW_PERC) 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | def future_dt_str(dt, td): 19 | """.""" 20 | if isinstance(td, str): 21 | td = float(td) 22 | td = timedelta(seconds=td) 23 | future_dt = dt + td 24 | return future_dt.strftime(DT_PRINT_FORMAT) 25 | 26 | 27 | def nowutc(): 28 | """.""" 29 | # NOTE: Not using UTC, as all the timers are set in reference to local time 30 | # now = datetime.utcnow().replace(tzinfo=utc) 31 | now = datetime.now() 32 | return now 33 | 34 | 35 | def gen_delay_selecting(): 36 | """Generate the delay in seconds in which the DISCOVER will be sent. 37 | 38 | [:rfc:`2131#section-4.4.1`]:: 39 | 40 | The client SHOULD wait a random time between one and ten seconds to 41 | desynchronize the use of DHCP at startup. 42 | 43 | """ 44 | delay = float(random.randint(0, MAX_DELAY_SELECTING)) 45 | logger.debug('Delay to enter in SELECTING %s.', delay) 46 | logger.debug('SELECTING will happen on %s', 47 | future_dt_str(nowutc(), delay)) 48 | return delay 49 | 50 | 51 | def gen_timeout_resend(attempts): 52 | """Generate the time in seconds in which DHCPDISCOVER wil be retransmited. 53 | 54 | [:rfc:`2131#section-3.1`]:: 55 | 56 | might retransmit the 57 | DHCPREQUEST message four times, for a total delay of 60 seconds 58 | 59 | [:rfc:`2131#section-4.1`]:: 60 | 61 | For example, in a 10Mb/sec Ethernet 62 | internetwork, the delay before the first retransmission SHOULD be 4 63 | seconds randomized by the value of a uniform random number chosen 64 | from the range -1 to +1. Clients with clocks that provide resolution 65 | granularity of less than one second may choose a non-integer 66 | randomization value. The delay before the next retransmission SHOULD 67 | be 8 seconds randomized by the value of a uniform number chosen from 68 | the range -1 to +1. The retransmission delay SHOULD be doubled with 69 | subsequent retransmissions up to a maximum of 64 seconds. 70 | 71 | """ 72 | timeout = 2 ** (attempts + 1) + random.uniform(-1, +1) 73 | logger.debug('next timeout resending will happen on %s', 74 | future_dt_str(nowutc(), timeout)) 75 | return timeout 76 | 77 | 78 | def gen_timeout_request_renew(lease): 79 | """Generate time in seconds to retransmit DHCPREQUEST. 80 | 81 | [:rfc:`2131#section-4..4.5`]:: 82 | 83 | In both RENEWING and REBINDING states, 84 | if the client receives no response to its DHCPREQUEST 85 | message, the client SHOULD wait one-half of the remaining 86 | time until T2 (in RENEWING state) and one-half of the 87 | remaining lease time (in REBINDING state), down to a 88 | minimum of 60 seconds, before retransmitting the 89 | DHCPREQUEST message. 90 | 91 | """ 92 | time_left = (lease.rebinding_time - lease.renewing_time) * RENEW_PERC 93 | if time_left < 60: 94 | time_left = 60 95 | logger.debug('Next request in renew will happen on %s', 96 | future_dt_str(nowutc(), time_left)) 97 | return time_left 98 | 99 | 100 | def gen_timeout_request_rebind(lease): 101 | """.""" 102 | time_left = (lease.lease_time - lease.rebinding_time) * RENEW_PERC 103 | if time_left < 60: 104 | time_left = 60 105 | logger.debug('Next request on rebinding will happen on %s', 106 | future_dt_str(nowutc(), time_left)) 107 | return time_left 108 | 109 | 110 | def gen_renewing_time(lease_time, elapsed=0): 111 | """Generate RENEWING time. 112 | 113 | [:rfc:`2131#section-4.4.5`]:: 114 | 115 | T1 116 | defaults to (0.5 * duration_of_lease). T2 defaults to (0.875 * 117 | duration_of_lease). Times T1 and T2 SHOULD be chosen with some 118 | random "fuzz" around a fixed value, to avoid synchronization of 119 | client reacquisition. 120 | 121 | """ 122 | renewing_time = int(lease_time) * RENEW_PERC - elapsed 123 | # FIXME:80 [:rfc:`2131#section-4.4.5`]: the chosen "fuzz" could fingerprint 124 | # the implementation 125 | # NOTE: here using same "fuzz" as systemd? 126 | range_fuzz = int(lease_time) * REBIND_PERC - renewing_time 127 | logger.debug('rebinding fuzz range %s', range_fuzz) 128 | fuzz = random.uniform(-(range_fuzz), 129 | +(range_fuzz)) 130 | renewing_time += fuzz 131 | logger.debug('Renewing time %s.', renewing_time) 132 | return renewing_time 133 | 134 | 135 | def gen_rebinding_time(lease_time, elapsed=0): 136 | """.""" 137 | rebinding_time = int(lease_time) * REBIND_PERC - elapsed 138 | # FIXME:90 [:rfc:`2131#section-4.4.5`]: the chosen "fuzz" could fingerprint 139 | # the implementation 140 | # NOTE: here using same "fuzz" as systemd? 141 | range_fuzz = int(lease_time) - rebinding_time 142 | logger.debug('rebinding fuzz range %s', range_fuzz) 143 | fuzz = random.uniform(-(range_fuzz), 144 | +(range_fuzz)) 145 | rebinding_time += fuzz 146 | logger.debug('Rebinding time %s.', rebinding_time) 147 | return rebinding_time 148 | -------------------------------------------------------------------------------- /tests/test_dhcpcapfsm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # SPDX-FileCopyrightText: 2016, juga 4 | # SPDX-License-Identifier: MIT 5 | """Tests for the FSM of the DHCP client implementation of the Anonymity Profile 6 | ([:rfc:`7844`]). 7 | 8 | .. todo:: 9 | 10 | Test for more cases: 11 | - delays getting OFFERs 12 | - delays getting ACK 13 | - RENEWING state 14 | - REBINDING state 15 | - lease expires 16 | 17 | """ 18 | import logging 19 | import logging.config 20 | import pytest 21 | from datetime import datetime 22 | 23 | from scapy.automaton import Automaton 24 | from scapy.config import conf 25 | 26 | from dhcpcanon.conflog import LOGGING 27 | from dhcpcanon.constants import STATES2NAMES 28 | from dhcpcanon.dhcpcapfsm import DHCPCAPFSM 29 | from dhcpcap_pkts import dhcp_ack, dhcp_offer 30 | from dhcpcapfsm_objs import (fsm_bound, fsm_init, fsm_preinit, fsm_requesting, 31 | fsm_selecting) 32 | 33 | logging.config.dictConfig(LOGGING) 34 | logger = logging.getLogger('dhcpcanon') 35 | logger_scapy_interactive = logging.getLogger('scapy.interactive') 36 | logger.setLevel(logging.DEBUG) 37 | logger_scapy_interactive.setLevel(logging.DEBUG) 38 | 39 | 40 | class DummySocket(object): 41 | def __init__(self, *args, **kargs): 42 | pass 43 | 44 | def send(self, pkt): 45 | pass 46 | 47 | def fileno(self): 48 | return 0 49 | 50 | def recv(self, n=0): 51 | return dhcp_offer 52 | 53 | def close(self): 54 | pass 55 | 56 | 57 | class DummySocketAck(DummySocket): 58 | def recv(self, n=0): 59 | return dhcp_ack 60 | 61 | 62 | @pytest.mark.skip(reason="DummySocket not working.") 63 | class TestDHCPCAPFSM: 64 | """.""" 65 | 66 | def test_preinit_bound(self): 67 | """Test FSM from PREINIT to BOUND. No delays. No script""" 68 | logger.debug('Test PREINIT') 69 | # fsm_preinit['script'].script_init(fsm_preinit['client'].lease, 70 | # fsm_preinit['current_state']) 71 | # recvsock=DummySocket) will fail with python3 72 | conf.L2listen = DummySocket 73 | # for sendp: 74 | conf.L2socket = DummySocket 75 | dhcpcanon = DHCPCAPFSM(client_mac='00:01:02:03:04:05', iface='eth0', 76 | xid=900000000, 77 | # scriptfile='/sbin/dhcpcanon-script', 78 | delay_selecting=1, timeout_select=1, 79 | ll=DummySocket) 80 | assert dhcpcanon.dict_self() == fsm_preinit 81 | logger.debug('Test INIT') 82 | logger.debug('============') 83 | logger.debug('state %s', STATES2NAMES[dhcpcanon.current_state]) 84 | # fsm_init['script'].script_init(fsm_init['client'].lease, 85 | # fsm_init['current_state'] - 1) 86 | logger.debug('Test start running, INIT') 87 | try: 88 | dhcpcanon.next() 89 | except Automaton.Singlestep as err: 90 | logger.debug('Singlestep %s in state %s', err, 91 | dhcpcanon.current_state) 92 | assert dhcpcanon.dict_self() == fsm_init 93 | logger.debug('Test SELECTING') 94 | logger.debug('===============') 95 | logger.debug('State %s', STATES2NAMES[dhcpcanon.current_state]) 96 | logger.debug('Num offers %s', len(dhcpcanon.offers)) 97 | # fsm_selecting['script'].script_init(fsm_init['client'].lease, 98 | # 'PREINIT') 99 | logger.debug('Test timeout selecting %s', 100 | dhcpcanon.get_timeout(dhcpcanon.current_state, 101 | dhcpcanon.timeout_selecting)) 102 | # FIXME:110 why is needed here to press enter to don't retransmit? 103 | try: 104 | dhcpcanon.next() 105 | except Automaton.Singlestep as err: 106 | logger.debug('Singlestep %s in state %s', err, 107 | dhcpcanon.current_state) 108 | # os.kill(os.getpid(), signal.SIGCONT) 109 | # logger.debug(dhcpcanon.dict_self()['script']) 110 | # logger.debug(fsm_selecting['script']) 111 | # TODO: case when offer is not received, 112 | # and next step is selecting again 113 | if len(dhcpcanon.offers) < 1: 114 | logger.debug('Offer not received, tests are not complete yet.') 115 | return 116 | # assert dhcpcanon.dict_self()['script'] == fsm_selecting['script'] 117 | assert dhcpcanon.dict_self()['client'].lease == \ 118 | fsm_selecting['client'].lease 119 | assert dhcpcanon.dict_self()['client'] == \ 120 | fsm_selecting['client'] 121 | # with mock.patch('dhcpcanon.timers.nowutc', 122 | # return_value=datetime(2017, 6, 23)): 123 | dhcpcanon.time_sent_request = datetime(2017, 6, 23) 124 | assert dhcpcanon.dict_self() == fsm_selecting 125 | logger.debug('Test REQUESTING') 126 | logger.debug('=================') 127 | logger.debug('State %s', STATES2NAMES[dhcpcanon.current_state]) 128 | # dummy socket that will receive an ACK 129 | dhcpcanon.listen_sock = DummySocketAck() 130 | # fsm_requesting['script'].script_init(fsm_init['client'].lease, 131 | # 'PREINIT') 132 | try: 133 | dhcpcanon.next() 134 | except Automaton.Singlestep as err: 135 | logger.debug('Singlestep %s in state %s', err, 136 | dhcpcanon.current_state) 137 | 138 | # assert dhcpcanon.dict_self()['script'] == fsm_requesting['script'] 139 | # set the timers accourding to the time pkt sent 140 | # dhcpcanon.client.lease.set_times(datetime(2017, 6, 23)) 141 | dhcpcanon.set_timers() 142 | assert dhcpcanon.dict_self()['client'].lease == \ 143 | fsm_requesting['client'].lease 144 | assert dhcpcanon.dict_self()['client'] == \ 145 | fsm_requesting['client'] 146 | assert dhcpcanon.dict_self() == fsm_requesting 147 | logger.debug('Test BOUND') 148 | logger.debug('============') 149 | logger.debug('State %s', STATES2NAMES[dhcpcanon.current_state]) 150 | # fsm_bound['script'].script_init(fsm_bound['client'].lease, 151 | # 'BOUND') 152 | try: 153 | dhcpcanon.next() 154 | except Automaton.Singlestep as err: 155 | logger.debug('Singlestep %s in state %s', err, 156 | dhcpcanon.current_state) 157 | # assert dhcpcanon.dict_self()['script'] == fsm_bound['script'] 158 | assert dhcpcanon.dict_self()['client'].lease == \ 159 | fsm_bound['client'].lease 160 | assert dhcpcanon.dict_self()['client'] == \ 161 | fsm_bound['client'] 162 | assert dhcpcanon.dict_self() == fsm_bound 163 | -------------------------------------------------------------------------------- /dhcpcanon/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Constants for the DHCP client implementation of the Anonymity Profile 5 | ([:rfc:`7844`]).""" 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | DT_PRINT_FORMAT = '%y-%m-%d %H:%M:%S' 12 | 13 | # client 14 | ########## 15 | BROADCAST_MAC = 'ff:ff:ff:ff:ff:ff' 16 | META_MAC = '00:00:00:00:00:00' 17 | BROADCAST_ADDR = '255.255.255.255' 18 | META_ADDR = '0.0.0.0' 19 | CLIENT_PORT = 68 20 | SERVER_PORT = 67 21 | 22 | # DHCP timers 23 | ########## 24 | LEASE_TIME = 1209600 # 14 DAYS 25 | RENEWING_TIME = 604800 # 7 DAYS 26 | REBINDING_TIME = 1058400 # 12 DAYS 27 | DELAY_SELECTING = 10 28 | TIMEOUT_SELECTING = 60 29 | TIMEOUT_REQUESTING = 60 30 | TIMEOUT_REQUEST_RENEWING = 226800 31 | TIMEOUT_REQUEST_REBINDING = 75600 32 | 33 | MAX_DELAY_SELECTING = 10 34 | RENEW_PERC = 0.5 35 | REBIND_PERC = 0.875 36 | 37 | # DHCP number packet retransmissions 38 | MAX_ATTEMPTS_DISCOVER = 5 39 | MAX_OFFERS_COLLECTED = 1 40 | MAX_ATTEMPTS_REQUEST = 5 41 | 42 | # DHCP packet 43 | ############## 44 | DHCP_OFFER_OPTIONS = [ 45 | 'server_id', 'subnet_mask', 'broadcast_address', 46 | 'router', 'domain', 'name_server', 'lease_time', 'renewal_time', 47 | 'rebinding_time'] 48 | 49 | # DHCP FSM 50 | ############# 51 | STATE_ERROR = -1 52 | STATE_PREINIT = 0 53 | STATE_INIT = 1 54 | STATE_SELECTING = 2 55 | STATE_REQUESTING = 3 56 | STATE_BOUND = 4 57 | STATE_RENEWING = 5 58 | STATE_REBINDING = 6 59 | STATE_END = 7 60 | 61 | STATES2NAMES = { 62 | STATE_ERROR: 'ERROR', 63 | STATE_PREINIT: 'PREINIT', 64 | STATE_INIT: 'INIT', 65 | STATE_SELECTING: 'SELECTING', 66 | STATE_REQUESTING: 'REQUESTING', 67 | STATE_BOUND: 'BOUND', 68 | STATE_RENEWING: 'RENEWING', 69 | STATE_REBINDING: 'REBINDING', 70 | STATE_END: 'END', 71 | } 72 | 73 | # NM integration 74 | ##################### 75 | REASONS_NM = ['bound', 'renew', 'rebind', 'expiry', 'fail', 'timeout', 76 | 'nak', 'end', 'abend'] 77 | 78 | REASONS_CL = ['MEDIUM', 'PREINIT', 'BOUND', 'RENEW', 'REBIND', 'REBOOT', 79 | 'EXPIRE', 'FAIL', 'STOP', 'RELEASE', 'NBI', 'TIMEOUT'] 80 | 81 | STATES2REASONS = { 82 | STATE_PREINIT: 'PREINIT', 83 | STATE_INIT: 'INIT', 84 | STATE_SELECTING: 'SELECTING', 85 | STATE_BOUND: 'BOUND', 86 | STATE_END: 'END', 87 | STATE_REBINDING: 'REBIND', 88 | STATE_RENEWING: 'RENEW', 89 | STATE_ERROR: "FAIL", 90 | # NOTE: there could be implemented a way toknow the reason for failure so 91 | # that it can be passed to NetworkManager, as dhclient does, ie: 92 | # "STOP", "EXPIRE" 93 | } 94 | 95 | # systemd events 96 | DHCP_EVENTS = { 97 | 'STOP': 0, 98 | 'IP_ACQUIRE': 1, 99 | 'IP_CHANGE': 2, 100 | 'EXPIRED': 3, 101 | 'RENEW': 4, 102 | } 103 | 104 | SCRIPT_ENV_KEYS = ['reason', 'medium', 'interface', 105 | # 'client', 'pid', 106 | 'new_ip_address', 'new_subnet_mask', 'new_network_number', 107 | 'new_domain_name_servers', 'new_domain_name', 'new_routers', 108 | 'new_broadcast_address', 'new_next_server', 109 | 'new_dhcp_server_id'] 110 | 111 | LEASEATTRS_SAMEAS_ENVKEYS = ['interface'] # , 'reason'] 112 | # 'client', 'pid', 113 | # these are not set as environment but put in lease file 114 | # , 'rebind', 'renew', 'expiry' 115 | 116 | LEASEATTRS2ENVKEYS = { 117 | 'address': 'new_ip_address', 118 | 'subnet_mask': 'new_subnet_mask', 119 | 'broadcast_address': 'new_broadcast_address', 120 | 'next_server': 'new_next_server', 121 | 'server_id': 'new_server_id', 122 | 123 | 'network': 'new_network_number', 124 | 'domain': 'new_domain_name', 125 | 'name_server': 'new_domain_name_servers', 126 | 'router': 'new_routers', 127 | } 128 | 129 | LEASE_ATTRS2LEASE_FILE = { 130 | 'interface': 'interface', 131 | 132 | 'address': 'fixed-address', 133 | 134 | 'subnet_mask': 'option subnet-mask', 135 | 'broadcast_address': 'option broadcast-address', 136 | 137 | 'domain': 'option domain-name', 138 | 'name_server': 'option domain-name-servers', 139 | 'router': 'option routers', 140 | 141 | 'lease_time': 'option dhcp-lease-time', 142 | 'rebinding_time': 'option dhcp-rebinding-time', 143 | 'renewal_time': 'option dhcp-renewal-time', 144 | 'server_id': 'option dhcp-server-identifier', 145 | 'renew': 'renew', 146 | 'rebind': 'rebind', 147 | 'expiry': 'expiry', 148 | } 149 | 150 | LEASE_ATTRS2LEASE_LOG = { 151 | 'interface': 'interface', 152 | 153 | 'subnet_mask': 'option subnet_mask', 154 | 'broadcast_address': 'option broadcast_address', 155 | 156 | 'address': 'ip_address', 157 | 158 | 'router': 'option routers', 159 | 'domain': 'option domain_name', 160 | 'name_server': 'option domain_name_servers', 161 | 162 | 'lease_time': 'option dhcp_lease_time', 163 | 'renewal_time': 'option dhcp_renewal_time', 164 | 'rebinding_time': 'option dhcp_rebinding_time', 165 | 'server_id': 'option dhcp_server_identifier', 166 | 'expiry': 'expiry', 167 | } 168 | 169 | ENV_OPTIONS_REQ = { 170 | 'requested_subnet_mask': '1', 171 | 'requested_router': '1', 172 | 'requested_domain_name_server': '1', 173 | 'requested_domain_name': '1', 174 | 'requested_router_discovery': '1', 175 | 'requested_static_route': '1', 176 | 'requested_vendor_specific': '1', 177 | 'requested_netbios_nameserver': '1', 178 | 'requested_netbios_node_type': '1', 179 | 'requested_netbios_scope': '1', 180 | 'requested_classless_static_route_option': '1', 181 | 'requested_private_classless_static_route': '1', 182 | 'requested_private_proxy_autodiscovery': '1', 183 | } 184 | 185 | PRL = b"\x01\x03\x06\x0f\x1f\x21\x2b\x2c\x2e\x2f\x79\xf9\xfc" 186 | """ 187 | SD_DHCP_OPTION_SUBNET_MASK = 1 188 | SD_DHCP_OPTION_ROUTER = 3 189 | SD_DHCP_OPTION_DOMAIN_NAME_SERVER = 6 190 | SD_DHCP_OPTION_DOMAIN_NAME = 15 191 | SD_DHCP_OPTION_ROUTER_DISCOVER = 31 192 | SD_DHCP_OPTION_STATIC_ROUTE = 33 193 | SD_DHCP_OPTION_VENDOR_SPECIFIC = 43 194 | SD_DHCP_OPTION_NETBIOS_NAMESERVER = 44 195 | SD_DHCP_OPTION_NETBIOS_NODETYPE = 46 196 | SD_DHCP_OPTION_NETBIOS_SCOPE = 47 197 | SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE = 121 198 | SD_DHCP_OPTION_PRIVATE_CLASSLESS_STATIC_ROUTE = 249 199 | SD_DHCP_OPTION_PRIVATE_PROXY_AUTODISCOVERY = 252 200 | """ 201 | 202 | FSM_ATTRS = ['request_attempts', 'discover_attempts', 'script', 203 | 'time_sent_request', 'current_state', 'client'] 204 | 205 | XID_MIN = 1 206 | XID_MAX = 900000000 207 | 208 | SCRIPT_PATH = '/sbin/dhcpcanon-script' 209 | PID_PATH = '/var/run/dhcpcanon.pid' 210 | LEASE_PATH = '/var/lib/dhcp/dhcpcanon.leases' 211 | CONF_PATH = '/etc/dhcp/dhcpcanon.conf' 212 | RESOLVCONF = '/sbin/resolvconf' 213 | RESOLVCONF_ADMIN = '/usr/bin/resolvconf-admin' 214 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM SPDX-FileCopyrightText: 2016, juga 4 | REM SPDX-License-Identifier: MIT 5 | 6 | REM Command file for Sphinx documentation 7 | 8 | if "%SPHINXBUILD%" == "" ( 9 | set SPHINXBUILD=sphinx-build 10 | ) 11 | set BUILDDIR=build 12 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 13 | set I18NSPHINXOPTS=%SPHINXOPTS% source 14 | if NOT "%PAPER%" == "" ( 15 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 16 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 17 | ) 18 | 19 | if "%1" == "" goto help 20 | 21 | if "%1" == "help" ( 22 | :help 23 | echo.Please use `make ^` where ^ is one of 24 | echo. html to make standalone HTML files 25 | echo. dirhtml to make HTML files named index.html in directories 26 | echo. singlehtml to make a single large HTML file 27 | echo. pickle to make pickle files 28 | echo. json to make JSON files 29 | echo. htmlhelp to make HTML files and a HTML help project 30 | echo. qthelp to make HTML files and a qthelp project 31 | echo. devhelp to make HTML files and a Devhelp project 32 | echo. epub to make an epub 33 | echo. epub3 to make an epub3 34 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 35 | echo. text to make text files 36 | echo. man to make manual pages 37 | echo. texinfo to make Texinfo files 38 | echo. gettext to make PO message catalogs 39 | echo. changes to make an overview over all changed/added/deprecated items 40 | echo. xml to make Docutils-native XML files 41 | echo. pseudoxml to make pseudoxml-XML files for display purposes 42 | echo. linkcheck to check all external links for integrity 43 | echo. doctest to run all doctests embedded in the documentation if enabled 44 | echo. coverage to run coverage check of the documentation if enabled 45 | echo. dummy to check syntax errors of document sources 46 | goto end 47 | ) 48 | 49 | if "%1" == "clean" ( 50 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 51 | del /q /s %BUILDDIR%\* 52 | goto end 53 | ) 54 | 55 | 56 | REM Check if sphinx-build is available and fallback to Python version if any 57 | %SPHINXBUILD% 1>NUL 2>NUL 58 | if errorlevel 9009 goto sphinx_python 59 | goto sphinx_ok 60 | 61 | :sphinx_python 62 | 63 | set SPHINXBUILD=python -m sphinx.__init__ 64 | %SPHINXBUILD% 2> nul 65 | if errorlevel 9009 ( 66 | echo. 67 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 68 | echo.installed, then set the SPHINXBUILD environment variable to point 69 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 70 | echo.may add the Sphinx directory to PATH. 71 | echo. 72 | echo.If you don't have Sphinx installed, grab it from 73 | echo.http://sphinx-doc.org/ 74 | exit /b 1 75 | ) 76 | 77 | :sphinx_ok 78 | 79 | 80 | if "%1" == "html" ( 81 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 82 | if errorlevel 1 exit /b 1 83 | echo. 84 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 85 | goto end 86 | ) 87 | 88 | if "%1" == "dirhtml" ( 89 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 90 | if errorlevel 1 exit /b 1 91 | echo. 92 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 93 | goto end 94 | ) 95 | 96 | if "%1" == "singlehtml" ( 97 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 101 | goto end 102 | ) 103 | 104 | if "%1" == "pickle" ( 105 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished; now you can process the pickle files. 109 | goto end 110 | ) 111 | 112 | if "%1" == "json" ( 113 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can process the JSON files. 117 | goto end 118 | ) 119 | 120 | if "%1" == "htmlhelp" ( 121 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; now you can run HTML Help Workshop with the ^ 125 | .hhp project file in %BUILDDIR%/htmlhelp. 126 | goto end 127 | ) 128 | 129 | if "%1" == "qthelp" ( 130 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 131 | if errorlevel 1 exit /b 1 132 | echo. 133 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 134 | .qhcp project file in %BUILDDIR%/qthelp, like this: 135 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DHCPanonymityprofile.qhcp 136 | echo.To view the help file: 137 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DHCPanonymityprofile.ghc 138 | goto end 139 | ) 140 | 141 | if "%1" == "devhelp" ( 142 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 143 | if errorlevel 1 exit /b 1 144 | echo. 145 | echo.Build finished. 146 | goto end 147 | ) 148 | 149 | if "%1" == "epub" ( 150 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 151 | if errorlevel 1 exit /b 1 152 | echo. 153 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 154 | goto end 155 | ) 156 | 157 | if "%1" == "epub3" ( 158 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 159 | if errorlevel 1 exit /b 1 160 | echo. 161 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 162 | goto end 163 | ) 164 | 165 | if "%1" == "latex" ( 166 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 167 | if errorlevel 1 exit /b 1 168 | echo. 169 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 170 | goto end 171 | ) 172 | 173 | if "%1" == "latexpdf" ( 174 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 175 | cd %BUILDDIR%/latex 176 | make all-pdf 177 | cd %~dp0 178 | echo. 179 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 180 | goto end 181 | ) 182 | 183 | if "%1" == "latexpdfja" ( 184 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 185 | cd %BUILDDIR%/latex 186 | make all-pdf-ja 187 | cd %~dp0 188 | echo. 189 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 190 | goto end 191 | ) 192 | 193 | if "%1" == "text" ( 194 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 195 | if errorlevel 1 exit /b 1 196 | echo. 197 | echo.Build finished. The text files are in %BUILDDIR%/text. 198 | goto end 199 | ) 200 | 201 | if "%1" == "man" ( 202 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 203 | if errorlevel 1 exit /b 1 204 | echo. 205 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 206 | goto end 207 | ) 208 | 209 | if "%1" == "texinfo" ( 210 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 211 | if errorlevel 1 exit /b 1 212 | echo. 213 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 214 | goto end 215 | ) 216 | 217 | if "%1" == "gettext" ( 218 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 222 | goto end 223 | ) 224 | 225 | if "%1" == "changes" ( 226 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 227 | if errorlevel 1 exit /b 1 228 | echo. 229 | echo.The overview file is in %BUILDDIR%/changes. 230 | goto end 231 | ) 232 | 233 | if "%1" == "linkcheck" ( 234 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 235 | if errorlevel 1 exit /b 1 236 | echo. 237 | echo.Link check complete; look for any errors in the above output ^ 238 | or in %BUILDDIR%/linkcheck/output.txt. 239 | goto end 240 | ) 241 | 242 | if "%1" == "doctest" ( 243 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 244 | if errorlevel 1 exit /b 1 245 | echo. 246 | echo.Testing of doctests in the sources finished, look at the ^ 247 | results in %BUILDDIR%/doctest/output.txt. 248 | goto end 249 | ) 250 | 251 | if "%1" == "coverage" ( 252 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 253 | if errorlevel 1 exit /b 1 254 | echo. 255 | echo.Testing of coverage in the sources finished, look at the ^ 256 | results in %BUILDDIR%/coverage/python.txt. 257 | goto end 258 | ) 259 | 260 | if "%1" == "xml" ( 261 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 262 | if errorlevel 1 exit /b 1 263 | echo. 264 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 265 | goto end 266 | ) 267 | 268 | if "%1" == "pseudoxml" ( 269 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 270 | if errorlevel 1 exit /b 1 271 | echo. 272 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 273 | goto end 274 | ) 275 | 276 | if "%1" == "dummy" ( 277 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 278 | if errorlevel 1 exit /b 1 279 | echo. 280 | echo.Build finished. Dummy builder generates no files. 281 | goto end 282 | ) 283 | 284 | :end 285 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016, juga 2 | # SPDX-License-Identifier: MIT 3 | # Makefile for Sphinx documentation 4 | # 5 | 6 | # You can set these variables from the command line. 7 | SPHINXOPTS = 8 | SPHINXBUILD = sphinx-build 9 | PAPER = 10 | BUILDDIR = build 11 | 12 | # Internal variables. 13 | PAPEROPT_a4 = -D latex_paper_size=a4 14 | PAPEROPT_letter = -D latex_paper_size=letter 15 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | # the i18n builder cannot share the environment and doctrees with the others 17 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 18 | # not in auto-generated conf 19 | SOURCEDIR = source 20 | IMAGEDIRS = source/images 21 | BUILDDIRIMAGES = $(BUILDDIR)/html/_images 22 | # generate SVG 23 | PYREVERSE = pyreverse 24 | PYREVERSE_FLAGS = -o svg -p dhcpcanon ../dhcpcanon 25 | # SVG to PDF conversion 26 | SVG2PDF = inkscape 27 | SVG2PDF_FLAGS = 28 | 29 | .PHONY: help 30 | help: 31 | @echo "Please use \`make ' where is one of" 32 | @echo " html to make standalone HTML files" 33 | @echo " dirhtml to make HTML files named index.html in directories" 34 | @echo " singlehtml to make a single large HTML file" 35 | @echo " pickle to make pickle files" 36 | @echo " json to make JSON files" 37 | @echo " htmlhelp to make HTML files and a HTML help project" 38 | @echo " qthelp to make HTML files and a qthelp project" 39 | @echo " applehelp to make an Apple Help Book" 40 | @echo " devhelp to make HTML files and a Devhelp project" 41 | @echo " epub to make an epub" 42 | @echo " epub3 to make an epub3" 43 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 44 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 45 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 46 | @echo " text to make text files" 47 | @echo " man to make manual pages" 48 | @echo " texinfo to make Texinfo files" 49 | @echo " info to make Texinfo files and run them through makeinfo" 50 | @echo " gettext to make PO message catalogs" 51 | @echo " changes to make an overview of all changed/added/deprecated items" 52 | @echo " xml to make Docutils-native XML files" 53 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 54 | @echo " linkcheck to check all external links for integrity" 55 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 56 | @echo " coverage to run coverage check of the documentation (if enabled)" 57 | @echo " dummy to check syntax errors of document sources" 58 | 59 | # Not in auto-generated conf 60 | FSMSVG := python -c "from dhcpcanon.dhcpcapfsm import DHCPCAPFSM; DHCPCAPFSM.graph(target='$(IMAGEDIRS)/dhcpcapfsm.svg')";mkdir -p $(BUILDDIRIMAGES);cp $(IMAGEDIRS)/*.svg $(BUILDDIRIMAGES) 61 | fsmsvg: 62 | @echo "Generating FSM SVG" 63 | $(FSMSVG) 64 | 65 | UMLSVG := $(PYREVERSE) $(PYREVERSE_FLAGS);mv *.svg $(IMAGEDIRS);mkdir -p $(BUILDDIRIMAGES);cp $(IMAGEDIRS)/*.svg $(BUILDDIRIMAGES) 66 | umlsvg: 67 | @echo "Generating UML SVG" 68 | $($UMLSVG) 69 | 70 | # Pattern rule for converting SVG to PDF 71 | %.pdf : %.svg 72 | $(SVG2PDF) -f $< -A $@ 73 | # Build a list of SVG files to convert to PDFs 74 | PDFs := $(foreach dir, $(IMAGEDIRS), $(patsubst %.svg,%.pdf,$(wildcard $(SOURCEDIR)/$(dir)/*.svg))) 75 | # Make a rule to build the PDFs 76 | images: 77 | @echo "Generating PDFs" 78 | $(PDFs) 79 | 80 | .PHONY: clean 81 | clean: 82 | rm -rf $(BUILDDIR)/* 83 | rm -f $(PDFs) 84 | 85 | .PHONY: html 86 | html: 87 | #$(FSMSVG) 88 | #$(UMLSVG) 89 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 90 | @echo 91 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 92 | 93 | .PHONY: dirhtml 94 | dirhtml: 95 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 96 | @echo 97 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 98 | 99 | .PHONY: singlehtml 100 | singlehtml: 101 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 102 | @echo 103 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 104 | 105 | .PHONY: pickle 106 | pickle: 107 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 108 | @echo 109 | @echo "Build finished; now you can process the pickle files." 110 | 111 | .PHONY: json 112 | json: 113 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 114 | @echo 115 | @echo "Build finished; now you can process the JSON files." 116 | 117 | .PHONY: htmlhelp 118 | htmlhelp: 119 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 120 | @echo 121 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 122 | ".hhp project file in $(BUILDDIR)/htmlhelp." 123 | 124 | .PHONY: qthelp 125 | qthelp: 126 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 127 | @echo 128 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 129 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 130 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DHCPanonymityprofile.qhcp" 131 | @echo "To view the help file:" 132 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DHCPanonymityprofile.qhc" 133 | 134 | .PHONY: applehelp 135 | applehelp: 136 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 137 | @echo 138 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 139 | @echo "N.B. You won't be able to view it unless you put it in" \ 140 | "~/Library/Documentation/Help or install it in your application" \ 141 | "bundle." 142 | 143 | .PHONY: devhelp 144 | devhelp: 145 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 146 | @echo 147 | @echo "Build finished." 148 | @echo "To view the help file:" 149 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DHCPanonymityprofile" 150 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DHCPanonymityprofile" 151 | @echo "# devhelp" 152 | 153 | .PHONY: epub 154 | epub: 155 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 156 | @echo 157 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 158 | 159 | .PHONY: epub3 160 | epub3: 161 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 162 | @echo 163 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 164 | 165 | .PHONY: latex 166 | latex: 167 | $(FSMSVG) 168 | #$(UMLSVG) 169 | $(PDFs) 170 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 171 | @echo 172 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 173 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 174 | "(use \`make latexpdf' here to do that automatically)." 175 | 176 | .PHONY: latexpdf 177 | latexpdf: 178 | $(FSMSVG) 179 | #$(UMLSVG) 180 | $(PDFs) 181 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 182 | @echo "Running LaTeX files through pdflatex..." 183 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 184 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 185 | 186 | .PHONY: latexpdfja 187 | latexpdfja: 188 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 189 | @echo "Running LaTeX files through platex and dvipdfmx..." 190 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 191 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 192 | 193 | .PHONY: text 194 | text: 195 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 196 | @echo 197 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 198 | 199 | .PHONY: man 200 | man: 201 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 202 | @echo 203 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 204 | 205 | .PHONY: texinfo 206 | texinfo: 207 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 208 | @echo 209 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 210 | @echo "Run \`make' in that directory to run these through makeinfo" \ 211 | "(use \`make info' here to do that automatically)." 212 | 213 | .PHONY: info 214 | info: 215 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 216 | @echo "Running Texinfo files through makeinfo..." 217 | make -C $(BUILDDIR)/texinfo info 218 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 219 | 220 | .PHONY: gettext 221 | gettext: 222 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 223 | @echo 224 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 225 | 226 | .PHONY: changes 227 | changes: 228 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 229 | @echo 230 | @echo "The overview file is in $(BUILDDIR)/changes." 231 | 232 | .PHONY: linkcheck 233 | linkcheck: 234 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 235 | @echo 236 | @echo "Link check complete; look for any errors in the above output " \ 237 | "or in $(BUILDDIR)/linkcheck/output.txt." 238 | 239 | .PHONY: doctest 240 | doctest: 241 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 242 | @echo "Testing of doctests in the sources finished, look at the " \ 243 | "results in $(BUILDDIR)/doctest/output.txt." 244 | 245 | .PHONY: coverage 246 | coverage: 247 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 248 | @echo "Testing of coverage in the sources finished, look at the " \ 249 | "results in $(BUILDDIR)/coverage/python.txt." 250 | 251 | .PHONY: xml 252 | xml: 253 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 254 | @echo 255 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 256 | 257 | .PHONY: pseudoxml 258 | pseudoxml: 259 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 260 | @echo 261 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 262 | 263 | .PHONY: dummy 264 | dummy: 265 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 266 | @echo 267 | @echo "Build finished. Dummy builder generates no files." 268 | -------------------------------------------------------------------------------- /man/dhcpcanon-script.8: -------------------------------------------------------------------------------- 1 | .\" dhcpcanon-script.8 2 | .\" 3 | .\" Copyright (c) 2012,2014,2016 by Internet Systems Consortium, Inc. ("ISC") 4 | .\" Copyright (c) 2009-2010 by Internet Systems Consortium, Inc. ("ISC") 5 | .\" Copyright (c) 2004-2005 by Internet Systems Consortium, Inc. ("ISC") 6 | .\" Copyright (c) 1996-2003 by Internet Software Consortium 7 | .\" Copyright (c) 2017 juga 8 | .\" SPDX-License-Identifier: MIT 9 | .\" 10 | .\" Permission to use, copy, modify, and distribute this software for any 11 | .\" purpose with or without fee is hereby granted, provided that the above 12 | .\" copyright notice and this permission notice appear in all copies. 13 | .\" 14 | .\" THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES 15 | .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16 | .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR 17 | .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18 | .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19 | .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 20 | .\" OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | .\" 22 | .\" Internet Systems Consortium, Inc. 23 | .\" 950 Charter Street 24 | .\" Redwood City, CA 94063 25 | .\" 26 | .\" https://www.isc.org/ 27 | .\" 28 | .\" Support and other services are available for ISC products - see 29 | .\" https://www.isc.org for more information or to learn more about ISC. 30 | .\" 31 | .\" $Id: dhcpcanon-script.8,v 1.14 2010/07/02 23:09:14 sar Exp $ 32 | .\" 33 | .TH dhcpcanon-script 8 34 | .SH NAME 35 | dhcpcanon-script - DHCP client network configuration script 36 | .SH DESCRIPTION 37 | The DHCP client network configuration script is invoked from time to 38 | time by \fBdhcpcanon(8)\fR. This script is used by the dhcp client to 39 | set each interface's initial configuration prior to requesting an 40 | address, to test the address once it has been offered, and to set the 41 | interface's final configuration once a lease has been acquired. If no 42 | lease is acquired, the script is used to test predefined leases, if 43 | any, and also called once if no valid lease can be identified. 44 | .PP 45 | This script is not meant to be customized by the end user. If local 46 | customizations are needed, they should be possible using the enter and 47 | exit hooks provided (see HOOKS for details). These hooks will allow the 48 | user to override the default behaviour of the client in creating a 49 | .B /etc/resolv.conf 50 | file. 51 | .PP 52 | No standard client script exists for some operating systems, even though 53 | the actual client may work, so a pioneering user may well need to create 54 | a new script or modify an existing one. 55 | .SH HOOKS 56 | When it starts, the client script first defines a shell function, 57 | .B make_resolv_conf , 58 | which is later used to create the 59 | .B /etc/resolv.conf 60 | file. To override the default behaviour, redefine this function in 61 | the enter hook script. 62 | .PP 63 | On after defining the make_resolv_conf function, the client script checks 64 | for the presence of an executable 65 | .B ETCDIR/dhcpcanon-enter-hooks 66 | script, and if present, it invokes the script inline, using the Bourne 67 | shell \'.\' command. The entire environment documented under OPERATION 68 | is available to this script, which may modify the environment if needed 69 | to change the behaviour of the script. If an error occurs during the 70 | execution of the script, it can set the exit_status variable to a nonzero 71 | value, and 72 | .B CLIENTBINDIR/dhcpcanon-script 73 | will exit with that error code immediately after the client script exits. 74 | .PP 75 | After all processing has completed, 76 | .B CLIENTBINDIR/dhcpcanon-script 77 | checks for the presence of an executable 78 | .B ETCDIR/dhcpcanon-exit-hooks 79 | script, which if present is invoked using the \'.\' command. The exit 80 | status of dhcpcanon-script will be passed to dhcpcanon-exit-hooks in the 81 | exit_status shell variable, and will always be zero if the script 82 | succeeded at the task for which it was invoked. The rest of the 83 | environment as described previously for dhcpcanon-enter-hooks is also 84 | present. The 85 | .B ETCDIR/dhcpcanon-exit-hooks 86 | script can modify the valid of exit_status to change the exit status 87 | of dhcpcanon-script. 88 | .SH OPERATION 89 | When dhcpcanon needs to invoke the client configuration script, it 90 | defines a set of variables in the environment, and then invokes 91 | .B CLIENTBINDIR/dhcpcanon-script. 92 | In all cases, $reason is set to the name of the reason why the script 93 | has been invoked. The following reasons are currently defined: 94 | MEDIUM, PREINIT, BOUND, RENEW, REBIND, REBOOT, EXPIRE, FAIL, STOP, RELEASE, 95 | NBI and TIMEOUT. 96 | .PP 97 | .SH MEDIUM 98 | The DHCP client is requesting that an interface's media type 99 | be set. The interface name is passed in $interface, and the media 100 | type is passed in $medium. 101 | .SH PREINIT 102 | The DHCP client is requesting that an interface be configured as 103 | required in order to send packets prior to receiving an actual 104 | address. For clients which use the BSD socket library, this means 105 | configuring the interface with an IP address of 0.0.0.0 and a 106 | broadcast address of 255.255.255.255. For other clients, it may be 107 | possible to simply configure the interface up without actually giving 108 | it an IP address at all. The interface name is passed in $interface, 109 | and the media type in $medium. 110 | .PP 111 | The DHCP client has done an initial binding to a new address. The 112 | new ip address is passed in $new_ip_address, and the interface name is 113 | passed in $interface. The media type is passed in $medium. Any 114 | options acquired from the server are passed using the option name 115 | described in \fBdhcp-options\fR, except that dashes (\'-\') are replaced 116 | by underscores (\'_\') in order to make valid shell variables, and the 117 | variable names start with new_. So for example, the new subnet mask 118 | would be passed in $new_subnet_mask. Options from a non-default 119 | universe will have the universe name prepended to the option name, for 120 | example $new_dhcp6_server_id. The options that the client 121 | explicitly requested via a PRL or ORO option are passed with the same 122 | option name as above but prepended with requested_ and with a value of 1, 123 | for example requested_subnet_mask=1. No such variable is defined for 124 | options not requested by the client or options that don't require a 125 | request option, such as the ip address (*_ip_address) or expiration 126 | time (*_expire). 127 | .PP 128 | Before actually configuring the address, dhcpcanon-script should 129 | somehow ARP for it and exit with a nonzero status if it receives a 130 | reply. In this case, the client will send a DHCPDECLINE message to 131 | the server and acquire a different address. This may also be done in 132 | the RENEW, REBIND, or REBOOT states, but is not required, and indeed 133 | may not be desirable. 134 | .PP 135 | When a binding has been completed, a lot of network parameters are 136 | likely to need to be set up. A new /etc/resolv.conf needs to be 137 | created, using the values of $new_domain_name and 138 | $new_domain_name_servers (which may list more than one server, 139 | separated by spaces). A default route should be set using 140 | $new_routers, and static routes may need to be set up using 141 | $new_static_routes. 142 | .PP 143 | If an IP alias has been declared, it must be set up here. The alias 144 | IP address will be written as $alias_ip_address, and other DHCP 145 | options that are set for the alias (e.g., subnet mask) will be passed 146 | in variables named as described previously except starting with 147 | $alias_ instead of $new_. Care should be taken that the alias IP 148 | address not be used if it is identical to the bound IP address 149 | ($new_ip_address), since the other alias parameters may be incorrect 150 | in this case. 151 | .SH RENEW 152 | When a binding has been renewed, the script is called as in BOUND, 153 | except that in addition to all the variables starting with $new_, and 154 | $requested_ there is another set of variables starting with $old_. 155 | Persistent settings that may have changed need to be deleted - for 156 | example, if a local route to the bound address is being configured, 157 | the old local route should be deleted. If the default route has changed, 158 | the old default route should be deleted. If the static routes have changed, 159 | the old ones should be deleted. Otherwise, processing can be done as with 160 | BOUND. 161 | .SH REBIND 162 | The DHCP client has rebound to a new DHCP server. This can be handled 163 | as with RENEW, except that if the IP address has changed, the ARP 164 | table should be cleared. 165 | .SH REBOOT 166 | The DHCP client has successfully reacquired its old address after a 167 | reboot. This can be processed as with BOUND. 168 | .SH EXPIRE 169 | The DHCP client has failed to renew its lease or acquire a new one, 170 | and the lease has expired. The IP address must be relinquished, and 171 | all related parameters should be deleted, as in RENEW and REBIND. 172 | .SH FAIL 173 | The DHCP client has been unable to contact any DHCP servers, and any 174 | leases that have been tested have not proved to be valid. The 175 | parameters from the last lease tested should be deconfigured. This 176 | can be handled in the same way as EXPIRE. 177 | .SH STOP 178 | The dhcpcanon has been informed to shut down gracefully, the 179 | dhcpcanon-script should unconfigure or shutdown the interface as 180 | appropriate. 181 | .SH RELEASE 182 | The dhcpcanon has been executed using the -r flag, indicating that the 183 | administrator wishes it to release its lease(s). dhcpcanon-script should 184 | unconfigure or shutdown the interface. 185 | .SH NBI 186 | No-Broadcast-Interfaces...dhcpcanon was unable to find any interfaces 187 | upon which it believed it should commence DHCP. What dhcpcanon-script 188 | should do in this situation is entirely up to the implementor. 189 | .SH TIMEOUT 190 | .PP 191 | The usual way to test a lease is to set up the network as with REBIND 192 | (since this may be called to test more than one lease) and then ping 193 | the first router defined in $routers. If a response is received, the 194 | lease must be valid for the network to which the interface is 195 | currently connected. It would be more complete to try to ping all of 196 | the routers listed in $new_routers, as well as those listed in 197 | $new_static_routes, but current scripts do not do this. 198 | .SH FILES 199 | Each operating system should generally have its own script file, 200 | although the script files for similar operating systems may be similar 201 | or even identical. The script files included in Internet 202 | Systems Consortium DHCP distribution appear in the distribution tree 203 | under client/scripts, and bear the names of the operating systems on 204 | which they are intended to work. 205 | .SH BUGS 206 | If more than one interface is being used, there's no obvious way to 207 | avoid clashes between server-supplied configuration parameters - for 208 | example, the stock dhcpcanon-script rewrites /etc/resolv.conf. If 209 | more than one interface is being configured, /etc/resolv.conf will be 210 | repeatedly initialized to the values provided by one server, and then 211 | the other. Assuming the information provided by both servers is 212 | valid, this shouldn't cause any real problems, but it could be 213 | confusing. 214 | .SH SEE ALSO 215 | dhcpcanon(8). 216 | .SH AUTHOR 217 | .B dhcpcanon-script(8) 218 | To learn more about Internet Systems Consortium, 219 | see 220 | .B https://www.isc.org. 221 | -------------------------------------------------------------------------------- /dhcpcanon/dhcpcap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:ts=4:sw=4:expandtab 2 3 | # Copyright 2016, 2017 juga (juga at riseup dot net), MIT license. 4 | """Client class for the DHCP client implementation of the Anonymity Profile 5 | ([:rfc:`7844`]).""" 6 | from __future__ import absolute_import 7 | 8 | # from __future__ import unicode_literals 9 | import logging 10 | 11 | import attr 12 | from netaddr import IPNetwork 13 | from scapy.arch import get_if_raw_hwaddr 14 | from scapy.config import conf 15 | from scapy.layers.dhcp import BOOTP, DHCP 16 | from scapy.layers.inet import IP, UDP 17 | from scapy.layers.l2 import Ether 18 | from scapy.utils import mac2str, str2mac 19 | 20 | from .constants import (BROADCAST_ADDR, BROADCAST_MAC, CLIENT_PORT, 21 | DHCP_EVENTS, DHCP_OFFER_OPTIONS, META_ADDR, 22 | SERVER_PORT, PRL) 23 | from .dhcpcaputils import gen_xid 24 | from .dhcpcaplease import DHCPCAPLease 25 | 26 | logger = logging.getLogger('dhcpcanon') 27 | 28 | 29 | @attr.s 30 | class DHCPCAP(object): 31 | """.""" 32 | iface = attr.ib(default=None) 33 | 34 | client_mac = attr.ib(default=None) 35 | client_ip = attr.ib(default=META_ADDR) 36 | client_port = attr.ib(default=CLIENT_PORT) 37 | 38 | server_mac = attr.ib(default=BROADCAST_MAC) 39 | server_ip = attr.ib(default=BROADCAST_ADDR) 40 | server_port = attr.ib(default=SERVER_PORT) 41 | 42 | lease = attr.ib(default=attr.Factory(DHCPCAPLease)) 43 | event = attr.ib(default=None) 44 | prl = attr.ib(default=None) 45 | xid = attr.ib(default=None) 46 | 47 | def __attrs_post_init__(self): 48 | """Initializes attributes after attrs __init__. 49 | 50 | These attributes do not change during the life of the object. 51 | 52 | """ 53 | logger.debug('Creating new DHCPCAP obj.') 54 | if self.iface is None: 55 | self.iface = conf.iface 56 | if self.client_mac is None: 57 | _, client_mac = get_if_raw_hwaddr(self.iface) 58 | self.client_mac = str2mac(client_mac) 59 | if self.prl is None: 60 | self.prl = PRL 61 | if self.xid is None: 62 | self.xid = gen_xid() 63 | logger.debug('Modifying Lease obj, setting iface.') 64 | self.lease.interface = self.iface 65 | 66 | def gen_ether_ip(self): 67 | """Generates link layer and IP layer part of DHCP packet. 68 | 69 | For broadcast packets is: 70 | Ether(src=client_mac, dst="ff:ff:ff:ff:ff:ff") / 71 | IP(src="0.0.0.0", dst="255.255.255.255") / 72 | 73 | """ 74 | ether_ip = (Ether(src=self.client_mac, dst=BROADCAST_MAC) / 75 | IP(src=META_ADDR, dst=BROADCAST_ADDR)) 76 | return ether_ip 77 | 78 | def gen_ether_ip_unicast(self): 79 | """Generates link layer and IP layer part of DHCP packet. 80 | 81 | For unicast packets is: 82 | Ether(src=client_mac, dst=server_mac) / 83 | IP(src=client_ip?, dst=server_ip) / 84 | 85 | """ 86 | ether_ip = (Ether(src=self.client_mac, dst=self.server_mac) / 87 | IP(src=self.client_ip, dst=self.server_ip)) 88 | return ether_ip 89 | 90 | def gen_udp(self): 91 | """Generates UDP layer part of DHCP packet. 92 | 93 | UDP layer is always: 94 | UDP(sport=68, dport=67) / 95 | 96 | """ 97 | udp = (UDP(sport=self.client_port, dport=self.server_port)) 98 | return udp 99 | 100 | def gen_bootp(self): 101 | """Generates BOOTP layer part of DHCP packet. 102 | 103 | [ :rfc:`7844#section-3.4` ] :: 104 | 105 | The presence of this address is necessary for the proper operation 106 | of the DHCP service. 107 | 108 | [:rfc:`7844#section-3.`] :: 109 | 110 | MAY contain the Client Identifier option, 111 | 112 | """ 113 | bootp = ( 114 | BOOTP(chaddr=[mac2str(self.client_mac)], xid=self.xid) 115 | ) 116 | return bootp 117 | 118 | def gen_bootp_unicast(self): 119 | """Generates BOOTP layer part of unicast DHCP packet. 120 | 121 | Same comments as in gen_bootp 122 | 123 | """ 124 | bootp = ( 125 | BOOTP(chaddr=[mac2str(self.client_mac)], xid=self.xid, 126 | ciaddr=self.client_ip) 127 | ) 128 | return bootp 129 | 130 | def gen_discover(self): 131 | """ 132 | Generate DHCP DISCOVER packet. 133 | 134 | [:rfc:`7844#section-3.1`] :: 135 | 136 | SHOULD randomize the ordering of options 137 | 138 | If this can not be implemented 139 | MAY order the options by option code number (lowest to highest). 140 | 141 | [:rfc:`7844#section-3.`] :: 142 | 143 | MAY contain the Parameter Request List option. 144 | 145 | """ 146 | dhcp_discover = ( 147 | self.gen_ether_ip() / 148 | self.gen_udp() / 149 | self.gen_bootp() / 150 | DHCP(options=[ 151 | ("message-type", "discover"), 152 | ("client_id", mac2str(self.client_mac)), 153 | ("param_req_list", self.prl), 154 | "end" 155 | ]) 156 | ) 157 | logger.debug('Generated discover %s.', dhcp_discover.summary()) 158 | return dhcp_discover 159 | 160 | def gen_request(self): 161 | """ 162 | Generate DHCP REQUEST packet. 163 | 164 | [:rfc:`7844#section-3.1`] :: 165 | 166 | SHOULD randomize the ordering of options 167 | 168 | If this can not be implemented 169 | MAY order the options by option code number (lowest to highest). 170 | 171 | [:rfc:`7844#section-3.`] :: 172 | 173 | MAY contain the Parameter Request List option. 174 | 175 | If in response to a DHCPOFFER,:: 176 | 177 | MUST contain the corresponding Server Identifier option 178 | MUST contain the Requested IP address option. 179 | 180 | If the message is not in response to a DHCPOFFER (BOUND, RENEW),:: 181 | MAY contain a Requested IP address option 182 | 183 | """ 184 | dhcp_req = ( 185 | self.gen_ether_ip() / 186 | self.gen_udp() / 187 | self.gen_bootp() / 188 | DHCP(options=[ 189 | ("message-type", "request"), 190 | ("client_id", mac2str(self.client_mac)), 191 | ("param_req_list", self.prl), 192 | ("requested_addr", self.lease.address), 193 | ("server_id", self.lease.server_id), 194 | "end"]) 195 | ) 196 | logger.debug('Generated request %s.', dhcp_req.summary()) 197 | return dhcp_req 198 | 199 | def gen_request_unicast(self): 200 | """ 201 | Generate DHCP REQUEST unicast packet. 202 | 203 | Same comments as in gen_request apply. 204 | 205 | """ 206 | dhcp_req = ( 207 | self.gen_ether_ip_unicast() / 208 | self.gen_udp() / 209 | self.gen_bootp_unicast() / 210 | DHCP(options=[ 211 | ("message-type", "request"), 212 | ("client_id", mac2str(self.client_mac)), 213 | ("param_req_list", self.prl), 214 | "end"]) 215 | ) 216 | logger.debug('Generated request %s.', dhcp_req.summary()) 217 | return dhcp_req 218 | 219 | def gen_decline(self): 220 | """ 221 | Generate DHCP decline packet (broadcast). 222 | 223 | [:rfc:`7844#section-3.`] :: 224 | 225 | MUST contain the Message Type option, 226 | MUST contain the Server Identifier option, 227 | MUST contain the Requested IP address option; 228 | 229 | .. note:: currently not being used. 230 | 231 | """ 232 | dhcp_decline = ( 233 | self.gen_ether_ip() / 234 | self.gen_udp() / 235 | self.gen_bootp() / 236 | DHCP(options=[ 237 | ("message-type", "decline"), 238 | ("server_id", self.server_ip), 239 | ("requested_addr", self.client_ip), 240 | "end"]) 241 | ) 242 | logger.debug('Generated decline.') 243 | logger.debug(dhcp_decline.summary()) 244 | return dhcp_decline 245 | 246 | def gen_release(self): 247 | """ 248 | Generate DHCP release packet (broadcast?). 249 | 250 | [:rfc:`7844#section-3.`] :: 251 | 252 | MUST contain the Message Type option and 253 | MUST contain the Server Identifier option, 254 | 255 | .. note:: currently not being used. 256 | 257 | """ 258 | dhcp_release = ( 259 | self.gen_ether_ip() / 260 | self.gen_udp() / 261 | self.gen_bootp() / 262 | DHCP(options=[ 263 | ("message-type", "release"), 264 | ("client_id", mac2str(self.client_mac)), 265 | ("server_id", self.server_ip), 266 | "end"]) 267 | ) 268 | logger.debug('Generated release.') 269 | logger.debug(dhcp_release.summary()) 270 | return dhcp_release 271 | 272 | def gen_inform(self): 273 | """ 274 | Generate DHCP inform packet (unicast). 275 | 276 | [:rfc:`7844#section-3.`] :: 277 | 278 | MUST contain the Message Type option, 279 | 280 | .. note:: currently not being used. 281 | 282 | """ 283 | dhcp_inform = ( 284 | self.gen_ether_ip_unicast() / 285 | self.gen_udp() / 286 | self.gen_bootp_unicast() / 287 | DHCP(options=[ 288 | ("message-type", "inform"), 289 | ("client_id", mac2str(self.client_mac)), 290 | "end"]) 291 | ) 292 | logger.debug('Generated inform.') 293 | logger.debug(dhcp_inform.summary()) 294 | return dhcp_inform 295 | 296 | def gen_check_lease_attrs(self, attrs_dict): 297 | """Generate network mask in CIDR format and subnet. 298 | 299 | Validate the given arguments. Otherwise AddrFormatError exception 300 | will be raised and catched in the FSM. 301 | 302 | """ 303 | # without some minimal options given by the server, is not possible 304 | # to create new lease 305 | assert attrs_dict['subnet_mask'] 306 | assert attrs_dict['address'] 307 | # if address and/or network are not valid this will raise an exception 308 | # (AddrFormatError) 309 | ipn = IPNetwork(attrs_dict['address'] + '/' + 310 | attrs_dict['subnet_mask']) 311 | # FIXME:70 should be this option required? 312 | # assert attrs_dict['server_id'] 313 | if attrs_dict.get('server_id') is None: 314 | attrs_dict['server_id'] = self.server_ip 315 | # TODO: there should be more complex checking here about getting an 316 | # address in a subnet? 317 | # else: 318 | # if IPAddress('server_id') not in ipn: 319 | # raise ValueError("server_id is not in the same network as" 320 | # "the offered address.") 321 | if attrs_dict.get('router') is None: 322 | attrs_dict['router'] = attrs_dict['server_id'] 323 | ripn = IPNetwork(attrs_dict['router'] + '/' + 324 | attrs_dict['subnet_mask']) 325 | assert ripn.network == ipn.network 326 | # set the options that are not given by the server 327 | attrs_dict['subnet_mask_cidr'] = str(ipn.prefixlen) 328 | attrs_dict['subnet'] = str(ipn.network) 329 | # check other options that might not be given by the server 330 | if attrs_dict.get('broadcast_address') is None: 331 | attrs_dict['broadcast_address'] = str(ipn.broadcast) 332 | if attrs_dict.get('name_server') is None: 333 | attrs_dict['name_server'] = attrs_dict['server_id'] 334 | if attrs_dict.get('next_server') is None: 335 | attrs_dict['next_server'] = attrs_dict['server_id'] 336 | logger.debug('Net values are valid') 337 | return attrs_dict 338 | 339 | def handle_offer_ack(self, pkt, time_sent_request=None): 340 | """Create a lease object with the values in OFFER/ACK packet.""" 341 | attrs_dict = dict() 342 | for opt in pkt[DHCP].options: 343 | if isinstance(opt, tuple) and opt[0] in DHCP_OFFER_OPTIONS: 344 | v = opt[1] if len(opt[1:]) < 2 else ' '.join(opt[1:]) 345 | v = str(v.decode('utf8')) if isinstance(v, bytes) else str(v) 346 | attrs_dict[opt[0]] = v 347 | attrs_dict.update({ 348 | "interface": self.iface, 349 | "address": pkt[BOOTP].yiaddr, 350 | "next_server": pkt[BOOTP].siaddr, 351 | }) 352 | # this function changes the dict 353 | self.gen_check_lease_attrs(attrs_dict) 354 | logger.debug('Creating Lease obj.') 355 | logger.debug('with attrs %s', attrs_dict) 356 | lease = DHCPCAPLease(**attrs_dict) 357 | return lease 358 | 359 | def handle_offer(self, pkt): 360 | """.""" 361 | logger.debug("Handling Offer.") 362 | logger.debug('Modifying obj DHCPCAP, setting lease.') 363 | self.lease = self.handle_offer_ack(pkt) 364 | 365 | def handle_ack(self, pkt, time_sent_request): 366 | """.""" 367 | logger.debug("Handling ACK.") 368 | logger.debug('Modifying obj DHCPCAP, setting server data.') 369 | self.server_mac = pkt[Ether].src 370 | self.server_ip = pkt[IP].src 371 | self.server_port = pkt[UDP].sport 372 | event = DHCP_EVENTS['IP_ACQUIRE'] 373 | # FIXME:0 check the fields match the previously offered ones? 374 | # FIXME:50 create a new object also on renewing/rebinding 375 | # or only set_times? 376 | lease = self.handle_offer_ack(pkt, time_sent_request) 377 | lease.set_times(time_sent_request) 378 | if self.lease is not None: 379 | if (self.lease.address != lease.address or 380 | self.lease.subnet_mask != lease.subnet_mask or 381 | self.lease.router != lease.router): 382 | event = DHCP_EVENTS['IP_CHANGE'] 383 | else: 384 | event = DHCP_EVENTS['RENEW'] 385 | logger.debug('Modifying obj DHCPCAP, setting lease, client ip, event.') 386 | self.lease = lease 387 | self.client_ip = self.lease.address 388 | self.event = event 389 | return event 390 | -------------------------------------------------------------------------------- /docs/source/images/dhcpcapfsm.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | Automaton_metaclass 11 | 12 | 13 | INIT 14 | 15 | INIT 16 | 17 | 18 | SELECTING 19 | 20 | SELECTING 21 | 22 | 23 | INIT->SELECTING 24 | 25 | 26 | timeout_delay_before_selecting/10.0s 27 | >[action_transmit_discover] 28 | 29 | 30 | END 31 | 32 | END 33 | 34 | 35 | ERROR 36 | 37 | ERROR 38 | 39 | 40 | ERROR->END 41 | 42 | 43 | 44 | 45 | REBINDING 46 | 47 | REBINDING 48 | 49 | 50 | REBINDING->INIT 51 | 52 | 53 | receive_nak_rebinding 54 | 55 | 56 | REBINDING->REBINDING 57 | 58 | 59 | timeout_request_rebinding/75600.0s 60 | >[action_transmit_request] 61 | 62 | 63 | BOUND 64 | 65 | BOUND 66 | 67 | 68 | REBINDING->BOUND 69 | 70 | 71 | receive_ack_rebinding 72 | >[on_ack_requesting] 73 | 74 | 75 | RENEWING 76 | 77 | RENEWING 78 | 79 | 80 | BOUND->RENEWING 81 | 82 | 83 | renewing_time_expires/604800.0s 84 | >[action_transmit_request] 85 | 86 | 87 | SELECTING->ERROR 88 | 89 | 90 | timeout_selecting/60.0s 91 | >[action_transmit_discover] 92 | 93 | 94 | SELECTING->SELECTING 95 | 96 | 97 | receive_offer 98 | >[action_transmit_request] 99 | 100 | 101 | SELECTING->SELECTING 102 | 103 | 104 | timeout_selecting/60.0s 105 | >[action_transmit_discover] 106 | 107 | 108 | REQUESTING 109 | 110 | REQUESTING 111 | 112 | 113 | SELECTING->REQUESTING 114 | 115 | 116 | receive_offer 117 | >[action_transmit_request] 118 | 119 | 120 | SELECTING->REQUESTING 121 | 122 | 123 | timeout_selecting/60.0s 124 | >[action_transmit_discover] 125 | 126 | 127 | REQUESTING->INIT 128 | 129 | 130 | receive_nak_requesting 131 | 132 | 133 | REQUESTING->ERROR 134 | 135 | 136 | timeout_requesting/60.0s 137 | >[action_transmit_request] 138 | 139 | 140 | REQUESTING->BOUND 141 | 142 | 143 | receive_ack_requesting 144 | >[on_ack_requesting] 145 | 146 | 147 | REQUESTING->REQUESTING 148 | 149 | 150 | timeout_requesting/60.0s 151 | >[action_transmit_request] 152 | 153 | 154 | RENEWING->INIT 155 | 156 | 157 | receive_nak_renewing 158 | 159 | 160 | RENEWING->REBINDING 161 | 162 | 163 | rebinding_time_expires/1058400.0s 164 | >[action_transmit_request] 165 | 166 | 167 | RENEWING->BOUND 168 | 169 | 170 | receive_ack_renewing 171 | >[on_renewing] 172 | 173 | 174 | RENEWING->RENEWING 175 | 176 | 177 | timeout_request_renewing/226800.0s 178 | >[action_transmit_request] 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/source/specification.rst: -------------------------------------------------------------------------------- 1 | .. _specification: 2 | 3 | RFC7844 DHCPv4 restricted version summary, questions and ``dhcpcanon`` specification 4 | ====================================================================================== 5 | 6 | This document is a more restrictive version summary of [:rfc:`7844`], 7 | where the keywords (``key words`` [:rfc:`2119`]) commented in 8 | `RFC7844 comments `_ 9 | are actually replaced. Use ``diff`` to see specific differences between these 10 | two documents. 11 | 12 | See :ref:`questions` for a summary of the questions stated here. 13 | 14 | .. note:: 15 | 16 | * Extracts from the [:rfc:`7844`] marked as 17 | `literal blocks `_. 18 | * Replacements are marked as 19 | `parsed literal `_ 20 | with the keyword replaced in bold 21 | 22 | 23 | Message types 24 | ----------------- 25 | 26 | .. note:: 27 | 28 | See :ref:`implementation` for a summary of the messages implementation 29 | 30 | DHCP* 31 | ~~~~~~ 32 | [:rfc:`7844#section-3.1`]:: 33 | 34 | SHOULD randomize the ordering of options 35 | 36 | .. parsed-literal:: 37 | 38 | If this can not be implemented 39 | **MUST** order the options by option code number (lowest to highest). 40 | 41 | 42 | DHCPDISCOVER 43 | ~~~~~~~~~~~~~ 44 | [:rfc:`7844#section-3.`]:: 45 | 46 | MUST contain the Message Type option, 47 | 48 | .. parsed-literal:: 49 | 50 | **MUST** NOT contain the Client Identifier option, 51 | 52 | **MUST** NOT contain the Parameter Request List option. 53 | 54 | **MUST** NOT contain any other option. 55 | 56 | 57 | DHCPREQUEST 58 | ~~~~~~~~~~~~~ 59 | [:rfc:`7844#section-3.`]:: 60 | 61 | MUST contain the Message Type option, 62 | 63 | .. parsed-literal:: 64 | 65 | **MUST** NOT contain the Client Identifier option, 66 | 67 | **MUST** NOT contain the Parameter Request List option. 68 | 69 | **MUST** NOT contain any other option. 70 | 71 | :: 72 | 73 | If in response to a DHCPOFFER, 74 | MUST contain the corresponding Server Identifier option 75 | MUST contain the Requested IP address option. 76 | 77 | If the message is not in response to a DHCPOFFER (BOUND, RENEW),: 78 | 79 | .. parsed-literal:: 80 | 81 | **MUST** NOT contain a Requested IP address option 82 | 83 | DHCPDECLINE 84 | ~~~~~~~~~~~~~ 85 | [:rfc:`7844#section-3.`]:: 86 | 87 | MUST contain the Message Type option, 88 | MUST contain the Server Identifier option, 89 | MUST contain the Requested IP address option; 90 | 91 | .. parsed-literal:: 92 | 93 | **MUST** NOT contain the Client Identifier option. 94 | 95 | - is it always broadcast? 96 | 97 | DHCPRELEASE 98 | ~~~~~~~~~~~~~ 99 | [:rfc:`7844#section-3.`] 100 | 101 | To do not leak when the client leaves the network, this message type 102 | **MUST** NOT be implemented. 103 | 104 | In this case, servers might run out of leases, but that is something 105 | that servers should fix decreasing the lease time. 106 | 107 | 108 | DHCPINFORM 109 | ~~~~~~~~~~~~~ 110 | [:rfc:`7844#section-3.`]:: 111 | 112 | MUST contain the Message Type option, 113 | 114 | .. parsed-literal:: 115 | 116 | **MUST** NOT contain the Client Identifier option, 117 | **MUST** NOT contain the Parameter Request List option. 118 | 119 | It **MUST** NOT contain any other option. 120 | 121 | 122 | Message Options 123 | ----------------- 124 | 125 | Client IP address (ciaddr) 126 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 127 | [:rfc:`7844#section-3.2`]:: 128 | 129 | MUST NOT include in the message a Client IP address that has been obtained 130 | with a different link-layer address. 131 | 132 | Requested IP Address Option (code 50) 133 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | [:rfc:`7844#section-3.3`] 135 | 136 | .. parsed-literal:: 137 | 138 | **MUST** NOT use the Requested IP address option in DHCPDISCOVER messages. 139 | 140 | :: 141 | 142 | MUST use the option when mandated (DHCPREQUEST) 143 | 144 | If in INIT-REBOOT: 145 | 146 | .. parsed-literal:: 147 | 148 | **MUST** perform a complete four-way handshake, starting with a DHCPDISCOVER 149 | 150 | - This is like not having INIT-REBOOT state?:: 151 | 152 | If the client can ascertain that this is exactly the same network to which it was previously connected, and if the link-layer address did not change, 153 | MAY issue a DHCPREQUEST to try to reclaim the current address. 154 | 155 | - This is like INIT-REBOOT state? 156 | - Is there a way to know ``if`` the link-layer address changed without leaking the link-layer? 157 | 158 | 159 | Client Hardware Address Field 160 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 161 | [:rfc:`7844#section-3.4`]:: 162 | 163 | If the hardware address is reset to a new randomized value, 164 | 165 | .. parsed-literal:: 166 | 167 | the DHCP client **MUST** use the new randomized value in the DHCP messages 168 | 169 | The client should be restarted when the hardware address changes and 170 | use the current address instead of the permanent one. 171 | 172 | Client Identifier Option (code 61) 173 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 174 | [:rfc:`7844#section-3.5`] 175 | 176 | .. parsed-literal:: 177 | 178 | **MUST** NOT have this option 179 | 180 | In the case that it would have this option because otherwise the server 181 | does not answer to the requests,:: 182 | 183 | DHCP 184 | clients MUST use client identifiers based solely on the link-layer 185 | address that will be used in the underlying connection. 186 | 187 | Parameter Request List Option (PRL) (code 55) 188 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 189 | [:rfc:`7844#section-3.6`] 190 | 191 | .. parsed-literal:: 192 | 193 | **MUST** NOT have this option 194 | 195 | 196 | Host Name option (code 12) 197 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 198 | [:rfc:`7844#section-3.7`] 199 | 200 | .. parsed-literal:: 201 | 202 | **MUST** NOT send the Host Name option. 203 | 204 | 205 | Client FQDN Option (code 81) 206 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 207 | [:rfc:`7844#section-3.8`] 208 | 209 | .. parsed-literal:: 210 | 211 | **MUST** NOT include the Client FQDN option 212 | 213 | 214 | UUID/GUID-Based Client Machine Identifier Option (code 97) 215 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 216 | [:rfc:`7844#section-3.9`]:: 217 | 218 | Nodes visiting untrusted networks MUST NOT send or use the PXE options. 219 | 220 | - And in the hypotetical case that nodes are visiting a "trusted" network, 221 | must this option be included for the PXE to work properly? 222 | 223 | 224 | User and Vendor Class DHCP Options 225 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 226 | [:rfc:`7844#section-3.10`] 227 | 228 | .. parsed-literal:: 229 | 230 | **MUST** NOT use the 231 | 232 | :: 233 | 234 | Vendor-Specific Information option (code 43), the Vendor Class 235 | Identifier option (code 60), the V-I Vendor Class option (code 124), 236 | or the V-I Vendor-Specific Information option (code 125), 237 | 238 | Operational considerations 239 | --------------------------- 240 | 241 | [:rfc:`7844#section-5.`] :: 242 | 243 | Implementers SHOULD provide a way for clients to control when the 244 | anonymity profiles are used and when standard behavior is preferred. 245 | 246 | ``dhcpcanon`` does not currently implement the standard behavior described in 247 | [:rfc:`2131`] in order to keep the implementation simple and 248 | because all existing implementations already implement it 249 | 250 | 251 | Not specified in RFC7844, but in RFC2131 252 | ----------------------------------------- 253 | 254 | Probe the offered IP 255 | ~~~~~~~~~~~~~~~~~~~~~ 256 | [:rfc:`2131#section-2.2`]:: 257 | 258 | the allocating 259 | server SHOULD probe the reused address before allocating the address, 260 | e.g., with an ICMP echo request, and the client SHOULD probe the 261 | newly received address, e.g., with ARP. 262 | 263 | The client SHOULD perform a 264 | check on the suggested address to ensure that the address is not 265 | already in use. For example, if the client is on a network that 266 | supports ARP, the client may issue an ARP request for the suggested 267 | request. When broadcasting an ARP request for the suggested address, 268 | the client must fill in its own hardware address as the sender's 269 | hardware address, and 0 as the sender's IP address, to avoid 270 | confusing ARP caches in other hosts on the same subnet.>> 271 | 272 | The client SHOULD broadcast an ARP 273 | reply to announce the client's new IP address and clear any outdated 274 | ARP cache entries in hosts on the client's subnet. 275 | 276 | - does any implementation issue an ARP request to probe the offered address? 277 | - is it issued after DHCPOFFER and before DHCPREQUEST, 278 | or after DHCPACK and before passing to BOUND state? 279 | 280 | Currently, there is not any probe 281 | 282 | 283 | Retransmission delays 284 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 285 | 286 | Sending DHCPDISCOVER [:rfc:`2131#section-4.4.1`]:: 287 | 288 | The client SHOULD wait a random time between one and ten seconds to 289 | desynchronize the use of DHCP at startup. 290 | 291 | - is the DISCOVER retranmitted in the same way as the REQUEST? 292 | 293 | [:rfc:`2131#section-3.1`]:: 294 | 295 | a client retransmitting as described in section 4.1 might retransmit the 296 | DHCPREQUEST message four times, for a total delay of 60 seconds 297 | 298 | [:rfc:`2131#section-4.4.5`]:: 299 | 300 | In both RENEWING and REBINDING states, 301 | if the client receives no response to its DHCPREQUEST 302 | message, the client SHOULD wait one-half of the remaining 303 | time until T2 (in RENEWING state) and one-half of the 304 | remaining lease time (in REBINDING state), down to a 305 | minimum of 60 seconds, before retransmitting the 306 | DHCPREQUEST message. 307 | 308 | [:rfc:`2131#section-4.1`]:: 309 | 310 | For example, in a 10Mb/sec Ethernet 311 | internetwork, the delay before the first retransmission SHOULD be 4 312 | seconds randomized by the value of a uniform random number chosen 313 | from the range -1 to +1 314 | 315 | Clients with clocks that provide resolution 316 | granularity of less than one second may choose a non-integer 317 | randomization value. 318 | 319 | The delay before the next retransmission SHOULD 320 | be 8 seconds randomized by the value of a uniform number chosen from 321 | the range -1 to +1. 322 | 323 | The retransmission delay SHOULD be doubled with 324 | subsequent retransmissions up to a maximum of 64 seconds. 325 | 326 | - the delay for the next retransmission is calculated with respect to the type 327 | of DHCP message or for the total of DHCP messages sent indendent of the type? 328 | - without this algorithm being mandatory, **it'd be possible to fingerprint the 329 | the implementation depending on the delay of the retransmission** 330 | - how does other implementations do? 331 | 332 | 333 | Selecting offer algorithm 334 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 335 | [:rfc:`2131#section-4.2`]:: 336 | 337 | DHCP clients are free to use any strategy in selecting a DHCP server 338 | among those from which the client receives a DHCPOFFER message. 339 | 340 | client may choose to collect several DHCPOFFER 341 | messages and select the "best" offer. 342 | 343 | If the client receives no acceptable offers, the client 344 | may choose to try another DHCPDISCOVER message. 345 | 346 | - what is a "no acceptable offer"? 347 | - which are the "strategies" to select OFFER implemented? 348 | - different algorithms to select an OFFER **could fingerprint the implementation** 349 | 350 | [:rfc:`2131#section-4.4.1`]:: 351 | 352 | The client collects DHCPOFFER messages over a period of time, selects 353 | one DHCPOFFER message from the (possibly many) incoming DHCPOFFER 354 | messages 355 | 356 | The time 357 | over which the client collects messages and the mechanism used to 358 | select one DHCPOFFER are implementation dependent. 359 | 360 | - Is it different the retransmission delays waiting for offer or ack/nak?, 361 | in all states? 362 | 363 | Currently, the first OFFER is chosen 364 | 365 | Timers 366 | ~~~~~~~ 367 | [:rfc:`2131#section-4.4.5`]:: 368 | 369 | Times T1 and T2 are configurable by the server through options. T1 370 | defaults to (0.5 * duration_of_lease). T2 defaults to (0.875 * 371 | duration_of_lease). Times T1 and T2 SHOULD be chosen with some 372 | random "fuzz" around a fixed value, to avoid synchronization of 373 | client reacquisition. 374 | 375 | T1 is then calculated as:: 376 | 377 | renewing_time = lease_time * 0.5 - time_elapsed_after_request 378 | range_fuzz = lease_time * 0.875 - renewing_time 379 | renewing_time += random.uniform(-(range_fuzz), +(range_fuzz)) 380 | 381 | And T2:: 382 | 383 | rebinding_time = lease_time * 0.875 - time_elapsed_after_request 384 | range_fuzz = lease_time - rebinding_time 385 | rebinding_time += random.uniform(-(range_fuzz), +(range_fuzz)) 386 | 387 | The range_fuzz is calculated in the same way that ``systemd`` implementation 388 | does 389 | 390 | - what's the fixed value for the fuzz and how is it calculated? 391 | - The "fuzz" range is not specified, the fuzz chosen **could fingerprint** the 392 | implementation. 393 | 394 | 395 | Leases 396 | ~~~~~~~ 397 | 398 | [:rfc:`7844#section-3.3`]:: 399 | 400 | There are scenarios in which a client connecting to a network 401 | remembers a previously allocated address, i.e., when it is in the 402 | INIT-REBOOT state. In that state, any client that is concerned with 403 | privacy SHOULD perform a complete four-way handshake, starting with a 404 | DHCPDISCOVER, to obtain a new address lease. If the client can 405 | ascertain that this is exactly the same network to which it was 406 | previously connected, and if the link-layer address did not change, 407 | the client MAY issue a DHCPREQUEST to try to reclaim the current 408 | address. 409 | 410 | - is there a way to know if the network the client is connected to is the same to which it was connected previously? 411 | 412 | For the sake of simplicity and privacy ``dhcpcanon`` does not currently 413 | implement the INIT-REBOOT state nor reuse previously allocated addresses. 414 | 415 | In future stages of ``dhcpcanon`` would be possible to reuse a previously 416 | allocated address. 417 | In order to do not leak identifying information when doing so, 418 | it would be needed: 419 | 420 | * to keep a database with previously allocated addresses associated to: 421 | 422 | * the link network where the address was obtained 423 | (without revealing the MAC being used). 424 | 425 | * the MAC address that was used in that network 426 | 427 | It is possible also that ``dhcpcanon`` will include a MAC randomization module 428 | in the same distribution package or would require it in order to start. 429 | -------------------------------------------------------------------------------- /docs/source/images/classes_dhcpcanon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | classes_dhcpcanon 11 | 12 | 13 | 0 14 | 15 | ClientScript 16 | 17 | env : NoneType 18 | env : _CountingAttr 19 | scriptname 20 | scriptname : _CountingAttr 21 | 22 | script_go() 23 | script_init() 24 | 25 | 26 | 1 27 | 28 | DHCPCAP 29 | 30 | client_ip 31 | client_ip : _CountingAttr 32 | client_mac 33 | client_mac : _CountingAttr 34 | client_port : _CountingAttr 35 | event 36 | event : _CountingAttr 37 | iface 38 | iface : _CountingAttr 39 | lease 40 | lease : _CountingAttr 41 | prl 42 | prl : _CountingAttr 43 | server_ip 44 | server_ip : _CountingAttr 45 | server_mac 46 | server_mac : _CountingAttr 47 | server_port 48 | server_port : _CountingAttr 49 | xid 50 | xid : _CountingAttr 51 | 52 | gen_bootp() 53 | gen_bootp_unicast() 54 | gen_check_lease_attrs() 55 | gen_decline() 56 | gen_discover() 57 | gen_ether_ip() 58 | gen_ether_ip_unicast() 59 | gen_inform() 60 | gen_release() 61 | gen_request() 62 | gen_request_unicast() 63 | gen_udp() 64 | handle_ack() 65 | handle_offer() 66 | handle_offer_ack() 67 | 68 | 69 | 2 70 | 71 | DHCPCAPFSM 72 | 73 | client 74 | current_state 75 | debug_level : int 76 | delay_before_selecting : NoneType 77 | delay_selecting : bool 78 | discover_attempts : int 79 | event 80 | offers : list 81 | request_attempts : int 82 | script : NoneType 83 | socket_kargs : dict 84 | time_sent_request : NoneType 85 | timeout_select : NoneType 86 | 87 | BOUND() 88 | END() 89 | ERROR() 90 | INIT() 91 | REBINDING() 92 | RENEWING() 93 | REQUESTING() 94 | SELECTING() 95 | action_transmit_discover() 96 | action_transmit_request() 97 | dict_self() 98 | get_timeout() 99 | lease_expires() 100 | on_ack_requesting() 101 | on_renewing() 102 | process_received_ack() 103 | process_received_nak() 104 | rebinding_time_expires() 105 | receive_ack_rebinding() 106 | receive_ack_renewing() 107 | receive_ack_requesting() 108 | receive_nak_rebinding() 109 | receive_nak_renewing() 110 | receive_nak_requesting() 111 | receive_offer() 112 | renewing_time_expires() 113 | reset() 114 | select_offer() 115 | send_discover() 116 | send_request() 117 | set_timeout() 118 | set_timers() 119 | timeout_delay_before_selecting() 120 | timeout_request_rebinding() 121 | timeout_request_renewing() 122 | timeout_requesting() 123 | timeout_selecting() 124 | 125 | 126 | 3 127 | 128 | DHCPCAPLease 129 | 130 | address : _CountingAttr 131 | broadcast_address : _CountingAttr 132 | domain : _CountingAttr 133 | expiry 134 | expiry : _CountingAttr 135 | interface : _CountingAttr 136 | lease_time : _CountingAttr 137 | name_server : _CountingAttr 138 | network : _CountingAttr 139 | next_server : _CountingAttr 140 | rebind 141 | rebind : _CountingAttr 142 | rebinding_time 143 | rebinding_time : _CountingAttr 144 | renew 145 | renew : _CountingAttr 146 | renewal_time 147 | renewal_time : _CountingAttr 148 | router : _CountingAttr 149 | server_id : _CountingAttr 150 | subnet : _CountingAttr 151 | subnet_mask : _CountingAttr 152 | subnet_mask_cidr : _CountingAttr 153 | 154 | info_lease() 155 | set_times() 156 | 157 | 158 | 159 | --------------------------------------------------------------------------------