├── .github └── workflows │ ├── build-release-latest.yml │ └── build-release-stable.yml ├── .gitignore ├── Changelog ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── build.sh ├── build ├── Dockerfile-centos ├── Dockerfile-debian ├── entrypoint-build-centos.sh ├── entrypoint-build-debian.sh ├── entrypoint-repo-centos.sh └── entrypoint-repo-debian.sh ├── debian ├── changelog ├── compat ├── control ├── copyright ├── dhcpy6d.dirs ├── dhcpy6d.init ├── dhcpy6d.install ├── dhcpy6d.logrotate ├── dhcpy6d.postinst ├── dhcpy6d.postrm ├── dhcpy6d.service ├── manpages ├── rules ├── source │ ├── format │ └── options └── watch ├── dhcpy6d ├── __init__.py ├── client │ ├── __init__.py │ ├── default.py │ ├── from_config.py │ ├── parse_pattern.py │ └── reuse_lease.py ├── config.py ├── constants.py ├── domain.py ├── globals.py ├── handler.py ├── helpers.py ├── log.py ├── macs.py ├── options │ ├── __init__.py │ ├── option_1.py │ ├── option_12.py │ ├── option_13.py │ ├── option_14.py │ ├── option_15.py │ ├── option_16.py │ ├── option_20.py │ ├── option_23.py │ ├── option_24.py │ ├── option_25.py │ ├── option_3.py │ ├── option_31.py │ ├── option_32.py │ ├── option_39.py │ ├── option_4.py │ ├── option_56.py │ ├── option_59.py │ ├── option_6.py │ ├── option_61.py │ ├── option_7.py │ ├── option_8.py │ ├── option_82.py │ └── option_83.py ├── route.py ├── storage │ ├── __init__.py │ ├── mysql.py │ ├── postgresql.py │ ├── schemas.py │ ├── sqlite.py │ ├── store.py │ └── textfile.py ├── threads.py └── transaction.py ├── doc ├── LICENSE ├── clients-example.conf ├── config.sql ├── dhcpy6d-clients.conf.rst ├── dhcpy6d-example.conf ├── dhcpy6d-minimal.conf ├── dhcpy6d.conf.rst ├── dhcpy6d.rst ├── volatile.postgresql └── volatile.sql ├── etc ├── default │ └── dhcpy6d ├── dhcpy6d.conf └── logrotate.d │ └── dhcpy6d ├── lib └── systemd │ └── system │ └── dhcpy6d.service ├── main.py ├── man ├── man5 │ ├── dhcpy6d-clients.conf.5 │ └── dhcpy6d.conf.5 └── man8 │ └── dhcpy6d.8 ├── redhat └── dhcpy6d.spec ├── setup.py └── var ├── lib └── volatile.sqlite └── log └── dhcpy6d.log /.github/workflows/build-release-latest.yml: -------------------------------------------------------------------------------- 1 | name: build-release-latest 2 | on: 3 | push: 4 | tags-ignore: 'v*' 5 | branches: '**' 6 | 7 | env: 8 | release: stable 9 | repo_dir: dhcpy6d-jekyll/docs/repo 10 | 11 | jobs: 12 | build-debian: 13 | runs-on: ubuntu-latest 14 | env: 15 | dist: debian 16 | steps: 17 | # get source 18 | - uses: actions/checkout@v4 19 | # build container image for package creation 20 | - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . 21 | # make entrypoints executable 22 | - run: chmod +x build/*.sh 23 | # execute container with matching entrypoint 24 | - run: | 25 | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ 26 | --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ 27 | --entrypoint /entrypoint.sh \ 28 | build-${{ github.job }} 29 | # upload results 30 | - uses: actions/upload-artifact@v4 31 | with: 32 | path: ./*.deb 33 | retention-days: 1 34 | name: ${{ github.job }} 35 | 36 | 37 | build-centos: 38 | runs-on: ubuntu-latest 39 | env: 40 | dist: centos 41 | steps: 42 | # get source 43 | - uses: actions/checkout@v4 44 | # build container image for package creation 45 | - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . 46 | # make entrypoints executable 47 | - run: chmod +x build/*.sh 48 | # execute container with matching entrypoint 49 | - run: | 50 | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ 51 | --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ 52 | --entrypoint /entrypoint.sh \ 53 | build-${{ github.job }} 54 | # upload results 55 | - uses: actions/upload-artifact@v4 56 | with: 57 | path: ./*.rpm 58 | retention-days: 1 59 | name: ${{ github.job }} 60 | 61 | github-release: 62 | runs-on: ubuntu-latest 63 | needs: [build-debian, build-centos] 64 | steps: 65 | - uses: actions/download-artifact@v4 66 | - run: cd build-debian && md5sum *dhcpy6d* > ../md5sums.txt 67 | - run: cd build-debian && sha256sum *dhcpy6d* > ../sha256sums.txt 68 | - run: cd build-centos && md5sum *dhcpy6d* >> ../md5sums.txt 69 | - run: cd build-centos && sha256sum *dhcpy6d* >> ../sha256sums.txt 70 | - uses: marvinpinto/action-automatic-releases@latest 71 | with: 72 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 73 | automatic_release_tag: "latest" 74 | prerelease: true 75 | files: | 76 | build-debian/* 77 | build-centos/* 78 | *sums.txt 79 | -------------------------------------------------------------------------------- /.github/workflows/build-release-stable.yml: -------------------------------------------------------------------------------- 1 | name: build-release-stable 2 | on: 3 | push: 4 | tags: 'v*' 5 | 6 | env: 7 | release: stable 8 | repo_dir: dhcpy6d-jekyll/docs/repo 9 | 10 | jobs: 11 | build-debian: 12 | runs-on: ubuntu-latest 13 | env: 14 | dist: debian 15 | steps: 16 | # get source 17 | - uses: actions/checkout@v2 18 | # build container image for package creation 19 | - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . 20 | # make entrypoints executable 21 | - run: chmod +x build/*.sh 22 | # execute container with matching entrypoint 23 | - run: | 24 | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ 25 | --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ 26 | --entrypoint /entrypoint.sh \ 27 | build-${{ github.job }} 28 | # upload results 29 | - uses: actions/upload-artifact@v2 30 | with: 31 | path: ./*.deb 32 | retention-days: 1 33 | 34 | build-centos: 35 | runs-on: ubuntu-latest 36 | env: 37 | dist: centos 38 | steps: 39 | # get source 40 | - uses: actions/checkout@v2 41 | # build container image for package creation 42 | - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . 43 | # make entrypoints executable 44 | - run: chmod +x build/*.sh 45 | # execute container with matching entrypoint 46 | - run: | 47 | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ 48 | --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ 49 | --entrypoint /entrypoint.sh \ 50 | build-${{ github.job }} 51 | # upload results 52 | - uses: actions/upload-artifact@v2 53 | with: 54 | path: ./*.rpm 55 | retention-days: 1 56 | 57 | repo-debian: 58 | runs-on: ubuntu-latest 59 | needs: [build-debian] 60 | env: 61 | dist: debian 62 | steps: 63 | - uses: actions/checkout@v2 64 | # get binaries created by other jobs 65 | - uses: actions/download-artifact@v2 66 | # build container image for repo packaging, using the same as for building 67 | - run: /usr/bin/docker build -t ${{ github.job }} -f build/Dockerfile-${{ env.dist }} . 68 | # make entrypoints executable 69 | - run: chmod +x build/entrypoint-*.sh 70 | # get secret signing key 71 | - run: echo "${{ secrets.DHCPY6D_SIGNING_KEY }}" > signing_key.asc 72 | # organize SSH deploy key for dhcp6d-jekyll repo 73 | - run: mkdir ~/.ssh 74 | - run: echo "${{ secrets.DHCPY6D_REPO_SSH_KEY }}" > ~/.ssh/id_ed25519 75 | - run: chmod -R go-rwx ~/.ssh 76 | # get and prepare dhcpy6d-jekyll 77 | - run: git clone git@github.com:HenriWahl/dhcpy6d-jekyll.git 78 | - run: rm -rf ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} 79 | - run: mkdir -p ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} 80 | # execute container with matching entrypoint 81 | - run: | 82 | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ 83 | --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ 84 | --entrypoint /entrypoint.sh \ 85 | --env RELEASE=${{ env.release }} \ 86 | ${{ github.job }} 87 | # commit and push new binaries to dhcpyd-jekyll 88 | - run: git config --global user.email "repo@dhcpy6d.de" && git config --global user.name "Dhcpy6d Repository" 89 | - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.dist }}" && git push 90 | 91 | repo-centos: 92 | runs-on: ubuntu-latest 93 | # has to wait for repo-debian to avoid parallel processing of git repo dhcpy6d-jekyll 94 | needs: [build-centos, repo-debian] 95 | env: 96 | dist: centos 97 | steps: 98 | - uses: actions/checkout@v2 99 | # get binaries created by other jobs 100 | - uses: actions/download-artifact@v2 101 | # build container image for repo packaging, using the same as for building 102 | - run: /usr/bin/docker build -t ${{ github.job }} -f build/Dockerfile-${{ env.dist }} . 103 | # make entrypoints executable 104 | - run: chmod +x build/entrypoint-*.sh 105 | # get secret signing key 106 | - run: echo "${{ secrets.DHCPY6D_SIGNING_KEY }}" > signing_key.asc 107 | # organize SSH deploy key for dhcp6d-jekyll repo 108 | - run: mkdir ~/.ssh 109 | - run: echo "${{ secrets.DHCPY6D_REPO_SSH_KEY }}" > ~/.ssh/id_ed25519 110 | - run: chmod -R go-rwx ~/.ssh 111 | # get and prepare dhcpy6d-jekyll 112 | - run: git clone git@github.com:HenriWahl/dhcpy6d-jekyll.git 113 | - run: rm -rf ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} 114 | - run: mkdir -p ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} 115 | # execute container with matching entrypoint 116 | - run: | 117 | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ 118 | --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ 119 | --entrypoint /entrypoint.sh \ 120 | --env RELEASE=${{ env.release }} \ 121 | ${{ github.job }} 122 | # commit and push new binaries to dhcpyd-jekyll 123 | - run: git config --global user.email "repo@dhcpy6d.de" && git config --global user.name "Dhcpy6d Repository" 124 | - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.dist }}" && git push 125 | 126 | github-release: 127 | runs-on: ubuntu-latest 128 | needs: [build-debian, build-centos] 129 | steps: 130 | - uses: actions/download-artifact@v2 131 | - run: cd artifact && md5sum *dhcpy6d* > md5sums.txt 132 | - run: cd artifact && sha256sum *dhcpy6d* > sha256sums.txt 133 | - uses: marvinpinto/action-automatic-releases@latest 134 | with: 135 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 136 | prerelease: true 137 | files: | 138 | artifact/* 139 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.conf 2 | *.pyc 3 | *.yaml 4 | *.db 5 | *.log 6 | .idea 7 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | Changelog for dhcpy6d 2 | 3 | 2023-07-02 1.6.0 4 | 5 | added option 82 support 6 | added option 83 support 7 | added partly option 20 support 8 | 9 | 2023-07-02 1.4.0 10 | 11 | added prefix_route_link_local for client config 12 | 13 | 2023-02-20 1.2.3 14 | 15 | fixed invalid hostname 16 | 17 | 2022-06-14 1.2.2 18 | 19 | fixed class interface parsing 20 | 21 | 2022-05-10 1.2.1 22 | 23 | fixed option 23 24 | 25 | 2022-04-04 1.2.0 26 | 27 | new option to exclude interface 28 | fixed dynamic prefix injection 29 | fixed volatile.sqlite update trouble 30 | fixed Debian build dependencies 31 | fixed documentation 32 | fixed reuse lease 33 | 34 | 2021-11-21 1.0.9 35 | 36 | fixed overwrite of SQLite DB when upgrading 37 | 38 | 2021-10-30 1.0.8 39 | 40 | fixed acceptance of empty addresses in client requests 41 | 42 | 2021-10-01 1.0.7 43 | 44 | fixed non-existing UserClass 45 | 46 | 2021-09-30 1.0.6 47 | 48 | fixed empty client config file 49 | fixed DB updates 50 | 51 | 2021-08-11 1.0.5 52 | 53 | fixed inability to use multiple MACs per host in DB 54 | 55 | 2021-08-10 1.0.4 56 | 57 | fixed default behavior (route_link_local=no) in clients.conf 58 | 59 | 2020-11-20 1.0.3 60 | 61 | added option DNS_USE_RNDC 62 | 63 | 2020-10-08 1.0.2 64 | 65 | fixed NTP_SERVER_DICT 66 | 67 | 2020-07-24 1.0.1 68 | 69 | fix mandatory logfile 70 | 71 | 2020-04-03 1.0 72 | 73 | added EUI64 address category 74 | added PXE boot support 75 | added support for fixed prefix per client config 76 | added address category dns to retrieve client ipv6 from DNS 77 | added self-creation of database tables 78 | improved PostgreSQL support 79 | migrated to Python 3 80 | code housekeeping 81 | fixes of course 82 | 83 | 2018-10-25 0.7.3 84 | 85 | added ignore_mac option to work with ppp interfaces 86 | 87 | 2018-06-15 0.7.2 88 | 89 | fix for MySQLdb.IntegrityError 90 | 91 | 2018-06-11 0.7.1 92 | 93 | fixed recycling of prefixes 94 | 95 | 2018-04-30 0.7 96 | 97 | added ntp_server option 98 | added request limits 99 | allow one to inject prefix - e.g. changed prefix from ISP 100 | optimized time requests 101 | ignore unknown clients 102 | fixes for prefix delegation 103 | 104 | 2017-09-29 0.6 105 | 106 | Prefix delegation (PD) 107 | fixes 108 | 109 | 2017-05-29 0.5 110 | 111 | Allow using PostgreSQL database for volatile and config storage 112 | Added category 'dns' for DNS-based IP-address retrieval 113 | Reply CONFIRM requests with NotOnLink to force clients to get new address 114 | Added --prefix option to be used for dynamic prefixes 115 | Systemd integration 116 | 117 | 2016-01-05 0.4.3 118 | 119 | autocommit to MySQL 120 | fixed fixed addresses 121 | some optimization in tidy-up-thread 122 | small fixes 123 | 124 | 2015-08-18 0.4.2 125 | 126 | fixed usage of fixed addresses in dhcpy6d-clients.conf 127 | fixed dns_update() to update default class clients too 128 | show warning if deprecated prefix_length is used in address definitions 129 | set socket to non-blocking to avoid freeze 130 | increase MAC/LLIP cache time from 30s to 300s because of laggy clients 131 | removed useless prefix length 132 | retry query on MySQL reconnect bugfix 133 | 134 | 2015-03-17 0.4.1 135 | 136 | listen on VLAN interfaces now really works 137 | some code cleaning 138 | 139 | 2014-10-22 0.4 140 | 141 | listen on VLAN interfaces 142 | access neighbor cache natively on Linux 143 | allows empty answers 144 | do not cache MAC/LLIP addresses longterm as default 145 | ability to generate server DUID 146 | complete manpages 147 | more complete configuration correctness checks 148 | fixed single address definition error 149 | 150 | 2013-07-29 0.3 151 | 152 | added ability to run as non-privileged user 153 | 154 | 2013-05-31 0.2 155 | 156 | Attention: leases database scheme changed. Possibly leases database 157 | has to be recreated! 158 | 'range' address lease management got more robust 159 | logging output changed 160 | 161 | 2013-05-23 0.1.5 162 | 163 | fixed race condition bug in category 'range' address lease storage 164 | 165 | 2013-05-18 0.1.4.1 166 | 167 | fixed lease storage bug 168 | 169 | 2013-05-17 0.1.4 170 | 171 | fixed race condition bug with already advertised addresses 172 | 173 | 2013-05-07 0.1.3 174 | 175 | added RFC 3646 compliant domain search list option 24 176 | reuse addresses of category "range" in a sensible way 177 | fixed bug with case sensitive textfile client config options 178 | 179 | 2013-03-19 0.1.2 180 | 181 | fixed multiple addresses renew bug 182 | 183 | 2013-01-15 0.1.1 184 | 185 | reverted to Handler.finish() method to prevent empty extra answer 186 | packets 187 | 188 | 2013-01-11 0.1 189 | 190 | initial stable release 191 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | LABEL maintainer=henri@dhcpy6d.de 3 | 4 | ARG HTTP_PROXY="" 5 | ENV HTTPS_PROXY $HTTP_PROXY 6 | ENV http_proxy $HTTP_PROXY 7 | ENV https_proxy $HTTP_PROXY 8 | 9 | RUN pip install distro \ 10 | dnspython \ 11 | mysqlclient \ 12 | psycopg2 13 | 14 | RUN useradd --system --user-group dhcpy6d 15 | 16 | WORKDIR /dhcpy6d 17 | 18 | CMD python3 main.py --config dhcpy6d.conf --user dhcpy6d --group dhcpy6d 19 | 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # necessary because of buggy distutils 2 | include Changelog 3 | include dhcpy6d 4 | include doc/LICENSE 5 | include doc/clients-example.conf 6 | include doc/config.sql 7 | include doc/dhcpy6d-example.conf 8 | include doc/dhcpy6d-minimal.conf 9 | include doc/volatile.sql 10 | include doc/volatile.postgresql 11 | include man/man5/dhcpy6d.conf.5 12 | include man/man5/dhcpy6d-clients.conf.5 13 | include man/man8/dhcpy6d.8 14 | include var/lib/volatile.sqlite 15 | include var/log/dhcpy6d.log 16 | include etc/dhcpy6d.conf 17 | include etc/logrotate.d/dhcpy6d 18 | include lib/systemd/system/dhcpy6d.service 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dhcpy6d 2 | ======= 3 | 4 | Dhcpy6d delivers IPv6 addresses and prefixes for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. 5 | Addresses may be generated randomly, by range, by arbitrary ID or MAC address. 6 | Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically 7 | Range-based prefixes be delegated as well as fixed prefixes per client. 8 | Changing prefixes from ISP can be applied dynamically. 9 | 10 | Supported platforms include Linux, OpenBSD, FreeBSD, NetBSD and macOS. At any other POSIX OS it might work too. 11 | 12 | Homepage: https://dhcpy6d.de/ 13 | 14 | Documentation: https://dhcpy6d.de/documentation 15 | 16 | Container Image: https://hub.docker.com/r/henriwahl/dhcpy6d 17 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # 4 | # simple build script for dhcpy6d 5 | # 6 | # 7 | 8 | OS=unknown 9 | 10 | function get_os() { 11 | if [ -f /etc/debian_version ]; then 12 | OS=debian 13 | elif [ -f /etc/redhat-release ]; then 14 | yum install -y sudo which 15 | OS=redhat 16 | fi 17 | } 18 | 19 | function create_manpages() { 20 | if ! which rst2man; then 21 | if [ "$OS" == "debian" ]; then 22 | sudo apt -y install python3-docutils 23 | fi 24 | if [ "$OS" == "redhat" ]; then 25 | sudo yum -y install python3-docutils 26 | fi 27 | fi 28 | 29 | echo "Creating manpages from RST files" 30 | rst2man doc/dhcpy6d.rst man/man8/dhcpy6d.8 31 | rst2man doc/dhcpy6d.conf.rst man/man5/dhcpy6d.conf.5 32 | rst2man doc/dhcpy6d-clients.conf.rst man/man5/dhcpy6d-clients.conf.5 33 | } 34 | 35 | # find out where script runs at 36 | get_os 37 | 38 | if [ "$1" = "man" ] 39 | then 40 | echo "Re-generating man pages" 41 | create_manpages 42 | elif [ "$OS" == "debian" ]; then 43 | echo "Building .deb package" 44 | 45 | create_manpages 46 | 47 | # install missing packages 48 | if ! which debuild; then 49 | sudo apt -y install build-essential devscripts dh-python dh-systemd python3-setuptools 50 | fi 51 | 52 | if [ ! -d /usr/share/doc/python3-all ]; then 53 | sudo apt -y install python3-all 54 | fi 55 | 56 | debuild --no-tgz-check -- clean 57 | debuild --no-tgz-check -- binary-indep 58 | 59 | elif [ "$OS" == "redhat" ]; then 60 | echo "Building .rpm package" 61 | 62 | create_manpages 63 | 64 | # install missing packages 65 | if ! which rpmbuild; then 66 | sudo yum -y install python3-devel python3-setuptools rpm-build 67 | fi 68 | 69 | TOPDIR=$HOME/dhcpy6d.$$ 70 | SPEC=redhat/dhcpy6d.spec 71 | 72 | # create source folder for rpmbuild 73 | mkdir -p $TOPDIR/SOURCES 74 | 75 | # init needed in TOPDIR/SOURCES 76 | cp -pf lib/systemd/system/dhcpy6d.service $TOPDIR/SOURCES/dhcpy6d 77 | 78 | # use setup.py sdist build output to get package name 79 | FILE=$(python3 setup.py sdist --dist-dir $TOPDIR/SOURCES | grep "creating dhcpy6d-" | head -n1 | cut -d" " -f2) 80 | echo Source file: $FILE.tar.gz 81 | 82 | # version 83 | VERSION=$(echo $FILE | cut -d"-" -f 2) 84 | 85 | # replace version in the spec file 86 | sed -i "s|Version:.*|Version: $VERSION|" $SPEC 87 | 88 | # workaround for less changes, but achieve build with new GitHub source 89 | # TDDO: clean up that build process 90 | cp ${TOPDIR}/SOURCES/${FILE}.tar.gz ${TOPDIR}/SOURCES/v${VERSION}.tar.gz 91 | 92 | # finally build binary rpm 93 | rpmbuild -bb --define "_topdir $TOPDIR" $SPEC 94 | 95 | echo $TOPDIR 96 | 97 | # get rpm file 98 | cp -f $(find $TOPDIR/RPMS -name "$FILE-?.*noarch.rpm") . 99 | 100 | # clean 101 | #rm -rf $TOPDIR 102 | else 103 | echo "Package creation is only supported on Debian and RedHat derivatives." 104 | fi 105 | -------------------------------------------------------------------------------- /build/Dockerfile-centos: -------------------------------------------------------------------------------- 1 | FROM rockylinux:8 2 | LABEL maintainer=henri@dhcpy6d.de 3 | 4 | # get build requirements 5 | RUN yum -y install createrepo \ 6 | git \ 7 | gpg \ 8 | python3-devel \ 9 | python3-docutils \ 10 | rpm-build \ 11 | rpm-sign \ 12 | sudo \ 13 | which 14 | 15 | # flexible entrypoint, mounted as volume 16 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /build/Dockerfile-debian: -------------------------------------------------------------------------------- 1 | FROM debian:12 2 | LABEL maintainer=henri@dhcpy6d.de 3 | 4 | # get build requirements 5 | RUN apt -y update 6 | RUN apt -y install apt-utils \ 7 | build-essential \ 8 | dpkg-dev \ 9 | devscripts \ 10 | dh-python \ 11 | git \ 12 | gpg \ 13 | python3-all \ 14 | python3-distro \ 15 | python3-distutils \ 16 | python3-docutils \ 17 | python3-lib2to3 \ 18 | python3-setuptools \ 19 | sudo 20 | 21 | # flexible entrypoint, mounted as volume 22 | ENTRYPOINT ["/entrypoint.sh"] 23 | -------------------------------------------------------------------------------- /build/entrypoint-build-centos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # flexible entrypoint, mounted as volume 4 | # 5 | 6 | set -e 7 | 8 | # got to working directory 9 | cd /dhcpy6d 10 | # run build script 11 | ./build.sh 12 | -------------------------------------------------------------------------------- /build/entrypoint-build-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # flexible entrypoint, mounted as volume 4 | # 5 | 6 | set -e 7 | 8 | # got to working directory 9 | cd /dhcpy6d 10 | # run build script 11 | ./build.sh 12 | # copy resulting Debian package back into working directory 13 | cp /*.deb /dhcpy6d -------------------------------------------------------------------------------- /build/entrypoint-repo-centos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # create repo package files and sign them 4 | # 5 | 6 | set -e 7 | 8 | # go to working directory volume 9 | cd /dhcpy6d 10 | 11 | # import signing key, stored from GitHub secrets in workflow 12 | gpg --import signing_key.asc 13 | 14 | # put package to its later place 15 | cp -r artifact/*.rpm dhcpy6d-jekyll/docs/repo/${RELEASE}/centos 16 | 17 | # RELEASE is a runtime --env argument to make it easier to provide stable and latest reo 18 | cd dhcpy6d-jekyll/docs/repo/${RELEASE}/centos 19 | 20 | # create repo files + sign package 21 | gpg --output RPM-GPG-KEY-dhcpy6d --armor --export 22 | cp RPM-GPG-KEY-dhcpy6d /etc/pki/rpm-gpg 23 | rpm --import RPM-GPG-KEY-dhcpy6d 24 | echo "%_signature gpg" > ~/.rpmmacros 25 | echo "%_gpg_name dhcpy6d" >> ~/.rpmmacros 26 | rpm --resign *.rpm 27 | createrepo --update . 28 | rm -rf .rpmmacros 29 | -------------------------------------------------------------------------------- /build/entrypoint-repo-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # create repo package files and sign them 4 | # 5 | 6 | set -e 7 | 8 | # go to working directory volume 9 | cd /dhcpy6d 10 | 11 | # import signing key, stored from GitHub secrets in workflow 12 | gpg --import signing_key.asc 13 | 14 | # put package to its later place 15 | cp -r artifact/*.deb dhcpy6d-jekyll/docs/repo/${RELEASE}/debian 16 | 17 | # RELEASE is a runtime --env argument to make it easier to provide stable and latest reo 18 | cd dhcpy6d-jekyll/docs/repo/${RELEASE}/debian 19 | 20 | # create repo files 21 | dpkg-scanpackages . > Packages 22 | gzip -k -f Packages 23 | apt-ftparchive release . > Release 24 | 25 | # sign package 26 | gpg -abs -o Release.gpg Release 27 | gpg --clearsign -o InRelease Release 28 | gpg --output key.gpg --armor --export 29 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: dhcpy6d 2 | Section: net 3 | X-Python-Version: >= 3.7 4 | X-Python3-Version: >= 3.7 5 | Priority: optional 6 | Maintainer: Axel Beckert 7 | Build-Depends: debhelper (>= 12.1.1~), 8 | python3-all (>= 3.7.3-1~) 9 | Build-Depends-Indep: dh-python 10 | Homepage: https://dhcpy6d.de 11 | Vcs-Git: git://github.com/HenriWahl/dhcpy6d.git 12 | Vcs-Browser: https://github.com/HenriWahl/dhcpy6d 13 | Standards-Version: 4.2.1 14 | 15 | Package: dhcpy6d 16 | Architecture: all 17 | Depends: adduser, 18 | lsb-base, 19 | python3-distro, 20 | python3-dnspython, 21 | ${misc:Depends}, 22 | ${python3:Depends}, 23 | ucf 24 | Pre-Depends: dpkg (>= 1.19.7) 25 | Suggests: python3-mysqldb, 26 | python3-psycopg2 27 | Description: MAC address aware DHCPv6 server written in Python 28 | Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be 29 | identified by DUID, hostname or MAC address as in the good old IPv4 30 | days. It allows easy dualstack transition, addresses may be 31 | generated randomly, by range, by arbitrary ID or MAC address. Clients 32 | can get more than one address, leases and client configuration can be 33 | stored in databases and DNS can be updated dynamically. 34 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: dhcpy6d 3 | Source: https://dhcpy6d.de/ 4 | 5 | Files: * 6 | Copyright: 2012-2024 Henri Wahl 7 | License: GPL-2+ 8 | 9 | Files: debian/* 10 | Copyright: 2012-2024 Henri Wahl 11 | 2014 Axel Beckert 12 | License: GPL-2+ 13 | 14 | License: GPL-2+ 15 | This program is free software; you can redistribute it 16 | and/or modify it under the terms of the GNU General Public 17 | License as published by the Free Software Foundation; either 18 | version 2 of the License, or (at your option) any later 19 | version. 20 | . 21 | This program is distributed in the hope that it will be 22 | useful, but WITHOUT ANY WARRANTY; without even the implied 23 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 24 | PURPOSE. See the GNU General Public License for more 25 | details. 26 | . 27 | You should have received a copy of the GNU General Public 28 | License along with this package; if not, write to the Free 29 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 30 | Boston, MA 02110-1301 USA 31 | . 32 | On Debian systems, the full text of the GNU General Public 33 | License version 2 can be found in the file 34 | `/usr/share/common-licenses/GPL-2'. 35 | -------------------------------------------------------------------------------- /debian/dhcpy6d.dirs: -------------------------------------------------------------------------------- 1 | usr/share/dhcpy6d/ 2 | usr/share/dhcpy6d/default 3 | var/lib 4 | var/log 5 | -------------------------------------------------------------------------------- /debian/dhcpy6d.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: dhcpy6d 4 | # Required-Start: $syslog $network $remote_fs 5 | # Required-Stop: $syslog $remote_fs 6 | # Should-Start: $local_fs 7 | # Should-Stop: $local_fs 8 | # Default-Start: 2 3 4 5 9 | # Default-Stop: 0 1 6 10 | # Short-Description: Start/Stop dhcpy6d DHCPv6 server 11 | # Description: (empty) 12 | ### END INIT INFO 13 | 14 | set -e 15 | 16 | PATH=/sbin:/bin:/usr/sbin:/usr/bin 17 | DHCPY6DBIN=/usr/sbin/dhcpy6d 18 | DHCPY6DCONF=/etc/dhcpy6d.conf 19 | DHCPY6DPID=/var/run/dhcpy6d.pid 20 | NAME="dhcpy6d" 21 | DESC="dhcpy6d DHCPv6 server" 22 | USER=dhcpy6d 23 | GROUP=dhcpy6d 24 | 25 | RUN=no 26 | DEFAULTFILE=/etc/default/dhcpy6d 27 | if [ -f $DEFAULTFILE ]; then 28 | . $DEFAULTFILE 29 | fi 30 | 31 | . /lib/lsb/init-functions 32 | 33 | check_status() 34 | { 35 | if [ ! -r "$DHCPY6DPID" ]; then 36 | test "$1" != -v || echo "$NAME is not running." 37 | return 3 38 | fi 39 | if read pid < "$DHCPY6DPID" && ps -p "$pid" > /dev/null 2>&1; then 40 | test "$1" != -v || echo "$NAME is running." 41 | return 0 42 | else 43 | test "$1" != -v || echo "$NAME is not running but $DHCPY6DPID exists." 44 | return 1 45 | fi 46 | } 47 | 48 | test -x $DHCPY6DBIN || exit 0 49 | 50 | case "$1" in 51 | start) 52 | if [ "$RUN" = "no" ]; then 53 | echo "dhcpy6d is disabled in /etc/default/dhcpy6d. Set RUN=yes to get it running." 54 | exit 0 55 | fi 56 | 57 | log_daemon_msg "Starting $DESC $NAME" 58 | 59 | if ! check_status; then 60 | 61 | start-stop-daemon --start --make-pidfile --pidfile ${DHCPY6DPID} \ 62 | --background --oknodo --no-close --exec $DHCPY6DBIN -- --config $DHCPY6DCONF \ 63 | --user $USER \ 64 | --group $GROUP \ 65 | --duid $DUID \ 66 | --really-do-it $RUN 67 | 68 | sleep 2 69 | if check_status -q; then 70 | log_end_msg 0 71 | else 72 | log_end_msg 1 73 | exit 1 74 | fi 75 | else 76 | log_end_msg 1 77 | exit 1 78 | fi 79 | ;; 80 | stop) 81 | log_daemon_msg "Stopping $DESC $NAME" 82 | start-stop-daemon --stop --quiet --pidfile ${DHCPY6DPID} --oknodo 83 | log_end_msg $? 84 | rm -f $DHCPY6DPID 85 | ;; 86 | restart|force-reload) 87 | $0 stop 88 | sleep 2 89 | $0 start 90 | if [ "$?" != "0" ]; then 91 | exit 1 92 | fi 93 | ;; 94 | status) 95 | echo "Status of $NAME: " 96 | check_status -v 97 | exit "$?" 98 | ;; 99 | *) 100 | echo "Usage: $0 (start|stop|restart|force-reload|status)" 101 | exit 1 102 | esac 103 | 104 | exit 0 105 | -------------------------------------------------------------------------------- /debian/dhcpy6d.install: -------------------------------------------------------------------------------- 1 | etc/default/dhcpy6d usr/share/dhcpy6d/default/ 2 | -------------------------------------------------------------------------------- /debian/dhcpy6d.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/dhcpy6d.log { 2 | weekly 3 | missingok 4 | rotate 4 5 | compress 6 | notifempty 7 | create 660 dhcpy6d dhcpy6d 8 | } 9 | 10 | -------------------------------------------------------------------------------- /debian/dhcpy6d.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # attempting to create lower privileged user/group for dhcpy6d 4 | # take from http://www.debian.org/doc/manuals/securing-debian-howto/ch9.en.html#s-bpp-lower-privs 5 | # 6 | 7 | set -e 8 | 9 | case "$1" in 10 | configure) 11 | 12 | # Sane defaults: 13 | 14 | [ -z "$SERVER_HOME" ] && SERVER_HOME=/var/lib/dhcpy6d 15 | [ -z "$SERVER_USER" ] && SERVER_USER=dhcpy6d 16 | [ -z "$SERVER_NAME" ] && SERVER_NAME="DHCPv6 server dhcpy6d" 17 | [ -z "$SERVER_GROUP" ] && SERVER_GROUP=dhcpy6d 18 | 19 | # Groups that the user will be added to, if undefined, then none. 20 | ADDGROUP="" 21 | 22 | # create user to avoid running server as root 23 | # 1. create group if not existing 24 | if ! getent group | grep -q "^$SERVER_GROUP:" ; then 25 | echo -n "Adding group $SERVER_GROUP.." 26 | addgroup --quiet --system $SERVER_GROUP 2>/dev/null ||true 27 | echo "..done" 28 | fi 29 | # 2. create homedir if not existing 30 | test -d $SERVER_HOME || mkdir $SERVER_HOME 31 | # 3. create user if not existing 32 | if ! getent passwd | grep -q "^$SERVER_USER:"; then 33 | echo -n "Adding system user $SERVER_USER.." 34 | adduser --quiet \ 35 | --system \ 36 | --ingroup $SERVER_GROUP \ 37 | --no-create-home \ 38 | --home $SERVER_HOME \ 39 | --gecos "$SERVER_NAME" \ 40 | --disabled-password \ 41 | $SERVER_USER 2>/dev/null || true 42 | echo "..done" 43 | fi 44 | # 4. adjust file and directory permissions 45 | chown -R $SERVER_USER:$SERVER_GROUP $SERVER_HOME 46 | chmod -R 0770 $SERVER_HOME 47 | if [ ! -e /var/log/dhcpy6d.log ]; then 48 | touch /var/log/dhcpy6d.log 49 | fi 50 | if [ ! -e /var/lib/dhcpy6d/volatile.sqlite ]; then 51 | touch /var/lib/dhcpy6d/volatile.sqlite 52 | fi 53 | chown $SERVER_USER:$SERVER_GROUP /var/log/dhcpy6d.log /var/lib/dhcpy6d/volatile.sqlite 54 | chmod 0660 /var/log/dhcpy6d.log /var/lib/dhcpy6d/volatile.sqlite 55 | # 6. add DUID entry to /etc/default/dhcpy6d if not yet existing 56 | TMPFILE=`mktemp` 57 | cat /usr/share/dhcpy6d/default/dhcpy6d > "${TMPFILE}" 58 | echo >> "${TMPFILE}" 59 | echo "# LLT DUID generated by Debian" >> "${TMPFILE}" 60 | if [ ! -e /etc/default/dhcpy6d ] || ! grep -q "DUID=" /etc/default/dhcpy6d; then 61 | echo "DUID=$(dhcpy6d --generate-duid)" >> "${TMPFILE}" 62 | else 63 | egrep "^DUID=" /etc/default/dhcpy6d >> "${TMPFILE}" 64 | fi 65 | ucf "${TMPFILE}" /etc/default/dhcpy6d 66 | ucfr dhcpy6d /etc/default/dhcpy6d 67 | ;; 68 | esac 69 | 70 | #DEBHELPER# 71 | -------------------------------------------------------------------------------- /debian/dhcpy6d.postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Delete dhcpy6d's log files upon package purge. 4 | # 5 | 6 | set -e 7 | 8 | case "$1" in 9 | purge) 10 | rm -f /var/log/dhcpy6d.log* /var/lib/dhcpy6d/volatile.sqlite 11 | # Taken from ucf's postrm example 12 | for ext in '' '~' '%' .bak .ucf-new .ucf-old .ucf-dist; do 13 | rm -f "/etc/default/dhcpy6d$ext" 14 | done 15 | if which ucf >/dev/null; then 16 | ucf --purge /etc/default/dhcpy6d 17 | fi 18 | if which ucfr >/dev/null; then 19 | ucfr --purge dhcpy6d /etc/default/dhcpy6d 20 | fi 21 | ;; 22 | esac 23 | 24 | #DEBHELPER# 25 | -------------------------------------------------------------------------------- /debian/dhcpy6d.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=DHCPv6 Server Daemon 3 | Documentation=man:dhcpy6d(8) man:dhcpy6d.conf(5) man:dhcpy6d-clients.conf(5) 4 | Wants=network-online.target 5 | After=network-online.target 6 | After=time-sync.target 7 | 8 | [Service] 9 | EnvironmentFile=/etc/default/dhcpy6d 10 | ExecStart=/usr/sbin/dhcpy6d --config /etc/dhcpy6d.conf --user dhcpy6d --group dhcpy6d --really-do-it ${RUN} --duid ${DUID} 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /debian/manpages: -------------------------------------------------------------------------------- 1 | man/man8/dhcpy6d.8 2 | man/man5/dhcpy6d.conf.5 3 | man/man5/dhcpy6d-clients.conf.5 4 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | %: 3 | dh $@ --buildsystem=pybuild --with python3 --with systemd 4 | 5 | override_dh_auto_install: 6 | dh_auto_install -- --install-args="--install-scripts=/usr/sbin --install-layout=deb" 7 | rm -f debian/dhcpy6d/usr/share/doc/dhcpy6d/LICENSE 8 | rm -f debian/dhcpy6d/var/log/dhcpy6d.log 9 | rm -f debian/dhcpy6d/usr/share/doc/dhcpy6d/*.[0-9] 10 | find debian/dhcpy6d/ -name __pycache__ -print0 | xargs -0 --no-run-if-empty rm -rv 11 | mv -v debian/dhcpy6d/usr/lib/python3.11 debian/dhcpy6d/usr/lib/python3 12 | mv -v debian/dhcpy6d/var/lib/dhcpy6d/volatile.sqlite debian/dhcpy6d/usr/share/dhcpy6d/ 13 | 14 | override_dh_install: 15 | dh_install 16 | chmod 0644 debian/dhcpy6d/usr/share/dhcpy6d/default/dhcpy6d 17 | 18 | override_dh_installsystemd: 19 | dh_installsystemd --no-enable --no-start 20 | 21 | # make -f debian/rules get-orig-source 22 | get-orig-source: 23 | python setup.py sdist 24 | mv -v dist/dhcpy6d-*.tar.gz ../dhcpy6d_`dpkg-parsechangelog -SVersion | cut -d- -f1`.orig.tar.gz 25 | rm -r MANIFEST dist 26 | 27 | # there are no tests - build package anyway 28 | override_dh_auto_test: 29 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore=MANIFEST\.in 2 | extend-diff-ignore=README\.md 3 | extend-diff-ignore=build\.sh 4 | extend-diff-ignore=redhat 5 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=4 2 | https://github.com/HenriWahl/dhcpy6d/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz -------------------------------------------------------------------------------- /dhcpy6d/__init__.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | """Module dhcpy6d""" 20 | 21 | import socket 22 | import socketserver 23 | import struct 24 | 25 | from .config import cfg 26 | from .globals import (collected_macs, 27 | IF_NAME, 28 | IF_NUMBER, 29 | NC, 30 | OS, 31 | timer) 32 | from .helpers import (colonify_ip6, 33 | colonify_mac, 34 | correct_mac, 35 | decompress_ip6, 36 | NeighborCacheRecord) 37 | from .log import log 38 | from .storage import volatile_store 39 | 40 | 41 | class UDPMulticastIPv6(socketserver.UnixDatagramServer): 42 | """ 43 | modify server_bind to work with multicast 44 | add DHCPv6 multicast group ff02::1:2 45 | """ 46 | def server_bind(self): 47 | """ 48 | multicast & python: http://code.activestate.com/recipes/442490/ 49 | """ 50 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 51 | # multicast parameters 52 | # hop is one because it is all about the same subnet 53 | self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0) 54 | self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1) 55 | 56 | for i in cfg.INTERFACE: 57 | IF_NAME[i] = socket.if_nametoindex(i) 58 | IF_NUMBER[IF_NAME[i]] = i 59 | if_number = struct.pack('I', IF_NAME[i]) 60 | #mgroup = socket.inet_pton(socket.AF_INET6, cfg.MCAST) + if_number 61 | # no need vor variable.... the DHCPv6 multicast address is predefined 62 | mgroup = socket.inet_pton(socket.AF_INET6, 'ff02::1:2') + if_number 63 | 64 | # join multicast group - should work definitively if not ignoring interface at startup 65 | if cfg.IGNORE_INTERFACE: 66 | try: 67 | self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mgroup) 68 | except Exception as err: 69 | print(err) 70 | else: 71 | self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mgroup) 72 | 73 | # bind socket to server address 74 | self.socket.bind(self.server_address) 75 | 76 | # attempt to avoid blocking 77 | self.socket.setblocking(False) 78 | 79 | # some more requests? 80 | self.request_queue_size = 100 81 | -------------------------------------------------------------------------------- /dhcpy6d/client/default.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from ..config import (Address, 20 | cfg, 21 | Prefix) 22 | from ..constants import CONST 23 | from ..domain import get_ip_from_dns 24 | 25 | from .parse_pattern import (parse_pattern_address, 26 | parse_pattern_prefix) 27 | 28 | 29 | def default(client=None, client_config=None, transaction=None): 30 | # use default class if host is unknown 31 | client.hostname = transaction.hostname 32 | client.client_class = 'default_' + transaction.interface 33 | # apply answer type of client to transaction - useful if no answer or no address available is configured 34 | transaction.answer = cfg.CLASSES[client.client_class].ANSWER 35 | 36 | if 'addresses' in cfg.CLASSES['default_' + transaction.interface].ADVERTISE and \ 37 | (3 or 4) in transaction.ia_options: 38 | for address in cfg.CLASSES['default_' + transaction.interface].ADDRESSES: 39 | # addresses of category 'dns' will be searched in DNS 40 | if cfg.ADDRESSES[address].CATEGORY == 'dns': 41 | a = get_ip_from_dns(client.hostname) 42 | else: 43 | a = parse_pattern_address(cfg.ADDRESSES[address], client, transaction) 44 | if a: 45 | ia = Address(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE, 46 | preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME, 47 | valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME, 48 | category=cfg.ADDRESSES[address].CATEGORY, 49 | aclass=cfg.ADDRESSES[address].CLASS, 50 | atype=cfg.ADDRESSES[address].TYPE, 51 | dns_update=cfg.ADDRESSES[address].DNS_UPDATE, 52 | dns_zone=cfg.ADDRESSES[address].DNS_ZONE, 53 | dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE, 54 | dns_ttl=cfg.ADDRESSES[address].DNS_TTL) 55 | client.addresses.append(ia) 56 | 57 | if 'prefixes' in cfg.CLASSES['default_' + transaction.interface].ADVERTISE and \ 58 | CONST.OPTION.IA_PD in transaction.ia_options: 59 | 60 | for prefix in cfg.CLASSES['default_' + transaction.interface].PREFIXES: 61 | p = parse_pattern_prefix(cfg.PREFIXES[prefix], client_config, transaction) 62 | # in case range has been exceeded p will be None 63 | if p: 64 | ia_pd = Prefix(prefix=p, 65 | length=cfg.PREFIXES[prefix].LENGTH, 66 | preferred_lifetime=cfg.PREFIXES[prefix].PREFERRED_LIFETIME, 67 | valid_lifetime=cfg.PREFIXES[prefix].VALID_LIFETIME, 68 | category=cfg.PREFIXES[prefix].CATEGORY, 69 | pclass=cfg.PREFIXES[prefix].CLASS, 70 | ptype=cfg.PREFIXES[prefix].TYPE, 71 | route_link_local=cfg.PREFIXES[prefix].ROUTE_LINK_LOCAL) 72 | client.prefixes.append(ia_pd) 73 | 74 | # given client has been modified successfully 75 | return True 76 | -------------------------------------------------------------------------------- /dhcpy6d/client/from_config.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from ..config import (Address, 20 | cfg, 21 | Prefix) 22 | from ..constants import CONST 23 | from ..domain import get_ip_from_dns 24 | 25 | from .parse_pattern import (parse_pattern_address, 26 | parse_pattern_prefix) 27 | 28 | 29 | def from_config(client=None, client_config=None, transaction=None): 30 | # give client hostname + class 31 | client.hostname = client_config.HOSTNAME 32 | client.client_class = client_config.CLASS 33 | # apply answer type of client to transaction - useful if no answer or no address available is configured 34 | transaction.answer = cfg.CLASSES[client.client_class].ANSWER 35 | # continue only if request interface matches class interfaces 36 | if transaction.interface in cfg.CLASSES[client.client_class].INTERFACE: 37 | # if fixed addresses are given build them 38 | if client_config.ADDRESS is not None and \ 39 | CONST.OPTION.IA_NA in transaction.ia_options: 40 | for address in client_config.ADDRESS: 41 | if len(address) > 0: 42 | # fixed addresses are assumed to be non-temporary 43 | # 44 | # todo: lifetime of address should be set by config too 45 | # 46 | ia = Address(address=address, 47 | ia_type='na', 48 | preferred_lifetime=cfg.PREFERRED_LIFETIME, 49 | valid_lifetime=cfg.VALID_LIFETIME, 50 | category='fixed', 51 | aclass='fixed', 52 | atype='fixed') 53 | client.addresses.append(ia) 54 | 55 | # if fixed prefixes are given and requested build them 56 | if client_config.PREFIX is not None and \ 57 | CONST.OPTION.IA_PD in transaction.ia_options: 58 | for prefix in client_config.PREFIX: 59 | ia_pd = Prefix(prefix=prefix['address'], 60 | length=prefix['length'], 61 | preferred_lifetime=cfg.PREFERRED_LIFETIME, 62 | valid_lifetime=cfg.VALID_LIFETIME, 63 | route_link_local=client_config.PREFIX_ROUTE_LINK_LOCAL) 64 | client.prefixes.append(ia_pd) 65 | 66 | if not client_config.CLASS == '': 67 | # add all addresses which belong to that class 68 | for address in cfg.CLASSES[client_config.CLASS].ADDRESSES: 69 | # addresses of category 'dns' will be searched in DNS 70 | if cfg.ADDRESSES[address].CATEGORY == 'dns': 71 | a = get_ip_from_dns(client.hostname) 72 | else: 73 | a = parse_pattern_address(cfg.ADDRESSES[address], client_config, transaction) 74 | # in case range has been exceeded a will be None 75 | if a: 76 | ia = Address(address=a, 77 | ia_type=cfg.ADDRESSES[address].IA_TYPE, 78 | preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME, 79 | valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME, 80 | category=cfg.ADDRESSES[address].CATEGORY, 81 | aclass=cfg.ADDRESSES[address].CLASS, 82 | atype=cfg.ADDRESSES[address].TYPE, 83 | dns_update=cfg.ADDRESSES[address].DNS_UPDATE, 84 | dns_zone=cfg.ADDRESSES[address].DNS_ZONE, 85 | dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE, 86 | dns_ttl=cfg.ADDRESSES[address].DNS_TTL) 87 | client.addresses.append(ia) 88 | 89 | # add all bootfiles which belong to that class 90 | for bootfile in cfg.CLASSES[client_config.CLASS].BOOTFILES: 91 | client_architecture = cfg.BOOTFILES[bootfile].CLIENT_ARCHITECTURE 92 | user_class = cfg.BOOTFILES[bootfile].USER_CLASS 93 | 94 | # check if transaction attributes matches the bootfile defintion 95 | if (not client_architecture or 96 | transaction.client_architecture == client_architecture or 97 | transaction.known_client_architecture == client_architecture) and \ 98 | (not user_class or 99 | transaction.user_class == user_class): 100 | client.bootfiles.append(cfg.BOOTFILES[bootfile]) 101 | 102 | # if prefixes are advertised in this class and the client demands a prefix (trough IA_PD) 103 | if 'prefixes' in cfg.CLASSES[client_config.CLASS].ADVERTISE and \ 104 | CONST.OPTION.IA_PD in transaction.ia_options: 105 | for prefix in cfg.CLASSES[client_config.CLASS].PREFIXES: 106 | p = parse_pattern_prefix(cfg.PREFIXES[prefix], client_config, transaction) 107 | # in case range has been exceeded p will be None 108 | if p: 109 | ia_pd = Prefix(prefix=p, 110 | length=cfg.PREFIXES[prefix].LENGTH, 111 | preferred_lifetime=cfg.PREFIXES[prefix].PREFERRED_LIFETIME, 112 | valid_lifetime=cfg.PREFIXES[prefix].VALID_LIFETIME, 113 | category=cfg.PREFIXES[prefix].CATEGORY, 114 | pclass=cfg.PREFIXES[prefix].CLASS, 115 | ptype=cfg.PREFIXES[prefix].TYPE, 116 | route_link_local=cfg.PREFIXES[prefix].ROUTE_LINK_LOCAL) 117 | client.prefixes.append(ia_pd) 118 | 119 | # if client link local address should be used for prefix routing enable it 120 | if client_config.PREFIX_ROUTE_LINK_LOCAL: 121 | client.prefix_route_link_local = True 122 | 123 | if client_config.ADDRESS == client_config.CLASS == '': 124 | # use default class if no class or address is given 125 | for address in cfg.CLASSES['default_' + transaction.interface].ADDRESSES: 126 | client.client_class = 'default_' + transaction.interface 127 | # addresses of category 'dns' will be searched in DNS 128 | if cfg.ADDRESSES[address].CATEGORY == 'dns': 129 | a = get_ip_from_dns(client.hostname) 130 | else: 131 | a = parse_pattern_address(cfg.ADDRESSES[address], client_config, transaction) 132 | if a: 133 | ia = Address(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE, 134 | preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME, 135 | valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME, 136 | category=cfg.ADDRESSES[address].CATEGORY, 137 | aclass=cfg.ADDRESSES[address].CLASS, 138 | atype=cfg.ADDRESSES[address].TYPE, 139 | dns_update=cfg.ADDRESSES[address].DNS_UPDATE, 140 | dns_zone=cfg.ADDRESSES[address].DNS_ZONE, 141 | dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE, 142 | dns_ttl=cfg.ADDRESSES[address].DNS_TTL) 143 | client.addresses.append(ia) 144 | 145 | for bootfile in cfg.CLASSES['default_' + transaction.interface].BOOTFILES: 146 | client_architecture = bootfile.CLIENT_ARCHITECTURE 147 | user_class = bootfile.USER_CLASS 148 | 149 | # check if transaction attributes match the bootfile definition 150 | if (not client_architecture or 151 | transaction.client_architecture == client_architecture or 152 | transaction.known_client_architecture == client_architecture) and \ 153 | (not user_class or 154 | transaction.user_class == user_class): 155 | client.bootfiles.append(bootfile) 156 | 157 | # given client has been modified successfully 158 | return True 159 | -------------------------------------------------------------------------------- /dhcpy6d/constants.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import socket 20 | import struct 21 | 22 | # DHCPv6 23 | MESSAGE = {1: 'SOLICIT', 24 | 2: 'ADVERTISE', 25 | 3: 'REQUEST', 26 | 4: 'CONFIRM', 27 | 5: 'RENEW', 28 | 6: 'REBIND', 29 | 7: 'REPLY', 30 | 8: 'RELEASE', 31 | 9: 'DECLINE', 32 | 10: 'RECONFIGURE', 33 | 11: 'INFORMATION-REQUEST', 34 | 12: 'RELAY-FORW', 35 | 13: 'RELAY-REPL'} 36 | 37 | # see http://www.iana.org/assignments/dhcpv6-parameters/ 38 | OPTION = {1: 'CLIENTID', 39 | 2: 'SERVERID', 40 | 3: 'IA_NA', 41 | 4: 'IA_TA', 42 | 5: 'IAADDR', 43 | 6: 'ORO', 44 | 7: 'PREFERENCE', 45 | 8: 'ELAPSED_TIME', 46 | 9: 'RELAY_MSG', 47 | 11: 'AUTH', 48 | 12: 'UNICAST', 49 | 13: 'STATUS_CODE', 50 | 14: 'RAPID_COMMIT', 51 | 15: 'USER_CLASS', 52 | 16: 'VENDOR_CLASS', 53 | 17: 'VENDOR_OPTS', 54 | 18: 'INTERFACE_ID', 55 | 19: 'RECONF_MSG', 56 | 20: 'RECONF_ACCEPT', 57 | 21: 'SIP_SERVER_D', 58 | 22: 'SIP_SERVER_A', 59 | 23: 'DNS_SERVERS', 60 | 24: 'DOMAIN_LIST', 61 | 25: 'IA_PD', 62 | 26: 'IAPREFIX', 63 | 27: 'NIS_SERVERS', 64 | 28: 'NISP_SERVERS', 65 | 29: 'NIS_DOMAIN_NAME', 66 | 30: 'NISP_DOMAIN_NAME', 67 | 31: 'SNTP_SERVERS', 68 | 32: 'INFORMATION_REFRESH_TIME', 69 | 33: 'BCMCS_SERVER_D', 70 | 34: 'BCMCS_SERVER_A', 71 | 36: 'GEOCONF_CIVIC', 72 | 37: 'REMOTE_ID', 73 | 38: 'SUBSCRIBER_ID', 74 | 39: 'CLIENT_FQDN', 75 | 40: 'PANA_AGENT', 76 | 41: 'NEW_POSIX_TIMEZONE', 77 | 42: 'NEW_TZDB_TIMEZONE', 78 | 43: 'ERO', 79 | 44: 'LQ_QUERY', 80 | 45: 'CLIENT_DATA', 81 | 46: 'CLT_TIME', 82 | 47: 'LQ_RELAY_DATA', 83 | 48: 'LQ_CLIENT_LINK', 84 | 49: 'MIP6_HNINF', 85 | 50: 'MIP6_RELAY', 86 | 51: 'V6_LOST', 87 | 52: 'CAPWAP_AC_V6', 88 | 53: 'RELAY_ID', 89 | 54: 'IPv6_Address_MoS', 90 | 55: 'Pv6_FQDN_MoS', 91 | 56: 'NTP_SERVER', 92 | 57: 'V6_ACCESS_DOMAIN', 93 | 58: 'SIP_UA_CS_LIST', 94 | 59: 'BOOTFILE_URL', 95 | 60: 'BOOTFILE_PARAM', 96 | 61: 'CLIENT_ARCH_TYPE', 97 | 82: 'SOL_MAX_RT', 98 | 83: 'INF_MAX_RT' 99 | } 100 | 101 | STATUS = {0: 'Success', 102 | 1: 'Failure', 103 | 2: 'No Addresses available', 104 | 3: 'No Binding', 105 | 4: 'Prefix not appropriate for link', 106 | 5: 'Use Multicast', 107 | 6: 'No Prefix available'} 108 | 109 | # see https://tools.ietf.org/html/rfc4578#section-2.1 110 | ARCHITECTURE_TYPE = {0: 'Intel x86PC', 111 | 1: 'NEC / PC98', 112 | 2: 'EFI Itanium', 113 | 3: 'DEC Alpha', 114 | 4: 'Arc x86', 115 | 5: 'Intel Lean Client', 116 | 6: 'EFI IA32', 117 | 7: 'EFI BC', 118 | 8: 'EFI Xscale', 119 | 9: 'EFI x86 - 64'} 120 | 121 | # used for NETLINK in get_neighbor_cache_linux() access by Github/vokac 122 | RTM_NEWNEIGH = 28 123 | RTM_DELNEIGH = 29 124 | RTM_GETNEIGH = 30 125 | NLM_F_REQUEST = 1 126 | # Modifiers to GET request 127 | NLM_F_ROOT = 0x100 128 | NLM_F_MATCH = 0x200 129 | NLM_F_DUMP = (NLM_F_ROOT | NLM_F_MATCH) 130 | # NETLINK message is always the same except header seq 131 | MSG = struct.pack('B', socket.AF_INET6) 132 | # always the same length... 133 | MSG_HEADER_LENGTH = 17 134 | # ...type... 135 | MSG_HEADER_TYPE = RTM_GETNEIGH 136 | # ...flags. 137 | MSG_HEADER_FLAGS = (NLM_F_REQUEST | NLM_F_DUMP) 138 | NLMSG_NOOP = 0x1 # /* Nothing. */ 139 | NLMSG_ERROR = 0x2 # /* Error */ 140 | NLMSG_DONE = 0x3 # /* End of a dump */ 141 | NLMSG_OVERRUN = 0x4 # /* Data lost */ 142 | 143 | NUD_INCOMPLETE = 0x01 144 | # state of peer 145 | NUD_REACHABLE = 0x02 146 | NUD_STALE = 0x04 147 | NUD_DELAY = 0x08 148 | NUD_PROBE = 0x10 149 | NUD_FAILED = 0x20 150 | NUD_NOARP = 0x40 151 | NUD_PERMANENT = 0x80 152 | NUD_NONE = 0x00 153 | 154 | NDA = { 155 | 0: 'NDA_UNSPEC', 156 | 1: 'NDA_DST', 157 | 2: 'NDA_LLADDR', 158 | 3: 'NDA_CACHEINFO', 159 | 4: 'NDA_PROBES', 160 | 5: 'NDA_VLAN', 161 | 6: 'NDA_PORT', 162 | 7: 'NDA_VNI', 163 | 8: 'NDA_IFINDEX', 164 | } 165 | NLMSG_ALIGNTO = 4 166 | NLA_ALIGNTO = 4 167 | 168 | 169 | # collect most constants in a class for easier handling by calling numeric values via class properties 170 | # at the same time still available with integer keys and string values 171 | class Constants: 172 | """ 173 | contains various categories of constants 174 | """ 175 | class Category: 176 | """ 177 | category containing constants 178 | 'reverting' the dictionary because in certain parts for example the number of an option is referred to by 179 | its name as property 180 | """ 181 | def __init__(self, category): 182 | for key, value in category.items(): 183 | self.__dict__[value.replace('-', '_').replace(' ', '_').replace('/', 'or').upper()] = key 184 | 185 | def keys(self): 186 | # return key 187 | return self.__dict__.keys() 188 | 189 | def __init__(self): 190 | self.MESSAGE = self.Category(MESSAGE) 191 | self.STATUS = self.Category(STATUS) 192 | self.OPTION = self.Category(OPTION) 193 | # needed for logging - use original dict 194 | self.MESSAGE_DICT = MESSAGE 195 | # architecture types as dict 196 | self.ARCHITECTURE_TYPE_DICT = ARCHITECTURE_TYPE 197 | 198 | 199 | # Add constants for global access 200 | CONST = Constants() 201 | -------------------------------------------------------------------------------- /dhcpy6d/domain.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import copy 20 | 21 | from dns.resolver import (NoAnswer, 22 | NoNameservers) 23 | 24 | from .config import cfg 25 | from .globals import (dns_query_queue, 26 | resolver_query) 27 | from .helpers import decompress_ip6 28 | from .storage import volatile_store 29 | 30 | 31 | def dns_update(transaction, action='update'): 32 | """ 33 | update DNS entries on specified nameserver 34 | at the moment this only works with Bind 35 | uses all addresses of client if they want to be dynamically updated 36 | 37 | regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: 38 | - client wants to update DNS itself -> sends 0 0 0 39 | - client wants server to update DNS -> sends 0 0 1 40 | - client wants no server DNS update -> sends 1 0 0 41 | """ 42 | if transaction.client: 43 | # if allowed use client supplied hostname, otherwise that from config 44 | if cfg.DNS_USE_CLIENT_HOSTNAME: 45 | # hostname from transaction 46 | hostname = transaction.hostname 47 | else: 48 | # hostname from client info built from configuration 49 | hostname = transaction.client.hostname 50 | 51 | # if address should be updated in DNS update it 52 | for a in transaction.client.addresses: 53 | if a.DNS_UPDATE and hostname != '' and a.VALID: 54 | if cfg.DNS_IGNORE_CLIENT or transaction.dns_s == 1: 55 | # put query into DNS query queue 56 | dns_query_queue.put((action, hostname, a)) 57 | return True 58 | else: 59 | return False 60 | 61 | 62 | def dns_delete(transaction, address='', action='release'): 63 | """ 64 | delete DNS entries on specified nameserver 65 | at the moment this only works with ISC Bind 66 | """ 67 | hostname, duid, mac, iaid = volatile_store.get_host_lease(address) 68 | 69 | # if address should be updated in DNS update it 70 | # local flag to check if address should be deleted from DNS 71 | delete = False 72 | 73 | for a in list(cfg.ADDRESSES.values()): 74 | # if there is any address type which prototype matches use its DNS ZONE 75 | if a.matches_prototype(address): 76 | # kind of RCF-compliant security measure - check if hostname and DUID from transaction fits them of store 77 | if duid == transaction.duid and \ 78 | iaid == transaction.iaid: 79 | delete = True 80 | # also check MAC address if MAC counts in general - not RFCish 81 | if 'mac' in cfg.IDENTIFICATION: 82 | if not mac == transaction.mac: 83 | delete = False 84 | 85 | if hostname != '' and delete: 86 | # use address from address types as template for the real 87 | # address to be deleted from DNS 88 | dns_address = copy.copy(a) 89 | dns_address.ADDRESS = address 90 | # put query into DNS query queue 91 | dns_query_queue.put((action, hostname, dns_address)) 92 | # enough 93 | break 94 | 95 | 96 | def get_ip_from_dns(hostname): 97 | """ 98 | Get IPv6 address from DNS for address category 'dns' 99 | """ 100 | try: 101 | answer = resolver_query.query(hostname, 'AAAA') 102 | return decompress_ip6(answer.rrset.to_text().split(' ')[-1]) 103 | except NoAnswer: 104 | return False 105 | except NoNameservers: 106 | return False 107 | -------------------------------------------------------------------------------- /dhcpy6d/globals.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import queue 20 | import platform 21 | import time 22 | 23 | import dns.resolver 24 | import dns.tsigkeyring 25 | 26 | from .config import cfg 27 | from .constants import CONST 28 | 29 | # dummy value is None for DNS related 30 | resolver_query = None 31 | resolver_update = None 32 | keyring = None 33 | 34 | # if nameserver is given create resolver 35 | if len(cfg.NAMESERVER) > 0: 36 | # default nameservers for DNS queries 37 | resolver_query = dns.resolver.Resolver() 38 | resolver_query.nameservers = cfg.NAMESERVER 39 | 40 | # RNDC Key for DNS updates from ISC Bind /etc/rndc.key 41 | if cfg.DNS_UPDATE: 42 | if cfg.DNS_USE_RNDC: 43 | keyring = dns.tsigkeyring.from_text({cfg.DNS_RNDC_KEY: cfg.DNS_RNDC_SECRET}) 44 | # resolver for DNS updates 45 | resolver_update = dns.resolver.Resolver() 46 | resolver_update.nameservers = [cfg.DNS_UPDATE_NAMESERVER] 47 | 48 | 49 | class Timer: 50 | """ 51 | global object containing time set by TimerThread 52 | """ 53 | __time = 0 54 | 55 | def __init__(self): 56 | self.time = time.time() 57 | 58 | @property 59 | def time(self): 60 | return self.__time 61 | 62 | @time.setter 63 | def time(self, new_time): 64 | self.__time = int(new_time) 65 | 66 | 67 | # global time variable, synchronized by TimerThread 68 | timer = Timer() 69 | 70 | # dictionary to store transactions - key is transaction ID, value a transaction object 71 | transactions = {} 72 | # collected MAC addresses from clients, mapping to link local IPs 73 | collected_macs = {} 74 | 75 | # queues for queries 76 | config_query_queue = queue.Queue() 77 | config_answer_queue = queue.Queue() 78 | volatile_query_queue = queue.Queue() 79 | volatile_answer_queue = queue.Queue() 80 | 81 | # queue for dns actualization 82 | dns_query_queue = queue.Queue() 83 | 84 | # queue for executing some script to modify routes after delegating prefixes 85 | route_queue = queue.Queue() 86 | 87 | # attempt to log connections and count them to find out which clients do silly crazy brute force 88 | requests = {} 89 | requests_blacklist = {} 90 | 91 | # save OS 92 | OS = platform.system() 93 | if 'BSD' in OS: 94 | OS = 'BSD' 95 | 96 | # platform-dependant neighbor cache call 97 | # every platform has its different output 98 | # dev, llip and mac are positions of output of call 99 | # len is minimal length a line has to have to be evaluable 100 | # 101 | # update: has been different to Linux which now access neighbor cache natively 102 | NC = {'BSD': {'call': '/usr/sbin/ndp -a -n', 103 | 'dev': 2, 104 | 'llip': 0, 105 | 'mac': 1, 106 | 'len': 3}, 107 | 'Darwin': {'call': '/usr/sbin/ndp -a -n', 108 | 'dev': 2, 109 | 'llip': 0, 110 | 'mac': 1, 111 | 'len': 3} 112 | } 113 | 114 | # libc access via ctypes, needed for interface handling, get it by helpers.get_libc() 115 | # obsolete in Python 3 116 | # LIBC = get_libc() 117 | 118 | # index IF name > number, gets filled in UDPMulticastIPv6 119 | IF_NAME = {} 120 | # index IF number > name 121 | IF_NUMBER = {} 122 | 123 | # IA_NA, IA_TA and IA_PD Options referred here in handler 124 | IA_OPTIONS = (CONST.OPTION.IA_NA, 125 | CONST.OPTION.IA_TA, 126 | CONST.OPTION.IA_PD) 127 | 128 | # options to be ignored when logging 129 | IGNORED_LOG_OPTIONS = ['options', 'client', 'client_config_dicts', 'timestamp', 'iat1', 'iat2', 'id'] 130 | 131 | # empty options string test 132 | EMPTY_OPTIONS = [None, False, '', []] 133 | 134 | # dummy IAID for transactions 135 | DUMMY_IAID = '00000000' 136 | 137 | # dummy MAC for transactions 138 | DUMMY_MAC = '00:00:00:00:00:00' 139 | 140 | # store 141 | # because of thread trouble there should not be too much db connections at once 142 | # so we need to use the queryqueue way - subject to change 143 | # source of configuration of hosts 144 | # use client configuration only if needed 145 | config_store = volatile_store = None 146 | -------------------------------------------------------------------------------- /dhcpy6d/helpers.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import (hexlify, 20 | unhexlify) 21 | import shlex 22 | import socket 23 | import sys 24 | 25 | # whitespace for options with more than one value 26 | WHITESPACE = ' ,' 27 | 28 | # define address characters once - for decompress_ipv6 29 | ADDRESS_CHARS_STRICT = ':0123456789abcdef' 30 | ADDRESS_CHARS_NON_STRICT = ':0123456789abcdefx' 31 | 32 | # localhost 33 | LOCALHOST = '::1' 34 | LOCALHOST_LLIP = '00000000000000000000000000000001' 35 | LOCALHOST_INTERFACES = ['', 'lo', 'lo0'] 36 | 37 | 38 | class Interface: 39 | """ 40 | hold interface information 41 | interface information comes in tuple from socket.if_nameindex() 42 | """ 43 | 44 | def __init__(self, interface_tuple): 45 | self.index, self.name = interface_tuple 46 | 47 | 48 | class NeighborCacheRecord: 49 | """ 50 | object for neighbor cache entries to be returned by get_neighbor_cache_linux() and in CollectedMACs 51 | .interface is only interesting for real neighbor cache records, to be ignored for collected MACs stored in DB 52 | """ 53 | 54 | def __init__(self, llip='', mac='', interface='', now=0): 55 | self.llip = llip 56 | self.mac = mac 57 | self.interface = interface 58 | self.timestamp = now 59 | 60 | 61 | def convert_dns_to_binary(name): 62 | """ 63 | convert domain name as described in RFC 1035, 3.1 64 | """ 65 | binary = '' 66 | domain_parts = name.split('.') 67 | for domain_part in domain_parts: 68 | binary += f'{len(domain_part):02x}' # length of Domain Name Segments 69 | binary += hexlify(domain_part.encode()).decode() 70 | # final zero size octet following RFC 1035 71 | binary += '00' 72 | return binary 73 | 74 | 75 | def convert_binary_to_dns(binary): 76 | """ 77 | convert domain name from hex like in RFC 1035, 3.1 78 | """ 79 | name = '' 80 | binary_parts = binary 81 | while len(binary_parts) > 0: 82 | # RFC 1035 - domain names are sequences of labels separated by length octets 83 | length = int(binary_parts[0:2], 16) 84 | # lenght*2 because 2 charse represent a byte 85 | label = unhexlify(binary_parts[2:2 + length * 2]).decode() 86 | binary_parts = binary_parts[2 + length * 2:] 87 | name += label 88 | # insert '.' if this is not the last label of FQDN 89 | # >2 because last byte is the zero byte terminator 90 | if len(binary_parts) > 2: 91 | name += '.' 92 | return str(name) 93 | 94 | 95 | def build_option(number, payload): 96 | """ 97 | glue option with payload 98 | """ 99 | # option number and length take 2 byte each so the string has to be 4 chars long 100 | option = f'{number:04x}' # option number 101 | option += f'{len(payload) // 2:04x}' # payload length, /2 because 2 chars are 1 byte 102 | option += payload 103 | return option 104 | 105 | 106 | def correct_mac(mac): 107 | """ 108 | OpenBSD shortens MAC addresses in ndp output - here they grow again 109 | """ 110 | decompressed = [f'{(int(m, 16)):02x}' for m in mac.split(':')] 111 | return ':'.join(decompressed) 112 | 113 | 114 | def colonify_mac(mac): 115 | """ 116 | return complete MAC address with colons 117 | """ 118 | if type(mac) == bytes: 119 | mac = mac.decode() 120 | return ':'.join((mac[0:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:12])) 121 | 122 | 123 | def decompress_ip6(ip6, strict=True): 124 | """ 125 | decompresses shortened IPv6 address and returns it as ':'-less 32 character string 126 | additionally allows testing for prototype address with less strict set of allowed characters 127 | """ 128 | 129 | ip6 = ip6.lower() 130 | # cache some repeated calls 131 | colon_count1 = ip6.count(':') 132 | colon_count2 = ip6.count('::') 133 | colon_count3 = ip6.count(':::') 134 | 135 | # if in strict mode there are no hex numbers and ':' something is wrong 136 | if strict: 137 | for c in ip6: 138 | if c not in ADDRESS_CHARS_STRICT: 139 | raise Exception(f'{ip6} should consist only of : 0 1 2 3 4 5 6 7 8 9 a b c d e f') 140 | else: 141 | # used for comparison of leases with address pattern - X replace the dynamic part of the address 142 | for c in ip6: 143 | if c not in ADDRESS_CHARS_NON_STRICT: 144 | raise Exception(f'{ip6} should consist only of : 0 1 2 3 4 5 6 7 8 9 a b c d e f x') 145 | # nothing to do 146 | if len(ip6) == 32 and colon_count1 == 0: 147 | return ip6 148 | 149 | # larger heaps of :: smell like something wrong 150 | if colon_count2 > 1 or colon_count3 >= 1: 151 | raise Exception(f"{ip6} has too many accumulated ':'") 152 | 153 | # less than 7 ':' but no '::' also make a bad impression 154 | if colon_count1 < 7 and colon_count2 != 1: 155 | raise Exception(f"{ip6} is missing some ':'") 156 | 157 | # replace :: with :0000:: - the last ':' will be cut of finally 158 | while ip6.count(':') < 8 and ip6.count('::') == 1: 159 | ip6 = ip6.replace('::', ':0000::') 160 | 161 | # remaining ':' will be cut off 162 | ip6 = ip6.replace('::', ':') 163 | 164 | # ':' at the beginning have to be filled up with 0000 too 165 | if ip6.startswith(':'): 166 | ip6 = '0000' + ip6 167 | 168 | # if a segment is shorter than 4 chars the gaps get filled with zeros 169 | ip6_segments_source = ip6.split(':') 170 | ip6_segments_target = list() 171 | for s in ip6_segments_source: 172 | if len(s) > 4: 173 | raise Exception(f"{ip6} has segment with more than 4 digits") 174 | else: 175 | ip6_segments_target.append(s.zfill(4)) 176 | 177 | # return with separator (mostly '') 178 | return ''.join(ip6_segments_target) 179 | 180 | 181 | def colonify_ip6(address): 182 | """ 183 | return complete IPv6 address with colons 184 | """ 185 | if address: 186 | if type(address) == bytes: 187 | address = address.decode() 188 | return ':'.join((address[0:4], address[4:8], address[8:12], address[12:16], 189 | address[16:20], address[20:24], address[24:28], address[28:32])) 190 | else: 191 | # return 'n/a' 192 | # provoke crash to see what happens with un-addresses 193 | print('Uncolonifyable address:', address) 194 | return False 195 | 196 | 197 | def convert_prefix_inline(prefix): 198 | """ 199 | check if supplied prefix is valid and convert it 200 | """ 201 | address, length = split_prefix(prefix) 202 | address = decompress_ip6(address) 203 | return {"address": address, 204 | "length": length} 205 | 206 | 207 | def combine_prefix_length(prefix, length): 208 | """ 209 | add prefix and length to 'prefix/length' string 210 | """ 211 | return f'{prefix}/{length}' 212 | 213 | 214 | def split_prefix(prefix): 215 | """ 216 | split prefix and length from 'prefix/length' notation 217 | """ 218 | return prefix.split('/') 219 | 220 | 221 | def decompress_prefix(prefix, length): 222 | """ 223 | return prefix with decompressed address part 224 | """ 225 | return combine_prefix_length(decompress_ip6(prefix), length) 226 | 227 | 228 | def error_exit(message='An error occured.', status=1): 229 | """ 230 | exit with given error message 231 | allow prefix, especially for spitting out section of configuration errors 232 | """ 233 | sys.stderr.write(f'\n{message}\n\n') 234 | sys.exit(status) 235 | 236 | 237 | def listify_option(option): 238 | """ 239 | return any comma or space separated option as list 240 | if erroneously a list has been given return it unchanged 241 | """ 242 | if option: 243 | if type(option) == str: 244 | lex = shlex.shlex(option) 245 | lex.whitespace = WHITESPACE 246 | lex.wordchars += ':.-/' 247 | return list(lex) 248 | elif type(option) == list: 249 | return(option) 250 | else: 251 | return False 252 | else: 253 | return None 254 | 255 | 256 | def send_control_message(message): 257 | """ 258 | Send a control message to the locally running dhcpy6d daemon 259 | """ 260 | # clean message of quotations marks 261 | message = message.strip('"').encode('utf8') 262 | socket_control = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 263 | socket_control.sendto(message, ('::1', 547)) 264 | 265 | 266 | def convert_mac_to_eui64(mac): 267 | """ 268 | Convert a MAC address to a EUI64 address 269 | """ 270 | # http://tools.ietf.org/html/rfc4291#section-2.5.1 271 | # only ':' come in MACs from get_neighbor_cache_linux() 272 | eui64 = mac.replace(':', '') 273 | eui64 = eui64[0:6] + 'fffe' + eui64[6:] 274 | eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:] 275 | 276 | split_string = lambda x, n: [x[i:i + n] for i in range(0, len(x), n)] 277 | 278 | return ':'.join(split_string(eui64, 4)) 279 | 280 | 281 | def get_interfaces(): 282 | """ 283 | return dict full of Interface objects 284 | :return: 285 | """ 286 | interfaces = {} 287 | for interface_tuple in socket.if_nameindex(): 288 | interface = Interface(interface_tuple) 289 | interfaces[interface.name] = interface 290 | return interfaces 291 | -------------------------------------------------------------------------------- /dhcpy6d/log.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from grp import getgrnam 20 | import logging 21 | from logging import (Formatter, 22 | getLogger, 23 | StreamHandler) 24 | from logging.handlers import (SysLogHandler, 25 | WatchedFileHandler) 26 | from os import chown 27 | from pwd import getpwnam 28 | from socket import gethostname 29 | 30 | from .config import cfg 31 | 32 | # globally available logging instace 33 | log = getLogger('dhcpy6d') 34 | 35 | if cfg.LOG: 36 | formatter = Formatter('{asctime} {name} {levelname} {message}', style='{') 37 | log.setLevel(logging.__dict__[cfg.LOG_LEVEL]) 38 | if cfg.LOG_FILE != '': 39 | chown(cfg.LOG_FILE, getpwnam(cfg.USER).pw_uid, getgrnam(cfg.GROUP).gr_gid) 40 | log_handler = WatchedFileHandler(cfg.LOG_FILE) 41 | log_handler.setFormatter(formatter) 42 | log.addHandler(log_handler) 43 | # std err console output 44 | if cfg.LOG_CONSOLE: 45 | log_handler = StreamHandler() 46 | log_handler.setFormatter(formatter) 47 | log.addHandler(log_handler) 48 | if cfg.LOG_SYSLOG: 49 | # time should be added by syslog daemon 50 | hostname = gethostname().split('.')[0] 51 | formatter = Formatter(hostname + ' {name} {levelname} {message}', style='{') 52 | # if /socket/file is given use this as address 53 | if cfg.LOG_SYSLOG_DESTINATION.startswith('/'): 54 | destination = cfg.LOG_SYSLOG_DESTINATION 55 | # if host and port are defined use them... 56 | elif cfg.LOG_SYSLOG_DESTINATION.count(':') == 1: 57 | destination = tuple(cfg.LOG_SYSLOG_DESTINATION.split(':')) 58 | # ...otherwise add port 514 to given host address 59 | else: 60 | destination = (cfg.LOG_SYSLOG_DESTINATION, 514) 61 | log_handler = SysLogHandler(address=destination, 62 | facility=SysLogHandler.__dict__['LOG_' + cfg.LOG_SYSLOG_FACILITY]) 63 | log_handler.setFormatter(formatter) 64 | log.addHandler(log_handler) 65 | -------------------------------------------------------------------------------- /dhcpy6d/options/__init__.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import importlib.util 20 | import pathlib 21 | import re 22 | 23 | 24 | class OptionTemplate: 25 | """ 26 | Template to be used by derived options - default and custom ones 27 | """ 28 | number = 0 29 | 30 | def __init__(self, number): 31 | self.number = number 32 | 33 | def build(self, **kwargs): 34 | """ 35 | to be filled with life by every single option 36 | every option has its special treatment of input and output data per request 37 | return default dummy values 38 | """ 39 | return '', False 40 | 41 | def initialize(self, **kwargs): 42 | """ 43 | to be filled with life by every single option 44 | every transaction has the opportunity to add options, depending on request 45 | """ 46 | pass 47 | 48 | @staticmethod 49 | def convert_to_string(number, payload): 50 | """ 51 | glue option number with payload 52 | """ 53 | # option number and length take 2 byte each so the string has to be 4 chars long 54 | option_string = f'{number:04x}' # option number 55 | option_string += f'{(len(payload)//2):04x}' # payload length, /2 because 2 chars are 1 byte 56 | option_string += payload 57 | return option_string 58 | 59 | 60 | # globally available options 61 | OPTIONS = {} 62 | options_path = pathlib.Path(__file__).parent 63 | pattern = re.compile('option_[0-9]{1,3}$') 64 | 65 | # get all option files in path and put them into options dict 66 | for path in options_path.glob('option_*.py'): 67 | # get rid of ".py" because this suffix won't be in option dict anyway 68 | name = path.name.rstrip(path.suffix) 69 | if re.match(pattern, name): 70 | # load option module 71 | spec = importlib.util.spec_from_file_location(name, path) 72 | option = importlib.util.module_from_spec(spec) 73 | spec.loader.exec_module(option) 74 | number = int(name.split('_')[1]) 75 | # add to global options constant 76 | OPTIONS[number] = option.Option(number) 77 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_1.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.options import OptionTemplate 20 | 21 | 22 | class Option(OptionTemplate): 23 | """ 24 | Option 1 Client Identifier Option 25 | """ 26 | def initialize(self, transaction=None, option=None, **kwargs): 27 | transaction.duid = option 28 | # See https://github.com/HenriWahl/dhcpy6d/issues/25 and DUID type is not used at all so just remove it 29 | # self.DUIDType = int(options[1][0:4], 16) 30 | # # DUID-EN can be retrieved from DUID 31 | # if self.DUIDType == 2: 32 | # # some HP printers seem to produce pretty bad requests, thus some cleaning is necessary 33 | # # e.g. '1 1 1 00020000000b0026b1f72a49' instead of '00020000000b0026b1f72a49' 34 | # self.DUID_EN = int(options[1].split(' ')[-1][4:12], 16) 35 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_12.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | from socket import (AF_INET6, 21 | inet_pton) 22 | 23 | from dhcpy6d.config import cfg 24 | from dhcpy6d.options import OptionTemplate 25 | 26 | 27 | class Option(OptionTemplate): 28 | """ 29 | Option 12 Server Unicast Option 30 | """ 31 | def build(self, **kwargs): 32 | response_string_part = self.convert_to_string(self.number, hexlify(inet_pton(AF_INET6, cfg.ADDRESS)).decode()) 33 | return response_string_part, self.number 34 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_13.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.options import OptionTemplate 20 | 21 | 22 | class Option(OptionTemplate): 23 | """ 24 | Option 13 Status Code Option - statuscode is taken from dictionary 25 | """ 26 | def build(self, status=None, **kwargs): 27 | response_string_part = self.convert_to_string(self.number, f'{status:04x}') 28 | return response_string_part, self.number 29 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_14.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.options import OptionTemplate 20 | 21 | 22 | class Option(OptionTemplate): 23 | """ 24 | Option 14 Rapid Commit Option - necessary for REPLY to SOLICIT message with Rapid Commit 25 | """ 26 | def build(self, **kwargs): 27 | # no real content - just the existence of this option makes it work 28 | response_string_part = self.convert_to_string(self.number, '') 29 | return response_string_part, self.number 30 | 31 | def initialize(self, transaction=None, **kwargs): 32 | transaction.rapid_commit = True 33 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_15.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import unhexlify 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | Option 15 User Class 26 | """ 27 | def initialize(self, transaction=None, option=None, **kwargs): 28 | # raw user class aka option is prefixed with null byte (00 in hex) and eot (04 in hex) 29 | transaction.user_class = unhexlify(option[4:]) 30 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_16.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import unhexlify 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | Option 16 Vendor Class 26 | """ 27 | def initialize(self, transaction=None, option=None, **kwargs): 28 | transaction.vendor_class_en = int(option[0:8], 16) 29 | transaction.vendor_class_data = unhexlify(option[12:]).decode() 30 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_20.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.config import cfg 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | Option 20 Reconfigure Accept 26 | """ 27 | def build(self, **kwargs): 28 | response_string_part = self.convert_to_string(self.number, '') 29 | # options in answer to be logged 30 | return response_string_part, self.number -------------------------------------------------------------------------------- /dhcpy6d/options/option_23.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | from socket import (AF_INET6, 21 | inet_pton) 22 | 23 | from dhcpy6d.config import cfg 24 | from dhcpy6d.options import OptionTemplate 25 | 26 | 27 | class Option(OptionTemplate): 28 | """ 29 | Option 23 DNS recursive name server 30 | """ 31 | def build(self, transaction=None, **kwargs): 32 | # dummy empty defaults 33 | response_string_part = '' 34 | options_answer_part = None 35 | # should not be necessary to check if transactions.client exists but there are 36 | # crazy clients out in the wild which might become silent this way 37 | if transaction.client: 38 | if len(cfg.CLASSES[transaction.client.client_class].NAMESERVER) > 0: 39 | nameserver = b'' 40 | for ns in cfg.CLASSES[transaction.client.client_class].NAMESERVER: 41 | nameserver += inet_pton(AF_INET6, ns) 42 | response_string_part = self.convert_to_string(self.number, hexlify(nameserver).decode()) 43 | options_answer_part = self.number 44 | elif len(cfg.NAMESERVER) > 0: 45 | # in case several nameservers are given convert them all and add them 46 | nameserver = b'' 47 | for ns in cfg.NAMESERVER: 48 | nameserver += inet_pton(AF_INET6, ns) 49 | response_string_part = self.convert_to_string(self.number, hexlify(nameserver).decode()) 50 | options_answer_part = self.number 51 | return response_string_part, options_answer_part 52 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_24.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.config import cfg 20 | from dhcpy6d.helpers import convert_dns_to_binary 21 | from dhcpy6d.options import OptionTemplate 22 | 23 | 24 | class Option(OptionTemplate): 25 | """ 26 | Option 24 Domain Search List 27 | """ 28 | def build(self, **kwargs): 29 | converted_domain_search_list = '' 30 | for d in cfg.DOMAIN_SEARCH_LIST: 31 | converted_domain_search_list += convert_dns_to_binary(d) 32 | response_string_part = self.convert_to_string(self.number, converted_domain_search_list) 33 | return response_string_part, self.number 34 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_25.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | from socket import (AF_INET6, 21 | inet_pton) 22 | 23 | from dhcpy6d import collected_macs 24 | from dhcpy6d.client import Client 25 | from dhcpy6d.config import cfg 26 | from dhcpy6d.constants import CONST 27 | from dhcpy6d.helpers import (colonify_ip6, 28 | combine_prefix_length) 29 | from dhcpy6d.options import OptionTemplate 30 | 31 | 32 | class Option(OptionTemplate): 33 | """ 34 | Option 25 Prefix Delegation 35 | """ 36 | def build(self, transaction=None, **kwargs): 37 | # dummy empty defaults 38 | response_string_part = '' 39 | options_answer_part = None 40 | 41 | # check if MAC of LLIP is really known 42 | if transaction.client_llip in collected_macs or cfg.IGNORE_MAC: 43 | # collect client information 44 | if transaction.client is None: 45 | transaction.client = Client(transaction) 46 | 47 | # Only if prefixes are provided 48 | if 'prefixes' in cfg.CLASSES[transaction.client.client_class].ADVERTISE: 49 | # check if only a short NoPrefixAvail answer or none at all is to be returned 50 | if not transaction.answer == 'normal': 51 | if transaction.answer == 'noprefix': 52 | # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' 53 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 54 | f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') 55 | # clean client prefixes which not be deployed anyway 56 | transaction.client.prefixes[:] = [] 57 | # options in answer to be logged 58 | options_answer_part = self.number 59 | else: 60 | # if client could not be built because of database problems send 61 | # status message back 62 | if transaction.client: 63 | # embed option 26 into option 25 - several if necessary 64 | ia_prefixes = '' 65 | try: 66 | for prefix in transaction.client.prefixes: 67 | ipv6_prefix = hexlify(inet_pton(AF_INET6, colonify_ip6(prefix.PREFIX))).decode() 68 | if prefix.VALID: 69 | preferred_lifetime = f'{int(prefix.PREFERRED_LIFETIME):08x}' 70 | valid_lifetime = f'{int(prefix.VALID_LIFETIME):08x}' 71 | else: 72 | preferred_lifetime = f'{0:08x}' 73 | valid_lifetime = f'{0:08x}' 74 | length = f'{int(prefix.LENGTH):02x}' 75 | ia_prefixes += self.convert_to_string(CONST.OPTION.IAPREFIX, 76 | preferred_lifetime + 77 | valid_lifetime + 78 | length + 79 | ipv6_prefix) 80 | 81 | if transaction.client.client_class != '': 82 | t1 = f'{int(cfg.CLASSES[transaction.client.client_class].T1):08x}' 83 | t2 = f'{int(cfg.CLASSES[transaction.client.client_class].T2):08x}' 84 | else: 85 | t1 = f'{int(cfg.T1):08x}' 86 | t2 = f'{int(cfg.T2):08x}' 87 | 88 | # even if there are no prefixes server has to deliver an empty PD 89 | response_string_part = self.convert_to_string(self.number, 90 | transaction.iaid + 91 | t1 + 92 | t2 + 93 | ia_prefixes) 94 | # if no prefixes available a NoPrefixAvail status code has to be sent 95 | if ia_prefixes == '': 96 | # REBIND not possible 97 | if transaction.last_message_received_type == CONST.MESSAGE.REBIND: 98 | # Option 13 Status Code Option - statuscode is 3: 'NoBinding' 99 | response_string_part += self.convert_to_string(CONST.OPTION.STATUS_CODE, 100 | f'{CONST.STATUS.NO_BINDING:04x}') 101 | else: 102 | # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' 103 | response_string_part += self.convert_to_string( # break because line too long 104 | CONST.OPTION.STATUS_CODE, 105 | f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') 106 | # options in answer to be logged 107 | options_answer_part = self.number 108 | 109 | except Exception as err: 110 | print(err) 111 | # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' 112 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 113 | f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') 114 | # options in answer to be logged 115 | options_answer_part = self.number 116 | else: 117 | # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' 118 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 119 | f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') 120 | # options in answer to be logged 121 | options_answer_part = self.number 122 | 123 | return response_string_part, options_answer_part 124 | 125 | def initialize(self, transaction=None, option=None, **kwargs): 126 | for payload in option: 127 | # iaid t1 t2 ia_prefix opt_length preferred validlt length prefix 128 | # 00000001 ffffffff ffffffff 001a 0019 00000e10 00001518 30 fd66123400.... 129 | # 8 16 24 28 32 40 48 50 82 130 | transaction.iaid = payload[0:8] 131 | transaction.iat1 = int(payload[8:16], 16) 132 | transaction.iat2 = int(payload[16:24], 16) 133 | # Prefixes given by client if any 134 | for p in range(len(payload[32:])//50): 135 | prefix = payload[50:][(p*58):(p*58)+32] 136 | length = int(payload[48:][(p*58):(p*58)+2], 16) 137 | prefix_combined = combine_prefix_length(prefix, length) 138 | # in case a prefix is asked for twice by one host ignore the twin 139 | if prefix_combined not in transaction.prefixes: 140 | transaction.prefixes.append(prefix_combined) 141 | del(prefix, length, prefix_combined) 142 | transaction.ia_options.append(CONST.OPTION.IA_PD) 143 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_3.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | from socket import (AF_INET6, 21 | inet_pton) 22 | 23 | from dhcpy6d import collected_macs 24 | from dhcpy6d.client import Client 25 | from dhcpy6d.config import cfg 26 | from dhcpy6d.constants import CONST 27 | from dhcpy6d.helpers import colonify_ip6 28 | from dhcpy6d.options import OptionTemplate 29 | 30 | 31 | class Option(OptionTemplate): 32 | """ 33 | Option 3 + 5 Identity Association for Non-temporary Address 34 | """ 35 | def build(self, transaction=None, **kwargs): 36 | # dummy empty defaults 37 | response_string_part = '' 38 | options_answer_part = None 39 | 40 | # check if MAC of LLIP is really known 41 | if transaction.client_llip in collected_macs or cfg.IGNORE_MAC: 42 | # collect client information 43 | if transaction.client is None: 44 | transaction.client = Client(transaction) 45 | 46 | if 'addresses' in cfg.CLASSES[transaction.client.client_class].ADVERTISE and \ 47 | CONST.OPTION.IA_NA in transaction.ia_options: 48 | # check if only a short NoAddrAvail answer or none at all is to be returned 49 | if not transaction.answer == 'normal': 50 | if transaction.answer == 'noaddress': 51 | # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' 52 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 53 | f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') 54 | # clean client addresses which not be deployed anyway 55 | transaction.client.addresses[:] = [] 56 | # options in answer to be logged 57 | options_answer_part = CONST.OPTION.STATUS_CODE 58 | else: 59 | # if client could not be built because of database problems send 60 | # status message back 61 | if transaction.client: 62 | # embed option 5 into option 3 - several if necessary 63 | ia_addresses = '' 64 | try: 65 | for address in transaction.client.addresses: 66 | if address.IA_TYPE == 'na': 67 | ipv6_address = hexlify(inet_pton(AF_INET6, colonify_ip6(address.ADDRESS))).decode() 68 | # if a transaction consists of too many requests from client - 69 | # - might be caused by going wild Windows clients - 70 | # reset all addresses with lifetime 0 71 | # lets start with maximal transaction count of 10 72 | if transaction.counter < 10: 73 | preferred_lifetime = f'{int(address.PREFERRED_LIFETIME):08x}' 74 | valid_lifetime = f'{int(address.VALID_LIFETIME):08x}' 75 | else: 76 | preferred_lifetime = '00000000' 77 | valid_lifetime = '00000000' 78 | ia_addresses += self.convert_to_string(CONST.OPTION.IAADDR, 79 | ipv6_address + 80 | preferred_lifetime + 81 | valid_lifetime) 82 | if ia_addresses != '': 83 | # 84 | # todo: default clients sometimes seem to have class '' 85 | # 86 | if transaction.client.client_class != '': 87 | t1 = f'{int(cfg.CLASSES[transaction.client.client_class].T1):08x}' 88 | t2 = f'{int(cfg.CLASSES[transaction.client.client_class].T2):08x}' 89 | else: 90 | t1 = f'{int(cfg.T1):08x}' 91 | t2 = f'{int(cfg.T2):08x}' 92 | 93 | response_string_part = self.convert_to_string(CONST.OPTION.IA_NA, 94 | transaction.iaid + 95 | t1 + 96 | t2 + 97 | ia_addresses) 98 | # options in answer to be logged 99 | options_answer_part = CONST.OPTION.IA_NA 100 | except: 101 | # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' 102 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 103 | f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') 104 | # options in answer to be logged 105 | options_answer_part = CONST.OPTION.STATUS_CODE 106 | else: 107 | # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' 108 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 109 | f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') 110 | # options in answer to be logged 111 | options_answer_part = CONST.OPTION.STATUS_CODE 112 | 113 | return response_string_part, options_answer_part 114 | 115 | def initialize(self, transaction=None, option=None, **kwargs): 116 | """ 117 | IA NA addresses of client 118 | """ 119 | for payload in option: 120 | transaction.iaid = payload[0:8] 121 | transaction.iat1 = int(payload[8:16], 16) 122 | transaction.iat2 = int(payload[16:24], 16) 123 | 124 | # addresses given by client if any 125 | for a in range(len(payload[32:]) // 44): 126 | address = payload[32:][(a * 56):(a * 56) + 32] 127 | # in case an address is asked for twice by one host ignore the twin 128 | # sometimes address seems to be EMPTY??? 129 | if address and colonify_ip6(address) and address not in transaction.addresses: 130 | transaction.addresses.append(address) 131 | transaction.ia_options.append(CONST.OPTION.IA_NA) 132 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_31.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | from socket import (AF_INET6, 21 | inet_pton) 22 | 23 | from dhcpy6d.config import cfg 24 | from dhcpy6d.options import OptionTemplate 25 | 26 | 27 | class Option(OptionTemplate): 28 | """ 29 | Option 31 SNTP Servers 30 | """ 31 | def build(self, **kwargs): 32 | # dummy empty return value 33 | response_string_part = '' 34 | 35 | if cfg.SNTP_SERVERS != '': 36 | sntp_servers = b'' 37 | for s in cfg.SNTP_SERVERS: 38 | sntp_server = inet_pton(AF_INET6, s) 39 | sntp_servers += sntp_server 40 | response_string_part = self.convert_to_string(self.number, hexlify(sntp_servers).decode()) 41 | 42 | return response_string_part, self.number 43 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_32.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.config import cfg 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | Option 32 Information Refresh Time 26 | """ 27 | def build(self, **kwargs): 28 | response_string_part = self.convert_to_string(self.number, f'{int(cfg.INFORMATION_REFRESH_TIME):08x}') 29 | # options in answer to be logged 30 | return response_string_part, self.number 31 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_39.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import re 20 | 21 | from dhcpy6d.config import cfg 22 | from dhcpy6d.helpers import (convert_binary_to_dns, 23 | convert_dns_to_binary) 24 | from dhcpy6d.options import OptionTemplate 25 | 26 | 27 | class Option(OptionTemplate): 28 | """ 29 | Option 39 FQDN 30 | 31 | http://tools.ietf.org/html/rfc4704#page-5 32 | regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: 33 | - client wants to update DNS itself -> sends 0 0 0 34 | - client wants server to update DNS -> sends 0 0 1 35 | - client wants no server DNS update -> sends 1 0 0 36 | """ 37 | 38 | def build(self, transaction=None, **kwargs): 39 | # dummy empty return value 40 | response_string_part = '' 41 | options_answer_part = None 42 | 43 | # http://tools.ietf.org/html/rfc4704#page-5 44 | # regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: 45 | # - client wants to update DNS itself -> sends 0 0 0 46 | # - client wants server to update DNS -> sends 0 0 1 47 | # - client wants no server DNS update -> sends 1 0 0 48 | if transaction.client: 49 | # flags for answer 50 | n, o, s = 0, 0, 0 51 | # use hostname supplied by client 52 | if cfg.DNS_USE_CLIENT_HOSTNAME: 53 | hostname = transaction.hostname 54 | # use hostname from config 55 | else: 56 | hostname = transaction.client.hostname 57 | if not hostname == '': 58 | if cfg.DNS_UPDATE: 59 | # DNS update done by server - don't care what client wants 60 | if cfg.DNS_IGNORE_CLIENT: 61 | s = 1 62 | o = 1 63 | else: 64 | # honor the client's request for the server to initiate DNS updates 65 | if transaction.dns_s == 1: 66 | s = 1 67 | # honor the client's request for no server-initiated DNS update 68 | elif transaction.dns_n == 1: 69 | n = 1 70 | else: 71 | # no DNS update at all, not for server and not for client 72 | if transaction.dns_n == 1 or \ 73 | transaction.dns_s == 1: 74 | o = 1 75 | # sum of flags 76 | nos_flags = n * 4 + o * 2 + s * 1 77 | fqdn_binary = convert_dns_to_binary(f'{hostname}.{cfg.DOMAIN}') 78 | response_string_part = self.convert_to_string(self.number, f'{nos_flags:02x}{fqdn_binary}') 79 | else: 80 | # if no hostname given put something in and force client override 81 | fqdn_binary = convert_dns_to_binary(f'invalid-hostname.{cfg.DOMAIN}') 82 | response_string_part = self.convert_to_string(self.number, f'{3:02x}{fqdn_binary}') 83 | # options in answer to be logged 84 | options_answer_part = self.number 85 | 86 | return response_string_part, options_answer_part 87 | 88 | def initialize(self, transaction=None, option=None, **kwargs): 89 | bits = f'{int(option[1:2]):04d}' 90 | transaction.dns_n = int(bits[1]) 91 | transaction.dns_o = int(bits[2]) 92 | transaction.dns_s = int(bits[3]) 93 | # only hostname needed 94 | transaction.fqdn = convert_binary_to_dns(option[2:]) 95 | transaction.hostname = transaction.fqdn.split('.')[0].lower() 96 | # test if hostname is valid 97 | hostname_pattern = re.compile('^([a-z0-9-_]+)*$') 98 | if hostname_pattern.match(transaction.hostname) is None: 99 | transaction.hostname = '' 100 | del hostname_pattern 101 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_4.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | from socket import (AF_INET6, 21 | inet_pton) 22 | 23 | from dhcpy6d import collected_macs 24 | from dhcpy6d.client import Client 25 | from dhcpy6d.config import cfg 26 | from dhcpy6d.constants import CONST 27 | from dhcpy6d.helpers import colonify_ip6 28 | from dhcpy6d.options import OptionTemplate 29 | 30 | 31 | class Option(OptionTemplate): 32 | """ 33 | Option 4 + 5 Identity Association for Temporary Address 34 | """ 35 | def build(self, transaction=None, **kwargs): 36 | # dummy empty defaults 37 | response_string_part = '' 38 | options_answer_part = None 39 | 40 | # check if MAC of LLIP is really known 41 | if transaction.client_llip in collected_macs or cfg.IGNORE_MAC: 42 | # collect client information 43 | if transaction.client is None: 44 | transaction.client = Client(transaction) 45 | 46 | if 'addresses' in cfg.CLASSES[transaction.client.client_class].ADVERTISE and \ 47 | CONST.OPTION.IA_TA in transaction.ia_options: 48 | # check if only a short NoAddrAvail answer or none at all ist t be returned 49 | if not transaction.answer == 'normal': 50 | if transaction.answer == 'noaddress': 51 | # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' 52 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 53 | f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') 54 | # clean client addresses which not be deployed anyway 55 | transaction.client.addresses[:] = [] 56 | # options in answer to be logged 57 | options_answer_part = CONST.OPTION.STATUS_CODE 58 | else: 59 | # if client could not be built because of database problems send 60 | # status message back 61 | if transaction.client: 62 | # embed option 5 into option 4 - several if necessary 63 | ia_addresses = '' 64 | try: 65 | for address in transaction.client.addresses: 66 | if address.IA_TYPE == 'ta': 67 | ipv6_address = hexlify(inet_pton(AF_INET6, colonify_ip6(address.ADDRESS))).decode() 68 | # if a transaction consists of too many requests from client - 69 | # - might be caused by going wild Windows clients - 70 | # reset all addresses with lifetime 0 71 | # lets start with maximal transaction count of 10 72 | if transaction.counter < 10: 73 | preferred_lifetime = f'{int(address.PREFERRED_LIFETIME):08x}' 74 | valid_lifetime = f'{int(address.VALID_LIFETIME):08x}' 75 | else: 76 | preferred_lifetime = '00000000' 77 | valid_lifetime = '00000000' 78 | ia_addresses += self.convert_to_string(CONST.OPTION.IAADDR, 79 | ipv6_address + 80 | preferred_lifetime + 81 | valid_lifetime) 82 | if ia_addresses != '': 83 | response_string_part = self.convert_to_string(CONST.OPTION.IA_TA, 84 | transaction.iaid + 85 | ia_addresses) 86 | # options in answer to be logged 87 | options_answer_part = CONST.OPTION.IA_TA 88 | except: 89 | # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' 90 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 91 | f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') 92 | # options in answer to be logged 93 | options_answer_part = CONST.OPTION.STATUS_CODE 94 | else: 95 | # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' 96 | response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, 97 | f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') 98 | # options in answer to be logged 99 | options_answer_part = CONST.OPTION.STATUS_CODE 100 | 101 | return response_string_part, options_answer_part 102 | 103 | def initialize(self, transaction=None, option=None, **kwargs): 104 | """ 105 | IA TA addresses of client 106 | """ 107 | for payload in option: 108 | transaction.iaid = payload[0:8] 109 | transaction.iat1 = int(payload[8:16], 16) 110 | transaction.iat2 = int(payload[16:24], 16) 111 | 112 | # addresses given by client if any 113 | for a in range(len(payload[32:]) // 44): 114 | address = payload[32:][(a * 56):(a * 56) + 32] 115 | # in case an address is asked for twice by one host ignore the twin 116 | # sometimes address seems to be EMPTY??? 117 | if address and colonify_ip6(address) and address not in transaction.addresses: 118 | transaction.addresses.append(address) 119 | transaction.ia_options.append(CONST.OPTION.IA_TA) 120 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_56.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | from socket import (AF_INET6, 21 | inet_pton) 22 | 23 | from dhcpy6d.config import cfg 24 | from dhcpy6d.helpers import convert_dns_to_binary 25 | from dhcpy6d.options import OptionTemplate 26 | 27 | 28 | class Option(OptionTemplate): 29 | """ 30 | Option 56 NTP server 31 | https://tools.ietf.org/html/rfc5908 32 | """ 33 | def build(self, **kwargs): 34 | # dummy empty defaults 35 | response_string_part = '' 36 | options_answer_part = None 37 | ntp_server_options = '' 38 | 39 | if len(cfg.NTP_SERVER) > 0: 40 | for ntp_server_type in list(cfg.NTP_SERVER_DICT.keys()): 41 | # ntp_server_suboption 42 | for ntp_server in cfg.NTP_SERVER_DICT[ntp_server_type]: 43 | ntp_server_suboption = '' 44 | if ntp_server_type == 'SRV': 45 | ntp_server_suboption = self.convert_to_string(1, hexlify(inet_pton(AF_INET6, ntp_server)).decode()) 46 | elif ntp_server_type == 'MC': 47 | ntp_server_suboption = self.convert_to_string(2, hexlify(inet_pton(AF_INET6, ntp_server)).decode()) 48 | elif ntp_server_type == 'FQDN': 49 | ntp_server_suboption = self.convert_to_string(3, convert_dns_to_binary(ntp_server)) 50 | ntp_server_options += ntp_server_suboption 51 | response_string_part = self.convert_to_string(self.number, ntp_server_options) 52 | # options in answer to be logged 53 | options_answer_part = self.number 54 | 55 | return response_string_part, options_answer_part 56 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_59.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from binascii import hexlify 20 | 21 | from dhcpy6d.client import Client 22 | from dhcpy6d.helpers import build_option 23 | from dhcpy6d.options import OptionTemplate 24 | 25 | 26 | class Option(OptionTemplate): 27 | """ 28 | Option 59 Network Boot 29 | https://tools.ietf.org/html/rfc5970 30 | """ 31 | def build(self, transaction=None, **kwargs): 32 | # dummy empty defaults 33 | response_string_part = '' 34 | options_answer_part = None 35 | 36 | # build client if not done yet 37 | if transaction.client is None: 38 | transaction.client = Client(transaction) 39 | 40 | bootfiles = transaction.client.bootfiles 41 | if len(bootfiles) > 0: 42 | # TODO add preference logic 43 | bootfile_url = bootfiles[0].BOOTFILE_URL 44 | transaction.client.chosen_boot_file = bootfile_url 45 | bootfile_options = hexlify(bootfile_url).decode() 46 | response_string_part += build_option(self.number, bootfile_options) 47 | # options in answer to be logged 48 | options_answer_part = self.number 49 | 50 | return response_string_part, options_answer_part 51 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_6.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.options import OptionTemplate 20 | 21 | 22 | class Option(OptionTemplate): 23 | """ 24 | Option 6 Option Request Option 25 | """ 26 | def initialize(self, transaction=None, option=None, **kwargs): 27 | options_request = [] 28 | options = option[:] 29 | # cut given option (which contains all requested options) into pieces 30 | while len(options) > 0: 31 | options_request.append(int(options[0:4], 16)) 32 | options = options[4:] 33 | transaction.options_request = options_request 34 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_61.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.constants import CONST 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | 61 Client System Architecture Type 26 | """ 27 | def initialize(self, transaction=None, option=None, **kwargs): 28 | # raw client architecture is supplied as a 16-bit integer (e. g. 0007) 29 | # See https://tools.ietf.org/html/rfc4578#section-2.1 30 | transaction.client_architecture = option 31 | # short number (0007 => 7 for dictionary usage) 32 | client_architecture_short = int(transaction.client_architecture) 33 | if client_architecture_short in CONST.ARCHITECTURE_TYPE_DICT: 34 | transaction.known_client_architecture = CONST.ARCHITECTURE_TYPE_DICT[client_architecture_short] 35 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_7.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.config import cfg 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | Option 7 Server Preference 26 | """ 27 | def build(self, **kwargs): 28 | response_string_part = self.convert_to_string(self.number, f'{int(cfg.SERVER_PREFERENCE):02x}') 29 | return response_string_part, self.number 30 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_8.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.options import OptionTemplate 20 | 21 | 22 | class Option(OptionTemplate): 23 | """ 24 | Option 8 Elapsed Time 25 | RFC 3315: This time is expressed in hundredths of a second (10^-2 seconds). 26 | """ 27 | def initialize(self, transaction=None, option=None, **kwargs): 28 | transaction.elapsed_time = int(option[0:8], 16) 29 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_82.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.config import cfg 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | Option 82 SOL_MAX_RT (sic!) 26 | """ 27 | def build(self, **kwargs): 28 | response_string_part = self.convert_to_string(self.number, f'{int(cfg.SOLICITATION_REFRESH_TIME):08x}') 29 | # options in answer to be logged 30 | return response_string_part, self.number 31 | -------------------------------------------------------------------------------- /dhcpy6d/options/option_83.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from dhcpy6d.config import cfg 20 | from dhcpy6d.options import OptionTemplate 21 | 22 | 23 | class Option(OptionTemplate): 24 | """ 25 | Option 83 INF_MAX_RT (sic!) 26 | """ 27 | def build(self, **kwargs): 28 | response_string_part = self.convert_to_string(self.number, f'{int(cfg.INFORMATION_REFRESH_TIME):08x}') 29 | # options in answer to be logged 30 | return response_string_part, self.number 31 | -------------------------------------------------------------------------------- /dhcpy6d/route.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from .config import cfg 20 | from .globals import (route_queue, 21 | timer) 22 | from .log import log 23 | from .storage import volatile_store 24 | 25 | 26 | class Route: 27 | """ 28 | store data of a route which should be given to an external application 29 | router is here the prefix requesting host 30 | """ 31 | 32 | def __init__(self, prefix, length, router): 33 | self.prefix = prefix 34 | self.length = length 35 | self.router = router 36 | 37 | 38 | def modify_route(transaction, mode): 39 | """ 40 | called when route has to be set - calls itself any external script or something like that 41 | """ 42 | # check if client is already set - otherwise crashes 43 | if transaction.client is not None: 44 | # only do anything if class of client has something defined to be called 45 | if (mode == 'up' and cfg.CLASSES[transaction.client.client_class].CALL_UP != '') or \ 46 | (mode == 'down' and cfg.CLASSES[transaction.client.client_class].CALL_DOWN != ''): 47 | # collect possible prefixes, lengths and router ip addresses in list 48 | routes = list() 49 | for prefix in transaction.client.prefixes: 50 | # use LinkLocal Address of client if wanted, either by prefix or client configs 51 | if prefix.ROUTE_LINK_LOCAL or transaction.client.prefix_route_link_local: 52 | router = transaction.client_llip 53 | else: 54 | if len(transaction.client.addresses) == 1: 55 | router = transaction.client.addresses[0].ADDRESS 56 | else: 57 | router = None 58 | log.error( 59 | 'modify_route: client needs exactly 1 address to be used as router to delegated prefix') 60 | if router is not None: 61 | routes.append(Route(prefix.PREFIX, prefix.LENGTH, router)) 62 | 63 | if mode == 'up': 64 | call = cfg.CLASSES[transaction.client.client_class].CALL_UP 65 | elif mode == 'down': 66 | call = cfg.CLASSES[transaction.client.client_class].CALL_DOWN 67 | else: 68 | # should not happen but just in case 69 | call = '' 70 | 71 | # call executables here 72 | for route in routes: 73 | route_queue.put((mode, call, route.prefix, route.length, route.router)) 74 | 75 | 76 | def manage_prefixes_routes(): 77 | """ 78 | delete or add inactive or active routes according to the prefixes in database 79 | """ 80 | volatile_store.release_free_prefixes(timer.time) 81 | inactive_prefixes = volatile_store.get_inactive_prefixes() 82 | active_prefixes = volatile_store.get_active_prefixes() 83 | 84 | for prefix in inactive_prefixes: 85 | length, router, pclass = volatile_store.get_route(prefix) 86 | if pclass in cfg.CLASSES: 87 | route_queue.put(('down', cfg.CLASSES[pclass].CALL_DOWN, prefix, length, router)) 88 | 89 | for prefix in active_prefixes: 90 | length, router, pclass = volatile_store.get_route(prefix) 91 | if pclass in cfg.CLASSES: 92 | route_queue.put(('up', cfg.CLASSES[pclass].CALL_UP, prefix, length, router)) 93 | -------------------------------------------------------------------------------- /dhcpy6d/storage/__init__.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import sys 20 | import threading 21 | import traceback 22 | 23 | from ..config import cfg 24 | from ..globals import (config_answer_queue, 25 | config_query_queue, 26 | config_store, 27 | volatile_answer_queue, 28 | volatile_query_queue, 29 | volatile_store) 30 | from ..helpers import error_exit 31 | 32 | from .mysql import DBMySQL 33 | from .postgresql import DBPostgreSQL 34 | from .sqlite import SQLite 35 | from .store import (ClientConfig, 36 | ClientConfigDicts, 37 | Store) 38 | from .textfile import Textfile 39 | 40 | 41 | class QueryQueue(threading.Thread): 42 | """ 43 | Pump queries around 44 | """ 45 | def __init__(self, name='', store_type=None, query_queue=None, answer_queue=None): 46 | threading.Thread.__init__(self, name=name) 47 | self.query_queue = query_queue 48 | self.answer_queue = answer_queue 49 | self.store = store_type 50 | self.daemon = True 51 | 52 | def run(self): 53 | """ 54 | receive queries and ask the DB interface for answers which will be put into 55 | answer queue 56 | """ 57 | while True: 58 | query = self.query_queue.get() 59 | try: 60 | answer = self.store.db_query(query) 61 | except Exception as error: 62 | traceback.print_exc(file=sys.stdout) 63 | sys.stdout.flush() 64 | answer = error 65 | 66 | self.answer_queue.put({query: answer}) 67 | 68 | 69 | # because of thread trouble there should not be too much db connections at once 70 | # so we need to use the queryqueue way - subject to change 71 | # source of configuration of hosts 72 | # use client configuration only if needed 73 | if cfg.STORE_CONFIG: 74 | if cfg.STORE_CONFIG == 'file': 75 | config_store = Textfile(config_query_queue, config_answer_queue) 76 | if cfg.STORE_CONFIG == 'mysql': 77 | config_store = DBMySQL(config_query_queue, config_answer_queue) 78 | if cfg.STORE_CONFIG == 'postgresql': 79 | config_store = DBPostgreSQL(config_query_queue, config_answer_queue) 80 | if cfg.STORE_CONFIG == 'sqlite': 81 | config_store = SQLite(config_query_queue, config_answer_queue, storage_type='config') 82 | # set client config schema version after config storage is established 83 | config_store.set_client_config_schema_version(cfg.STORE_CONFIG_SCHEMA_VERSION) 84 | else: 85 | # dummy configstore if no client config is needed 86 | config_store = Store(config_query_queue, config_answer_queue) 87 | # 'none' store is always connected 88 | config_store.connected = True 89 | 90 | # storage for changing data like leases, LLIPs, DUIDs etc. 91 | if cfg.STORE_VOLATILE == 'mysql': 92 | volatile_store = DBMySQL(volatile_query_queue, volatile_answer_queue) 93 | if cfg.STORE_VOLATILE == 'postgresql': 94 | volatile_store = DBPostgreSQL(volatile_query_queue, volatile_answer_queue) 95 | if cfg.STORE_VOLATILE == 'sqlite': 96 | volatile_store = SQLite(volatile_query_queue, volatile_answer_queue, storage_type='volatile') 97 | 98 | # do not start if no database connection exists 99 | if not config_store.connected: 100 | error_exit('Configuration database is not connected!') 101 | if not volatile_store.connected: 102 | error_exit('Database for volatile data is not connected!') 103 | -------------------------------------------------------------------------------- /dhcpy6d/storage/mysql.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import sys 20 | import traceback 21 | 22 | from ..config import cfg 23 | from ..helpers import error_exit 24 | 25 | from .store import DB 26 | 27 | 28 | class DBMySQL(DB): 29 | """ 30 | access MySQL and MariaDB 31 | """ 32 | QUERY_TABLES = f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{cfg.STORE_DB_DB}'" 33 | 34 | def db_connect(self): 35 | """ 36 | Connect to database server according to database type 37 | """ 38 | # try to get some MySQL/MariaDB-module imported 39 | try: 40 | if 'MySQLdb' not in sys.modules: 41 | import MySQLdb 42 | self.db_module = sys.modules['MySQLdb'] 43 | except: 44 | try: 45 | if 'pymsql' not in sys.modules: 46 | import pymysql 47 | self.db_module = sys.modules['pymysql'] 48 | except: 49 | error_exit('ERROR: Cannot find module MySQLdb or PyMySQL. Please install one of them to proceed.') 50 | try: 51 | self.connection = self.db_module.connect(host=cfg.STORE_DB_HOST, 52 | db=cfg.STORE_DB_DB, 53 | user=cfg.STORE_DB_USER, 54 | passwd=cfg.STORE_DB_PASSWORD) 55 | self.connection.autocommit(True) 56 | self.cursor = self.connection.cursor() 57 | self.connected = True 58 | except: 59 | traceback.print_exc(file=sys.stdout) 60 | sys.stdout.flush() 61 | self.connected = False 62 | 63 | return self.connected 64 | 65 | def db_query(self, query): 66 | """ 67 | execute DB query 68 | """ 69 | try: 70 | self.cursor.execute(query) 71 | except self.db_module.IntegrityError: 72 | return 'INSERT_ERROR' 73 | except Exception as err: 74 | # try to reestablish database connection 75 | print(f'Error: {str(err)}') 76 | print(f'Query: {query}') 77 | if not self.db_connect(): 78 | return None 79 | else: 80 | try: 81 | self.cursor.execute(query) 82 | except: 83 | traceback.print_exc(file=sys.stdout) 84 | sys.stdout.flush() 85 | self.connected = False 86 | return None 87 | 88 | result = self.cursor.fetchall() 89 | return result 90 | -------------------------------------------------------------------------------- /dhcpy6d/storage/postgresql.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import sys 20 | import traceback 21 | 22 | from ..config import cfg 23 | from ..helpers import error_exit 24 | 25 | from .schemas import POSTGRESQL_SCHEMA 26 | from .store import DB 27 | 28 | 29 | class DBPostgreSQL(DB): 30 | """ 31 | PostgreSQL connection - to be tested! 32 | """ 33 | # different to default derived MYSQL_SQLITE schema 34 | SCHEMAS = POSTGRESQL_SCHEMA 35 | QUERY_TABLES = f"SELECT table_name FROM information_schema.tables WHERE " \ 36 | f"table_schema = 'public' AND " \ 37 | f"table_catalog = '{cfg.STORE_DB_DB}'" 38 | 39 | def db_connect(self): 40 | """ 41 | Connect to database server according to database type 42 | """ 43 | try: 44 | if 'psycopg2' not in sys.modules: 45 | import psycopg2 46 | self.db_module = sys.modules['psycopg2'] 47 | except: 48 | traceback.print_exc(file=sys.stdout) 49 | sys.stdout.flush() 50 | error_exit('ERROR: Cannot find module psycopg2. Please install to proceed.') 51 | try: 52 | self.connection = self.db_module.connect(host=cfg.STORE_DB_HOST, 53 | database=cfg.STORE_DB_DB, 54 | user=cfg.STORE_DB_USER, 55 | password=cfg.STORE_DB_PASSWORD) 56 | self.connection.autocommit = True 57 | self.cursor = self.connection.cursor() 58 | self.connected = True 59 | except: 60 | traceback.print_exc(file=sys.stdout) 61 | sys.stdout.flush() 62 | self.connected = False 63 | return self.connected 64 | 65 | def db_query(self, query): 66 | """ 67 | execute DB query 68 | """ 69 | try: 70 | self.cursor.execute(query) 71 | # catch impossible INSERTs 72 | except self.db_module.errors.UniqueViolation: 73 | return 'INSERT_ERROR' 74 | except self.db_module.errors.IntegrityError: 75 | return 'INSERT_ERROR' 76 | except Exception as err: 77 | # try to reestablish database connection 78 | print(f'Error: {str(err)}') 79 | if not self.db_connect(): 80 | return None 81 | else: 82 | try: 83 | self.cursor.execute(query) 84 | except: 85 | traceback.print_exc(file=sys.stdout) 86 | sys.stdout.flush() 87 | self.connected = False 88 | return None 89 | try: 90 | result = self.cursor.fetchall() 91 | # If there is no result after a database reconnect a None would lead to eternal loop 92 | except self.db_module.ProgrammingError: 93 | return [] 94 | except Exception as err: 95 | return None 96 | return result 97 | -------------------------------------------------------------------------------- /dhcpy6d/storage/sqlite.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import grp 20 | import os 21 | import pwd 22 | import sys 23 | import traceback 24 | 25 | from ..config import cfg 26 | from .store import Store 27 | 28 | 29 | class SQLite(Store): 30 | """ 31 | file-based SQLite database, might be an option for single installations 32 | """ 33 | QUERY_TABLES = "SELECT name FROM sqlite_master WHERE type='table'" 34 | 35 | def __init__(self, query_queue, answer_queue, storage_type='volatile'): 36 | 37 | Store.__init__(self, query_queue, answer_queue) 38 | self.connection = None 39 | 40 | try: 41 | self.db_connect(storage_type) 42 | except: 43 | traceback.print_exc(file=sys.stdout) 44 | sys.stdout.flush() 45 | 46 | def db_connect(self, storage_type='volatile'): 47 | """ 48 | initialize DB connection 49 | """ 50 | # only import if needed 51 | if 'sqlite3' not in sys.modules: 52 | import sqlite3 53 | self.db_module = sqlite3 54 | try: 55 | if storage_type == 'volatile': 56 | storage = cfg.STORE_SQLITE_VOLATILE 57 | # set ownership of storage file according to settings 58 | os.chown(cfg.STORE_SQLITE_VOLATILE, pwd.getpwnam(cfg.USER).pw_uid, grp.getgrnam(cfg.GROUP).gr_gid) 59 | if storage_type == 'config': 60 | storage = cfg.STORE_SQLITE_CONFIG 61 | self.connection = self.db_module.connect(storage, check_same_thread = False) 62 | self.cursor = self.connection.cursor() 63 | self.connected = True 64 | except: 65 | traceback.print_exc(file=sys.stdout) 66 | sys.stdout.flush() 67 | return None 68 | 69 | def db_query(self, query): 70 | """ 71 | execute query on DB 72 | """ 73 | try: 74 | self.cursor.execute(query) 75 | # commit only if explicitly wanted 76 | if query.startswith('INSERT'): 77 | self.connection.commit() 78 | elif query.startswith('UPDATE'): 79 | self.connection.commit() 80 | elif query.startswith('DELETE'): 81 | self.connection.commit() 82 | self.connected = True 83 | except self.db_module.IntegrityError: 84 | return 'INSERT_ERROR' 85 | except Exception as err: 86 | # try to reestablish database connection 87 | print(f'Error: {str(err.args[0])}') 88 | print(f'Query: {query}') 89 | if not self.db_connect(): 90 | return None 91 | 92 | result = self.cursor.fetchall() 93 | return result 94 | -------------------------------------------------------------------------------- /dhcpy6d/storage/textfile.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | import configparser 20 | 21 | from ..config import cfg, BOOLPOOL 22 | from ..helpers import (decompress_ip6, 23 | error_exit, 24 | listify_option, convert_prefix_inline) 25 | 26 | from .store import (ClientConfig, 27 | Store) 28 | 29 | 30 | class Textfile(Store): 31 | """ 32 | client config in text files 33 | """ 34 | def __init__(self, query_queue, answer_queue): 35 | Store.__init__(self, query_queue, answer_queue) 36 | self.connection = None 37 | 38 | # store config information of hosts 39 | self.hosts = {} 40 | self.index_mac = {} 41 | self.index_duid = {} 42 | 43 | # store IDs for ID-based hosts to check if there are duplicates 44 | self.ids = {} 45 | 46 | # instantiate a Configparser 47 | config = configparser.ConfigParser() 48 | config.read(cfg.STORE_FILE_CONFIG) 49 | 50 | # read all sections of config file 51 | # a section here is a host 52 | for section in config.sections(): 53 | hostname = config[section]['hostname'].lower() 54 | # only if section matches hostname the following steps are of any use 55 | if section.lower() == hostname: 56 | self.hosts[hostname] = ClientConfig() 57 | for item in config.items(hostname): 58 | # lowercase all MAC addresses, DUIDs, IPv6 addresses and prefixes 59 | if item[0].upper() in ['ADDRESS', 'DUID', 'HOSTNAME', 'MAC', 'PREFIX', 'PREFIX_ROUTE_LINK_LOCAL']: 60 | self.hosts[hostname].__setattr__(item[0].upper(), str(item[1]).lower()) 61 | else: 62 | self.hosts[hostname].__setattr__(item[0].upper(), str(item[1])) 63 | 64 | # Test if host has ID 65 | if self.hosts[hostname].CLASS in cfg.CLASSES: 66 | for a in cfg.CLASSES[self.hosts[hostname].CLASS].ADDRESSES: 67 | if cfg.ADDRESSES[a].CATEGORY == 'id' and self.hosts[hostname].ID == '': 68 | error_exit(f"Textfile client configuration: No ID given " 69 | f"for client '{self.hosts[hostname].HOSTNAME}'") 70 | else: 71 | error_exit(f"Textfile client configuration: Class '{self.hosts[hostname].CLASS}' " 72 | f"of host '{self.hosts[hostname].HOSTNAME}' is not defined") 73 | 74 | if self.hosts[hostname].ID != '': 75 | if self.hosts[hostname].ID in list(self.ids.keys()): 76 | error_exit(f"Textfile client configuration: ID '{self.hosts[hostname].ID}' " 77 | f"of client '{self.hosts[hostname].HOSTNAME}' is already used " 78 | f"by '{self.ids[self.hosts[hostname].ID]}'.") 79 | else: 80 | self.ids[self.hosts[hostname].ID] = self.hosts[hostname].HOSTNAME 81 | 82 | # in case of various MAC addresses split them... 83 | self.hosts[hostname].MAC = listify_option(self.hosts[hostname].MAC) 84 | 85 | # in case of various fixed addresses split them and avoid decompressing of ':'... 86 | self.hosts[hostname].ADDRESS = listify_option(self.hosts[hostname].ADDRESS) 87 | 88 | # Decompress IPv6-Addresses 89 | if self.hosts[hostname].ADDRESS is not None: 90 | self.hosts[hostname].ADDRESS = [decompress_ip6(x) for x in self.hosts[hostname].ADDRESS] 91 | 92 | # in case of multiple supplied prefixes convert them to list 93 | self.hosts[hostname].PREFIX = listify_option(self.hosts[hostname].PREFIX) 94 | 95 | # split prefix into address and length, verify address 96 | if self.hosts[hostname].PREFIX is not None: 97 | self.hosts[hostname].PREFIX = [convert_prefix_inline(x) for x in self.hosts[hostname].PREFIX] 98 | 99 | # boolify prefix route link local setting 100 | if self.hosts[hostname].PREFIX_ROUTE_LINK_LOCAL: 101 | self.hosts[hostname].PREFIX_ROUTE_LINK_LOCAL = BOOLPOOL[self.hosts[hostname].PREFIX_ROUTE_LINK_LOCAL] 102 | 103 | # and put the host objects into index 104 | if self.hosts[hostname].MAC: 105 | for m in self.hosts[hostname].MAC: 106 | if m not in self.index_mac: 107 | self.index_mac[m] = [self.hosts[hostname]] 108 | else: 109 | self.index_mac[m].append(self.hosts[hostname]) 110 | 111 | # add DUIDs to IndexDUID 112 | if not self.hosts[hostname].DUID == '': 113 | if not self.hosts[hostname].DUID in self.index_duid: 114 | self.index_duid[self.hosts[hostname].DUID] = [self.hosts[hostname]] 115 | else: 116 | self.index_duid[self.hosts[hostname].DUID].append(self.hosts[hostname]) 117 | else: 118 | error_exit(f"Textfile client configuration: section [{section.lower()}] " 119 | f"does not match hostname '{hostname}'") 120 | # not very meaningful in case of databaseless textfile config but for completeness 121 | self.connected = True 122 | 123 | def get_client_config_by_mac(self, transaction): 124 | """ 125 | get host(s?) and its information belonging to that mac 126 | """ 127 | hosts = list() 128 | mac = transaction.mac 129 | if mac in self.index_mac: 130 | hosts.extend(self.index_mac[mac]) 131 | return hosts 132 | else: 133 | return None 134 | 135 | def get_client_config_by_duid(self, transaction): 136 | """ 137 | get host and its information belonging to that DUID 138 | """ 139 | hosts = list() 140 | duid = transaction.duid 141 | if duid in self.index_duid: 142 | hosts.extend(self.index_duid[duid]) 143 | return hosts 144 | else: 145 | return None 146 | 147 | def get_client_config_by_hostname(self, transaction): 148 | """ 149 | get host and its information by hostname 150 | """ 151 | hostname = transaction.hostname 152 | if hostname in self.hosts: 153 | return [self.hosts[hostname]] 154 | else: 155 | return None 156 | 157 | def get_client_config(self, hostname='', client_class='', duid='', address=[], mac=[], host_id=''): 158 | """ 159 | give back ClientConfig object 160 | """ 161 | return ClientConfig(hostname=hostname, client_class=client_class, duid=duid, address=address, mac=mac, host_id=host_id) 162 | -------------------------------------------------------------------------------- /dhcpy6d/transaction.py: -------------------------------------------------------------------------------- 1 | # DHCPy6d DHCPv6 Daemon 2 | # 3 | # Copyright (C) 2009-2024 Henri Wahl 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 18 | 19 | from .config import cfg 20 | from .constants import CONST 21 | from .globals import (DUMMY_IAID, 22 | DUMMY_MAC, 23 | EMPTY_OPTIONS, 24 | IGNORED_LOG_OPTIONS, 25 | timer) 26 | from .helpers import (colonify_ip6, 27 | combine_prefix_length, 28 | split_prefix) 29 | from .options import OPTIONS 30 | 31 | 32 | class Transaction: 33 | """ 34 | all data of one transaction, to be collected in Transactions 35 | """ 36 | def __init__(self, transaction_id, client_llip, interface, message_type, options): 37 | # Transaction ID 38 | self.id = transaction_id 39 | # Link Local IP of client 40 | self.client_llip = client_llip 41 | # Interface the request came in 42 | self.interface = interface 43 | # MAC address 44 | self.mac = DUMMY_MAC 45 | # last message for following the protocol 46 | self.last_message_received_type = message_type 47 | # dictionary for options 48 | self.options = options 49 | # default dummy OptionsRequest 50 | self.options_request = list() 51 | # timestamp to manage/clean transactions 52 | self.timestamp = timer.time 53 | # dummy hostname 54 | self.fqdn = '' 55 | self.hostname = '' 56 | # DNS Options for option 39 57 | self.dns_n = 0 58 | self.dns_o = 0 59 | self.dns_s = 0 60 | # dummy IAID 61 | self.iaid = DUMMY_IAID 62 | # dummy IAT1 63 | self.iat1 = cfg.T1 64 | # dummy IAT2 65 | self.iat2 = cfg.T2 66 | # IA option - NA, TA or PD -> DHCPv6 option 3, 4 or 25 67 | # to be used in option_requests in Handler.build_response() 68 | self.ia_options = [] 69 | # Addresses given by client, for example for RENEW or RELEASE requests 70 | self.addresses = [] 71 | # same with prefixes 72 | self.prefixes = [] 73 | # might be used against clients that are running wild 74 | # initial 1 as being increased after handling 75 | self.counter = 1 76 | # temporary storage for client configuration from DB config 77 | # - only used if config comes from DB 78 | self.client_config_dicts = None 79 | # client config from config store 80 | self.client = None 81 | # Vendor Class Option 82 | self.vendor_class_en = None 83 | self.vendor_class_data = '' 84 | # Rapid Commit flag 85 | self.rapid_commit = False 86 | # answer type - take from class definition, one of 'normal', 'noaddress', 'noprefix' or 'none' 87 | # defaults to 'normal' as this is the main purpose of dhcpy6d 88 | self.answer = 'normal' 89 | # default DUID value 90 | self.duid = '' 91 | # Elapsed Time - option 8, at least sent by WIDE dhcp6c when requesting delegated prefix 92 | self.elapsed_time = 0 93 | # Client architecture type (RFC 5970) 94 | self.client_architecture = '' 95 | # Known client architecture type (RFC 4578) (e.g. EFI x86 - 64) 96 | self.known_client_architecture = '' 97 | # UserClass (https://tools.ietf.org/html/rfc3315#section-22.15) 98 | self.user_class = '' 99 | 100 | # if the options have some treatment for transactions just apply it if there is an defined option 101 | # if ta options are discovered here, the ia_options value of this transaction instance will be set 102 | for option in options: 103 | if option in OPTIONS: 104 | OPTIONS[option].initialize(transaction=self, option=options[option]) 105 | 106 | def get_options_string(self): 107 | """ 108 | get all options in one string for debugging 109 | """ 110 | options_string = '' 111 | # put own attributes into a string 112 | options = sorted(self.__dict__.keys()) 113 | # options.sort() 114 | for option in options: 115 | # ignore some attributes 116 | if option not in IGNORED_LOG_OPTIONS and \ 117 | not self.__dict__[option] in EMPTY_OPTIONS: 118 | if option == 'addresses': 119 | if (CONST.OPTION.IA_NA or CONST.OPTION.IA_TA) in self.ia_options: 120 | option_string = f'{option}:' 121 | for address in self.__dict__[option]: 122 | option_string += f' {colonify_ip6(address)}' 123 | options_string = f'{options_string} | {option_string}' 124 | elif option == 'prefixes': 125 | if CONST.OPTION.IA_PD in self.ia_options: 126 | option_string = f'{option}:' 127 | for p in self.__dict__[option]: 128 | prefix, length = split_prefix(p) 129 | option_string += combine_prefix_length(colonify_ip6(prefix), length) 130 | elif option == 'client_llip': 131 | option_string = f'{option}: {colonify_ip6(self.__dict__[option])}' 132 | options_string = f'{options_string} | {option_string}' 133 | elif option == 'mac': 134 | if self.__dict__[option] != DUMMY_MAC: 135 | # option_string = f'{option}: {str(self.__dict__[option])}' 136 | option_string = f'{option}: {self.__dict__[option]}' 137 | options_string = f'{options_string} | {option_string}' 138 | else: 139 | # option_string = f'{option}: {str(self.__dict__[option])}' 140 | option_string = f'{option}: {self.__dict__[option]}' 141 | options_string = f'{options_string} | {option_string}' 142 | 143 | return options_string 144 | -------------------------------------------------------------------------------- /doc/clients-example.conf: -------------------------------------------------------------------------------- 1 | # These are some example clients. Every section is a client. 2 | # Every client has to have a hostname, a class and at least 3 | # one of mac or duid to be identified depending on class definition. 4 | # 5 | # The option attribute "id" can be used for address definitions of 6 | # category "id". 7 | 8 | [client1] 9 | hostname = client1 10 | mac = 01:01:01:01:01:01 11 | class = valid_client 12 | 13 | [client2] 14 | hostname = client2 15 | mac = 02:02:02:02:02:02 16 | class = invalid_client 17 | 18 | [client3] 19 | hostname = client3 20 | mac= 03:03:03:03:03:03 21 | duid = 000100011234567890abcdef1234 22 | class = valid_client 23 | 24 | [client4] 25 | hostname = client4 26 | mac = 04:04:04:04:04:04 27 | id = 4 28 | class = valid_client 29 | -------------------------------------------------------------------------------- /doc/config.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE hosts ( 2 | hostname varchar(255) NOT NULL, 3 | mac varchar(1024) DEFAULT NULL, 4 | class varchar(255) DEFAULT NULL, 5 | address varchar(255) DEFAULT NULL, 6 | prefix varchar(255) DEFAULT NULL, 7 | prefix_route_link_local INT DEFAULT 0, 8 | id varchar(255) DEFAULT NULL, 9 | duid varchar(255) DEFAULT NULL, 10 | PRIMARY KEY (hostname) 11 | ); 12 | -------------------------------------------------------------------------------- /doc/dhcpy6d-clients.conf.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | dhcpy6d-clients.conf 3 | ==================== 4 | 5 | ---------------------------------------------------- 6 | Clients configuration file for DHCPv6 server dhcpy6d 7 | ---------------------------------------------------- 8 | 9 | :Author: Copyright (C) 2012-2024 Henri Wahl 10 | :Date: 2022-06-14 11 | :Version: 1.2.2 12 | :Manual section: 5 13 | :Copyright: This manual page is licensed under the GPL-2 license. 14 | 15 | Description 16 | =========== 17 | 18 | This file contains all client configuration data if these options are set in 19 | **dhcpy6d.conf**: 20 | 21 | **store_config = file** 22 | 23 | and 24 | 25 | **store_file_config = /path/to/dhcpy6d-clients.conf** 26 | 27 | An alternative method to store client configuration is using database storage with SQLite or MySQLor PostgreSQL databases. 28 | Further details are available at ``_. 29 | 30 | This file follows RFC 822 style parsed by Python ConfigParser module. 31 | 32 | Some options allow multiple values. These have to be separated by spaces. 33 | 34 | 35 | Client sections 36 | =============== 37 | 38 | **[host_name]** 39 | Every client is configured in one section. It might have multiple attributes which are necessary depending on the configured **identification** and general address settings from *dhcpy6d.conf*. 40 | 41 | Client attributes 42 | ================= 43 | Every client section contains several attributes. **hostname** and **class** are mandatory. A third one should match at least one of the **identification** attributes configured in *dhcpy6d.conf*. 44 | 45 | Both of the following 2 attributes are necessary - the **class** and at least one of the others. 46 | 47 | Mandatory client attribute 'class' 48 | ------------------------------------- 49 | 50 | **class = ** 51 | Every client needs a class. If a client is identified, it depends from its class, which addresses it will get. 52 | This relation is configured in *dhcpy6d.conf*. 53 | 54 | Semi-mandatory client attributes 55 | -------------------------------- 56 | 57 | Depending on **identification** in *dhcpy6d.conf* clients need to have the corresponding attributes. At least one of them is needed. 58 | 59 | **mac = ** 60 | The MAC address of the Link Local Address of the client DHCPv6 request, formatted like the most usual 01:02:03:04:05:06. 61 | 62 | **duid = ** 63 | The DUID of the client which comes with the DHCPv6 request message. No hex and \\ needed, just like for example 000100011234567890abcdef1234 . 64 | 65 | **hostname = ** 66 | The client non-FQDN hostname. It will be used for dynamic DNS updates. 67 | 68 | Extra attributes 69 | ---------------- 70 | 71 | These attributes do not serve for identification of a client but for appropriate address generation. 72 | 73 | **id = ** **id** 74 | has to be a hex number in the range 0-FFFF. The client ID from this directive will be inserted in the *address pattern* of category **id** instead of the **$id$** placeholder. 75 | 76 | **address =
[
...]** 77 | Addresses configured here will be sent to a client in addition to the ones it gets due to its class. Might be useful for some extra static address definitions. 78 | 79 | **prefix = [ ...]** 80 | Prefix configured here will be sent to client in addition to the ones it gets due to its class. 81 | 82 | **prefix_route_link_local = yes|no** 83 | As default Link Local Address of requesting client is not used as router address for external call. 84 | Instead the client should be able to retrieve exactly 1 address from server to be used as router for the delegated prefix. 85 | Alternatively the client Link Local Address might be used by enabling this option. 86 | 87 | Note, that you must set this configuration option to **yes** when more than one address is assigned to the client. 88 | In this case, dhcpy6d cannot determine which of the assigned addresses should be used for routing. 89 | *Default: no* 90 | 91 | Examples 92 | ======== 93 | 94 | The next lines contain some example client definitions: 95 | 96 | | [client1] 97 | | hostname = client1 98 | | mac = 01:01:01:01:01:01 99 | | class = valid_client 100 | 101 | | [client2] 102 | | hostname = client2 103 | | mac = 02:02:02:02:02:02 104 | | class = invalid_client 105 | 106 | | [client3] 107 | | hostname = client3 108 | | duid = 000100011234567890abcdef1234 109 | | class = valid_client 110 | | address = 2001:db8::babe:1 111 | 112 | | [client4] 113 | | hostname = client4 114 | | mac = 04:04:04:04:04:04 115 | | id = 1234 116 | | class = valid_client 117 | 118 | | [client5] 119 | | hostname = client5 120 | | mac = 01:01:01:01:01:02 121 | | class = valid_client 122 | | prefix = 2001:db8::/48 123 | 124 | License 125 | ======= 126 | 127 | This program is free software; you can redistribute it 128 | and/or modify it under the terms of the GNU General Public 129 | License as published by the Free Software Foundation; either 130 | version 2 of the License, or (at your option) any later 131 | version. 132 | 133 | This program is distributed in the hope that it will be 134 | useful, but WITHOUT ANY WARRANTY; without even the implied 135 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 136 | PURPOSE. See the GNU General Public License for more 137 | details. 138 | 139 | You should have received a copy of the GNU General Public 140 | License along with this package; if not, write to the Free 141 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 142 | Boston, MA 02110-1301 USA 143 | 144 | On Debian systems, the full text of the GNU General Public 145 | License version 2 can be found in the file 146 | */usr/share/common-licenses/GPL-2*. 147 | 148 | See also 149 | ======== 150 | 151 | * dhcpy6d(8) 152 | * dhcpy6d.conf(5) 153 | * ``_ 154 | * ``_ 155 | 156 | -------------------------------------------------------------------------------- /doc/dhcpy6d-minimal.conf: -------------------------------------------------------------------------------- 1 | # dhcpy6d minimal example configuration 2 | 3 | [dhcpy6d] 4 | # Interface to listen to multicast ff02::1:2. 5 | interface = eth1 6 | # Do not identify and configure clients. 7 | store_config = none 8 | # SQLite DB for leases and LLIP-MAC-mapping. 9 | store_volatile = sqlite 10 | store_sqlite_volatile = volatile.sqlite 11 | # Not really necessary but might help for debugging. 12 | log = on 13 | log_console = on 14 | 15 | # Special address type which applies to all not specially 16 | # configured clients. 17 | [address_default] 18 | # Choosing MAC-based addresses. 19 | category = mac 20 | # ULA-type address pattern. 21 | pattern = fd01:db8:dead:bad:beef:$mac$ -------------------------------------------------------------------------------- /doc/dhcpy6d.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | dhcpy6d 3 | ======= 4 | 5 | ---------------------------------------------------------------- 6 | MAC address aware DHCPv6 server 7 | ---------------------------------------------------------------- 8 | 9 | :Author: Copyright (C) 2012-2024 Henri Wahl 10 | :Date: 2022-06-14 11 | :Version: 1.2.2 12 | :Manual section: 8 13 | :Copyright: This manual page is licensed under the GPL-2 license. 14 | 15 | 16 | Synopsis 17 | ======== 18 | 19 | **dhcpy6d** [**-c** *file*] [**-u** *user*] [**-g** *group*] [**-p** *prefix*] [**-r** *yes|no*] [**-d** *duid*] [**-m** *message*] [**-G**] 20 | 21 | 22 | Description 23 | =========== 24 | **dhcpy6d** is an open source server for DHCPv6, the DHCP protocol for IPv6. 25 | 26 | Its development is driven by the need to be able to use the existing 27 | IPv4 infrastructure in coexistence with IPv6. In a dualstack 28 | scenario, the existing DHCPv4 most probably uses MAC addresses of 29 | clients to identify them. This is not intended by RFC 3315 for 30 | DHCPv6, but also not forbidden. Dhcpy6d is able to do so in local 31 | network segments and therefore offers a pragmatical method for 32 | parallel use of DHCPv4 and DHCPv6, because existing client management 33 | solutions could be used further. 34 | 35 | **dhcpy6d** comes with the following features: 36 | 37 | * identifies clients by MAC address, DUID or hostname 38 | * generates addresses randomly, by MAC address, by range, by given ID or from DNS name 39 | * filters clients by MAC, DUID or hostname 40 | * assigns multiple addresses per client 41 | * allows one to organize clients in different classes 42 | * stores leases in MySQL, PostgreSQL or SQLite database 43 | * client information can be retrieved from MySQL or PostgreSQL database or textfile 44 | * dynamically updates DNS (Bind) 45 | * supports rapid commit 46 | * listens on multiple interfaces 47 | 48 | Options 49 | ======= 50 | 51 | Most configuration is done via the configuration file. 52 | 53 | **-c, --config=** 54 | Set the configuration file to use. Default is /etc/dhcpy6d.conf. 55 | 56 | **-u, --user=** 57 | Set the unprivileged user to be used. 58 | 59 | **-g, --group=** 60 | Set the unprivileged group to be used. 61 | 62 | **-r, --really-do-it=** 63 | Really activate the DHCPv6 server. This is a precaution to prevent larger network trouble. 64 | 65 | **-d, --duid=** 66 | Set the DUID for the server. This argument is used by /etc/init.d/dhcpy6d and /lib/systemd/system/dhcpy6d.service respectively. 67 | 68 | **-p, --prefix=** 69 | Set the prefix which will be substituted for the $prefix$ variable in address definitions. Useful for setups where the ISP uses a changing prefix. 70 | 71 | **-G, --generate-duid** 72 | Generate DUID to be used in config file. This argument is used to generate a DUID for /etc/default/dhcpy6d. After generation dhcpy6d exits. 73 | 74 | **-m, --message ""** 75 | Send message to running dhcpy6d server. At the moment the only valid message is *"prefix "*. The value of ** will be used instantly where *$prefix$* is to be replaced as placeholder in address definitions. This might be of use for dynamic prefixes by ISPs, for example: *dhcpy6d -m "prefix 2001:db8"*. 76 | 77 | Files 78 | ===== 79 | 80 | * /etc/dhcpy6d.conf 81 | * /etc/dhcpy6d-clients.conf 82 | * /var/lib/dhcpy6d/ 83 | * /var/log/dhcpy6d.log 84 | 85 | 86 | License 87 | ======= 88 | 89 | This program is free software; you can redistribute it 90 | and/or modify it under the terms of the GNU General Public 91 | License as published by the Free Software Foundation; either 92 | version 2 of the License, or (at your option) any later 93 | version. 94 | 95 | This program is distributed in the hope that it will be 96 | useful, but WITHOUT ANY WARRANTY; without even the implied 97 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 98 | PURPOSE. See the GNU General Public License for more 99 | details. 100 | 101 | You should have received a copy of the GNU General Public 102 | License along with this package; if not, write to the Free 103 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 104 | Boston, MA 02110-1301 USA 105 | 106 | On Debian systems, the full text of the GNU General Public 107 | License version 2 can be found in the file 108 | */usr/share/common-licenses/GPL-2*. 109 | 110 | See also 111 | ======== 112 | * dhcpy6d.conf(5) 113 | * dhcpy6d-clients.conf(5) 114 | * ``_ 115 | * ``_ 116 | 117 | 118 | -------------------------------------------------------------------------------- /doc/volatile.postgresql: -------------------------------------------------------------------------------- 1 | CREATE TABLE leases ( 2 | address varchar(32) NOT NULL, 3 | active smallint NOT NULL, 4 | preferred_lifetime int NOT NULL, 5 | valid_lifetime int NOT NULL, 6 | hostname varchar(255) NOT NULL, 7 | type varchar(255) NOT NULL, 8 | category varchar(255) NOT NULL, 9 | ia_type varchar(255) NOT NULL, 10 | class varchar(255) NOT NULL, 11 | mac varchar(17) NOT NULL, 12 | duid varchar(255) NOT NULL, 13 | last_update bigint NOT NULL, 14 | preferred_until bigint NOT NULL, 15 | valid_until bigint NOT NULL, 16 | iaid varchar(8) DEFAULT NULL, 17 | last_message int NOT NULL DEFAULT 0, 18 | PRIMARY KEY (address) 19 | ); 20 | 21 | CREATE TABLE macs_llips ( 22 | mac varchar(17) NOT NULL, 23 | link_local_ip varchar(39) NOT NULL, 24 | last_update bigint NOT NULL, 25 | PRIMARY KEY (mac) 26 | ); 27 | 28 | CREATE TABLE prefixes ( 29 | prefix varchar(32) NOT NULL, 30 | length smallint NOT NULL, 31 | active smallint NOT NULL, 32 | preferred_lifetime int NOT NULL, 33 | valid_lifetime int NOT NULL, 34 | hostname varchar(255) NOT NULL, 35 | type varchar(255) NOT NULL, 36 | category varchar(255) NOT NULL, 37 | class varchar(255) NOT NULL, 38 | mac varchar(17) NOT NULL, 39 | duid varchar(255) NOT NULL, 40 | last_update bigint NOT NULL, 41 | preferred_until bigint NOT NULL, 42 | valid_until bigint NOT NULL, 43 | iaid varchar(8) DEFAULT NULL, 44 | last_message int NOT NULL DEFAULT 0, 45 | PRIMARY KEY (prefix) 46 | ); 47 | 48 | CREATE TABLE meta ( 49 | item_key varchar(255) NOT NULL, 50 | item_value varchar(255) NOT NULL, 51 | PRIMARY KEY (item_key) 52 | ); 53 | 54 | CREATE TABLE routes ( 55 | prefix varchar(32) NOT NULL, 56 | length smallint NOT NULL, 57 | router varchar(32) NOT NULL, 58 | last_update bigint NOT NULL, 59 | PRIMARY KEY (prefix) 60 | ); 61 | 62 | INSERT INTO meta (item_key, item_value) VALUES ('version', '3'); 63 | -------------------------------------------------------------------------------- /doc/volatile.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE leases ( 2 | address varchar(32) NOT NULL, 3 | active tinyint(4) NOT NULL, 4 | preferred_lifetime int(11) NOT NULL, 5 | valid_lifetime int(11) NOT NULL, 6 | hostname varchar(255) NOT NULL, 7 | type varchar(255) NOT NULL, 8 | category varchar(255) NOT NULL, 9 | ia_type varchar(255) NOT NULL, 10 | class varchar(255) NOT NULL, 11 | mac varchar(17) NOT NULL, 12 | duid varchar(255) NOT NULL, 13 | last_update bigint NOT NULL, 14 | preferred_until bigint NOT NULL, 15 | valid_until bigint NOT NULL, 16 | iaid varchar(8) DEFAULT NULL, 17 | last_message int(11) NOT NULL DEFAULT 0, 18 | PRIMARY KEY (address) 19 | ); 20 | 21 | CREATE TABLE macs_llips ( 22 | mac varchar(17) NOT NULL, 23 | link_local_ip varchar(39) NOT NULL, 24 | last_update bigint NOT NULL, 25 | PRIMARY KEY (mac) 26 | ); 27 | 28 | CREATE TABLE prefixes ( 29 | prefix varchar(32) NOT NULL, 30 | length tinyint(4) NOT NULL, 31 | active tinyint(4) NOT NULL, 32 | preferred_lifetime int(11) NOT NULL, 33 | valid_lifetime int(11) NOT NULL, 34 | hostname varchar(255) NOT NULL, 35 | type varchar(255) NOT NULL, 36 | category varchar(255) NOT NULL, 37 | class varchar(255) NOT NULL, 38 | mac varchar(17) NOT NULL, 39 | duid varchar(255) NOT NULL, 40 | last_update bigint NOT NULL, 41 | preferred_until bigint NOT NULL, 42 | valid_until bigint NOT NULL, 43 | iaid varchar(8) DEFAULT NULL, 44 | last_message int(11) NOT NULL DEFAULT 0, 45 | PRIMARY KEY (prefix) 46 | ); 47 | 48 | CREATE TABLE meta ( 49 | item_key varchar(255) NOT NULL, 50 | item_value varchar(255) NOT NULL, 51 | PRIMARY KEY (item_key) 52 | ); 53 | 54 | CREATE TABLE routes ( 55 | prefix varchar(32) NOT NULL, 56 | length tinyint(4) NOT NULL, 57 | router varchar(32) NOT NULL, 58 | last_update bigint NOT NULL, 59 | PRIMARY KEY (prefix) 60 | ); 61 | 62 | INSERT INTO meta (item_key, item_value) VALUES ('version', '3'); 63 | -------------------------------------------------------------------------------- /etc/default/dhcpy6d: -------------------------------------------------------------------------------- 1 | # dhcpy6d is disabled by default 2 | RUN=no 3 | -------------------------------------------------------------------------------- /etc/dhcpy6d.conf: -------------------------------------------------------------------------------- 1 | # dhcpy6d default configuration 2 | # 3 | # Please see the examples in /usr/share/doc/dhcpy6d and 4 | # https://dhcpy6.de/documentation for more information. 5 | 6 | [dhcpy6d] 7 | # Interface to listen to multicast ff02::1:2. 8 | interface = eth0 9 | # Do not identify and configure clients. 10 | store_config = none 11 | # SQLite DB for leases and LLIP-MAC-mapping. 12 | store_volatile = sqlite 13 | store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite 14 | log = on 15 | log_file = /var/log/dhcpy6d.log 16 | 17 | # set to yes to really answer to clients 18 | # not necessary in Debian where it comes from /etc/default/dhcpy6d and /etc/init.d/dhcpy6 19 | #really_do_it = no 20 | 21 | # Special address type which applies to all not specially 22 | # configured clients. 23 | [address_default] 24 | # Choosing MAC-based addresses. 25 | category = mac 26 | # ULA-type address pattern. 27 | pattern = fd01:db8:dead:bad:beef:$mac$ 28 | -------------------------------------------------------------------------------- /etc/logrotate.d/dhcpy6d: -------------------------------------------------------------------------------- 1 | /var/log/dhcpy6d.log { 2 | weekly 3 | missingok 4 | rotate 4 5 | compress 6 | notifempty 7 | create 660 dhcpy6d dhcpy6d 8 | } 9 | 10 | -------------------------------------------------------------------------------- /lib/systemd/system/dhcpy6d.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=DHCPv6 Server Daemon 3 | Documentation=man:dhcpy6d(8) man:dhcpy6d.conf(5) man:dhcpy6d-clients.conf(5) 4 | Wants=network-online.target 5 | After=network-online.target 6 | After=time-sync.target 7 | 8 | [Service] 9 | ExecStart=/usr/sbin/dhcpy6d --config /etc/dhcpy6d.conf --user dhcpy6d --group dhcpy6d 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # DHCPy6d DHCPv6 Daemon 4 | # 5 | # Copyright (C) 2009-2024 Henri Wahl 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 20 | 21 | import distro 22 | import sys 23 | 24 | # access /usr/share/pyshared on Debian 25 | # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=715010 26 | 27 | if distro.id() == 'debian': 28 | sys.path[0:0] = ['/usr/share/pyshared'] 29 | 30 | import grp 31 | import pwd 32 | import os 33 | import socket 34 | 35 | from dhcpy6d import UDPMulticastIPv6 36 | from dhcpy6d.config import cfg 37 | 38 | from dhcpy6d.globals import (config_answer_queue, 39 | config_query_queue, 40 | IF_NAME, 41 | route_queue, 42 | volatile_answer_queue, 43 | volatile_query_queue) 44 | from dhcpy6d.log import log 45 | from dhcpy6d.handler import RequestHandler 46 | 47 | from dhcpy6d.route import manage_prefixes_routes 48 | from dhcpy6d.storage import (config_store, 49 | QueryQueue, 50 | volatile_store) 51 | from dhcpy6d.threads import (DNSQueryThread, 52 | RouteThread, 53 | TidyUpThread, 54 | TimerThread) 55 | 56 | 57 | # main part, initializing all stuff 58 | def run(): 59 | log.info('Starting dhcpy6d daemon...') 60 | log.info(f'Server DUID: {cfg.SERVERDUID}') 61 | 62 | # configure SocketServer 63 | UDPMulticastIPv6.address_family = socket.AF_INET6 64 | udp_server = UDPMulticastIPv6(('', 547), RequestHandler) 65 | 66 | # start query queue watcher 67 | config_query_queue_watcher = QueryQueue(name='config_query_queue', 68 | store_type=config_store, 69 | query_queue=config_query_queue, 70 | answer_queue=config_answer_queue) 71 | config_query_queue_watcher.start() 72 | volatile_query_queue_watcher = QueryQueue(name='volatile_query_queue', 73 | store_type=volatile_store, 74 | query_queue=volatile_query_queue, 75 | answer_queue=volatile_answer_queue) 76 | volatile_query_queue_watcher.start() 77 | 78 | # check if database tables are up-to-date or exist and create them if not 79 | volatile_store.check_storage() 80 | 81 | # check if config database - if any - supports prefixes 82 | config_store.check_config_prefixes_support() 83 | 84 | # if global dynamic prefix was not given take it from database - only possible after database initialisation 85 | if cfg.PREFIX == '': 86 | cfg.PREFIX = volatile_store.get_dynamic_prefix() 87 | if cfg.PREFIX is None: 88 | cfg.PREFIX = '' 89 | 90 | # apply dynamic prefix to addresses and prefixes 91 | for a in cfg.ADDRESSES: 92 | cfg.ADDRESSES[a].inject_dynamic_prefix_into_prototype(cfg.PREFIX) 93 | for p in cfg.PREFIXES: 94 | cfg.PREFIXES[p].inject_dynamic_prefix_into_prototype(cfg.PREFIX) 95 | 96 | # collect all known MAC addresses from database 97 | if cfg.CACHE_MAC_LLIP: 98 | volatile_store.collect_macs_from_db() 99 | 100 | # start timer 101 | timer_thread = TimerThread() 102 | timer_thread.start() 103 | 104 | # start route queue to care for routes in background 105 | route_thread = RouteThread(route_queue) 106 | route_thread.start() 107 | 108 | # delete invalid and add valid routes - useful after reboot 109 | if cfg.MANAGE_ROUTES_AT_START: 110 | manage_prefixes_routes() 111 | 112 | # start TidyUp thread for cleaning in background 113 | tidyup_thread = TidyUpThread() 114 | tidyup_thread.start() 115 | 116 | # start DNS query queue to care for DNS in background 117 | dns_query_thread = DNSQueryThread() 118 | dns_query_thread.start() 119 | 120 | # set user and group 121 | log.info(f'Running as user {cfg.USER} (UID {pwd.getpwnam(cfg.USER).pw_uid}) and ' 122 | f'group {cfg.GROUP} (GID {grp.getgrnam(cfg.GROUP).gr_gid})') 123 | # first set group because otherwise the freshly unprivileged user could not modify its groups itself 124 | os.setgid(grp.getgrnam(cfg.GROUP).gr_gid) 125 | os.setuid(pwd.getpwnam(cfg.USER).pw_uid) 126 | 127 | # log interfaces 128 | log.info(f'Listening on interfaces: {" ".join(IF_NAME)}') 129 | 130 | # serve forever 131 | try: 132 | udp_server.serve_forever() 133 | except KeyboardInterrupt: 134 | sys.exit(0) 135 | 136 | 137 | if __name__ == '__main__': 138 | run() 139 | -------------------------------------------------------------------------------- /man/man5/dhcpy6d-clients.conf.5: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | . 4 | .nr rst2man-indent-level 0 5 | . 6 | .de1 rstReportMargin 7 | \\$1 \\n[an-margin] 8 | level \\n[rst2man-indent-level] 9 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 10 | - 11 | \\n[rst2man-indent0] 12 | \\n[rst2man-indent1] 13 | \\n[rst2man-indent2] 14 | .. 15 | .de1 INDENT 16 | .\" .rstReportMargin pre: 17 | . RS \\$1 18 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 19 | . nr rst2man-indent-level +1 20 | .\" .rstReportMargin post: 21 | .. 22 | .de UNINDENT 23 | . RE 24 | .\" indent \\n[an-margin] 25 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 26 | .nr rst2man-indent-level -1 27 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 28 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 29 | .. 30 | .TH "DHCPY6D-CLIENTS.CONF" 5 "2022-06-14" "1.2.2" "" 31 | .SH NAME 32 | dhcpy6d-clients.conf \- Clients configuration file for DHCPv6 server dhcpy6d 33 | .SH DESCRIPTION 34 | .sp 35 | This file contains all client configuration data if these options are set in 36 | \fBdhcpy6d.conf\fP: 37 | .sp 38 | \fBstore_config = file\fP 39 | .sp 40 | and 41 | .sp 42 | \fBstore_file_config = /path/to/dhcpy6d\-clients.conf\fP 43 | .sp 44 | An alternative method to store client configuration is using database storage with SQLite or MySQLor PostgreSQL databases. 45 | Further details are available at \fI\%https://dhcpy6d.de/documentation/config\fP\&. 46 | .sp 47 | This file follows RFC 822 style parsed by Python ConfigParser module. 48 | .sp 49 | Some options allow multiple values. These have to be separated by spaces. 50 | .SH CLIENT SECTIONS 51 | .INDENT 0.0 52 | .TP 53 | .B \fB[host_name]\fP 54 | Every client is configured in one section. It might have multiple attributes which are necessary depending on the configured \fBidentification\fP and general address settings from \fIdhcpy6d.conf\fP\&. 55 | .UNINDENT 56 | .SH CLIENT ATTRIBUTES 57 | .sp 58 | Every client section contains several attributes. \fBhostname\fP and \fBclass\fP are mandatory. A third one should match at least one of the \fBidentification\fP attributes configured in \fIdhcpy6d.conf\fP\&. 59 | .sp 60 | Both of the following 2 attributes are necessary \- the \fBclass\fP and at least one of the others. 61 | .SS Mandatory client attribute \(aqclass\(aq 62 | .INDENT 0.0 63 | .TP 64 | .B \fBclass = \fP 65 | Every client needs a class. If a client is identified, it depends from its class, which addresses it will get. 66 | This relation is configured in \fIdhcpy6d.conf\fP\&. 67 | .UNINDENT 68 | .SS Semi\-mandatory client attributes 69 | .sp 70 | Depending on \fBidentification\fP in \fIdhcpy6d.conf\fP clients need to have the corresponding attributes. At least one of them is needed. 71 | .INDENT 0.0 72 | .TP 73 | .B \fBmac = \fP 74 | The MAC address of the Link Local Address of the client DHCPv6 request, formatted like the most usual 01:02:03:04:05:06. 75 | .TP 76 | .B \fBduid = \fP 77 | The DUID of the client which comes with the DHCPv6 request message. No hex and \e needed, just like for example 000100011234567890abcdef1234 . 78 | .TP 79 | .B \fBhostname = \fP 80 | The client non\-FQDN hostname. It will be used for dynamic DNS updates. 81 | .UNINDENT 82 | .SS Extra attributes 83 | .sp 84 | These attributes do not serve for identification of a client but for appropriate address generation. 85 | .INDENT 0.0 86 | .TP 87 | .B \fBid = \fP \fBid\fP 88 | has to be a hex number in the range 0\-FFFF. The client ID from this directive will be inserted in the \fIaddress pattern\fP of category \fBid\fP instead of the \fB$id$\fP placeholder. 89 | .TP 90 | .B \fBaddress =
[
...]\fP 91 | Addresses configured here will be sent to a client in addition to the ones it gets due to its class. Might be useful for some extra static address definitions. 92 | .TP 93 | .B \fBprefix = [ ...]\fP 94 | Prefix configured here will be sent to client in addition to the ones it gets due to its class. 95 | .TP 96 | .B \fBprefix_route_link_local = yes|no\fP 97 | As default Link Local Address of requesting client is not used as router address for external call. 98 | Instead the client should be able to retrieve exactly 1 address from server to be used as router for the delegated prefix. 99 | Alternatively the client Link Local Address might be used by enabling this option. 100 | .sp 101 | Note, that you must set this configuration option to \fByes\fP when more than one address is assigned to the client. 102 | In this case, dhcpy6d cannot determine which of the assigned addresses should be used for routing. 103 | \fIDefault: no\fP 104 | .UNINDENT 105 | .SH EXAMPLES 106 | .sp 107 | The next lines contain some example client definitions: 108 | .nf 109 | [client1] 110 | hostname = client1 111 | mac = 01:01:01:01:01:01 112 | class = valid_client 113 | .fi 114 | .sp 115 | .nf 116 | [client2] 117 | hostname = client2 118 | mac = 02:02:02:02:02:02 119 | class = invalid_client 120 | .fi 121 | .sp 122 | .nf 123 | [client3] 124 | hostname = client3 125 | duid = 000100011234567890abcdef1234 126 | class = valid_client 127 | address = 2001:db8::babe:1 128 | .fi 129 | .sp 130 | .nf 131 | [client4] 132 | hostname = client4 133 | mac = 04:04:04:04:04:04 134 | id = 1234 135 | class = valid_client 136 | .fi 137 | .sp 138 | .nf 139 | [client5] 140 | hostname = client5 141 | mac = 01:01:01:01:01:02 142 | class = valid_client 143 | prefix = 2001:db8::/48 144 | .fi 145 | .sp 146 | .SH LICENSE 147 | .sp 148 | This program is free software; you can redistribute it 149 | and/or modify it under the terms of the GNU General Public 150 | License as published by the Free Software Foundation; either 151 | version 2 of the License, or (at your option) any later 152 | version. 153 | .sp 154 | This program is distributed in the hope that it will be 155 | useful, but WITHOUT ANY WARRANTY; without even the implied 156 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 157 | PURPOSE. See the GNU General Public License for more 158 | details. 159 | .sp 160 | You should have received a copy of the GNU General Public 161 | License along with this package; if not, write to the Free 162 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 163 | Boston, MA 02110\-1301 USA 164 | .sp 165 | On Debian systems, the full text of the GNU General Public 166 | License version 2 can be found in the file 167 | \fI/usr/share/common\-licenses/GPL\-2\fP\&. 168 | .SH SEE ALSO 169 | .INDENT 0.0 170 | .IP \(bu 2 171 | dhcpy6d(8) 172 | .IP \(bu 2 173 | dhcpy6d.conf(5) 174 | .IP \(bu 2 175 | \fI\%https://dhcpy6d.de\fP 176 | .IP \(bu 2 177 | \fI\%https://github.com/HenriWahl/dhcpy6d\fP 178 | .UNINDENT 179 | .SH AUTHOR 180 | Copyright (C) 2012-2024 Henri Wahl 181 | .SH COPYRIGHT 182 | This manual page is licensed under the GPL-2 license. 183 | .\" Generated by docutils manpage writer. 184 | . 185 | -------------------------------------------------------------------------------- /man/man8/dhcpy6d.8: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | . 4 | .nr rst2man-indent-level 0 5 | . 6 | .de1 rstReportMargin 7 | \\$1 \\n[an-margin] 8 | level \\n[rst2man-indent-level] 9 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 10 | - 11 | \\n[rst2man-indent0] 12 | \\n[rst2man-indent1] 13 | \\n[rst2man-indent2] 14 | .. 15 | .de1 INDENT 16 | .\" .rstReportMargin pre: 17 | . RS \\$1 18 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 19 | . nr rst2man-indent-level +1 20 | .\" .rstReportMargin post: 21 | .. 22 | .de UNINDENT 23 | . RE 24 | .\" indent \\n[an-margin] 25 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 26 | .nr rst2man-indent-level -1 27 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 28 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 29 | .. 30 | .TH "DHCPY6D" 8 "2022-06-14" "1.2.2" "" 31 | .SH NAME 32 | dhcpy6d \- MAC address aware DHCPv6 server 33 | .SH SYNOPSIS 34 | .sp 35 | \fBdhcpy6d\fP [\fB\-c\fP \fIfile\fP] [\fB\-u\fP \fIuser\fP] [\fB\-g\fP \fIgroup\fP] [\fB\-p\fP \fIprefix\fP] [\fB\-r\fP \fIyes|no\fP] [\fB\-d\fP \fIduid\fP] [\fB\-m\fP \fImessage\fP] [\fB\-G\fP] 36 | .SH DESCRIPTION 37 | .sp 38 | \fBdhcpy6d\fP is an open source server for DHCPv6, the DHCP protocol for IPv6. 39 | .sp 40 | Its development is driven by the need to be able to use the existing 41 | IPv4 infrastructure in coexistence with IPv6. In a dualstack 42 | scenario, the existing DHCPv4 most probably uses MAC addresses of 43 | clients to identify them. This is not intended by RFC 3315 for 44 | DHCPv6, but also not forbidden. Dhcpy6d is able to do so in local 45 | network segments and therefore offers a pragmatical method for 46 | parallel use of DHCPv4 and DHCPv6, because existing client management 47 | solutions could be used further. 48 | .sp 49 | \fBdhcpy6d\fP comes with the following features: 50 | .INDENT 0.0 51 | .IP \(bu 2 52 | identifies clients by MAC address, DUID or hostname 53 | .IP \(bu 2 54 | generates addresses randomly, by MAC address, by range, by given ID or from DNS name 55 | .IP \(bu 2 56 | filters clients by MAC, DUID or hostname 57 | .IP \(bu 2 58 | assigns multiple addresses per client 59 | .IP \(bu 2 60 | allows one to organize clients in different classes 61 | .IP \(bu 2 62 | stores leases in MySQL, PostgreSQL or SQLite database 63 | .IP \(bu 2 64 | client information can be retrieved from MySQL or PostgreSQL database or textfile 65 | .IP \(bu 2 66 | dynamically updates DNS (Bind) 67 | .IP \(bu 2 68 | supports rapid commit 69 | .IP \(bu 2 70 | listens on multiple interfaces 71 | .UNINDENT 72 | .SH OPTIONS 73 | .sp 74 | Most configuration is done via the configuration file. 75 | .INDENT 0.0 76 | .TP 77 | .B \fB\-c, \-\-config=\fP 78 | Set the configuration file to use. Default is /etc/dhcpy6d.conf. 79 | .TP 80 | .B \fB\-u, \-\-user=\fP 81 | Set the unprivileged user to be used. 82 | .TP 83 | .B \fB\-g, \-\-group=\fP 84 | Set the unprivileged group to be used. 85 | .TP 86 | .B \fB\-r, \-\-really\-do\-it=\fP 87 | Really activate the DHCPv6 server. This is a precaution to prevent larger network trouble. 88 | .TP 89 | .B \fB\-d, \-\-duid=\fP 90 | Set the DUID for the server. This argument is used by /etc/init.d/dhcpy6d and /lib/systemd/system/dhcpy6d.service respectively. 91 | .TP 92 | .B \fB\-p, \-\-prefix=\fP 93 | Set the prefix which will be substituted for the $prefix$ variable in address definitions. Useful for setups where the ISP uses a changing prefix. 94 | .TP 95 | .B \fB\-G, \-\-generate\-duid\fP 96 | Generate DUID to be used in config file. This argument is used to generate a DUID for /etc/default/dhcpy6d. After generation dhcpy6d exits. 97 | .TP 98 | .B \fB\-m, \-\-message \(dq\(dq\fP 99 | Send message to running dhcpy6d server. At the moment the only valid message is \fI\(dqprefix \(dq\fP\&. The value of \fI\fP will be used instantly where \fI$prefix$\fP is to be replaced as placeholder in address definitions. This might be of use for dynamic prefixes by ISPs, for example: \fIdhcpy6d \-m \(dqprefix 2001:db8\(dq\fP\&. 100 | .UNINDENT 101 | .SH FILES 102 | .INDENT 0.0 103 | .IP \(bu 2 104 | /etc/dhcpy6d.conf 105 | .IP \(bu 2 106 | /etc/dhcpy6d\-clients.conf 107 | .IP \(bu 2 108 | /var/lib/dhcpy6d/ 109 | .IP \(bu 2 110 | /var/log/dhcpy6d.log 111 | .UNINDENT 112 | .SH LICENSE 113 | .sp 114 | This program is free software; you can redistribute it 115 | and/or modify it under the terms of the GNU General Public 116 | License as published by the Free Software Foundation; either 117 | version 2 of the License, or (at your option) any later 118 | version. 119 | .sp 120 | This program is distributed in the hope that it will be 121 | useful, but WITHOUT ANY WARRANTY; without even the implied 122 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 123 | PURPOSE. See the GNU General Public License for more 124 | details. 125 | .sp 126 | You should have received a copy of the GNU General Public 127 | License along with this package; if not, write to the Free 128 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 129 | Boston, MA 02110\-1301 USA 130 | .sp 131 | On Debian systems, the full text of the GNU General Public 132 | License version 2 can be found in the file 133 | \fI/usr/share/common\-licenses/GPL\-2\fP\&. 134 | .SH SEE ALSO 135 | .INDENT 0.0 136 | .IP \(bu 2 137 | dhcpy6d.conf(5) 138 | .IP \(bu 2 139 | dhcpy6d\-clients.conf(5) 140 | .IP \(bu 2 141 | \fI\%https://dhcpy6d.de\fP 142 | .IP \(bu 2 143 | \fI\%https://github.com/HenriWahl/dhcpy6d\fP 144 | .UNINDENT 145 | .SH AUTHOR 146 | Copyright (C) 2012-2024 Henri Wahl 147 | .SH COPYRIGHT 148 | This manual page is licensed under the GPL-2 license. 149 | .\" Generated by docutils manpage writer. 150 | . 151 | -------------------------------------------------------------------------------- /redhat/dhcpy6d.spec: -------------------------------------------------------------------------------- 1 | %{?!dhcpy6d_uid: %define dhcpy6d_uid dhcpy6d} 2 | %{?!dhcpy6d_gid: %define dhcpy6d_gid %dhcpy6d_uid} 3 | 4 | %{!?python3_sitelib: %global python3_sitelib %(%{__python3} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} 5 | 6 | Name: dhcpy6d 7 | Version: 1.0.9 8 | Release: 1%{?dist} 9 | Summary: DHCPv6 server daemon 10 | 11 | %if 0%{?suse_version} 12 | Group: Productivity/Networking/Boot/Servers 13 | %else 14 | Group: System Environment/Daemons 15 | %endif 16 | 17 | License: GPLv2 18 | URL: https://dhcpy6d.de 19 | Source0: https://github.com/HenriWahl/%{name}/archive/refs/tags/v%{version}.tar.gz 20 | # in order to build from tarball 21 | # tar -zxvf dhcpy6d-%%{version}.tar.gz -C ~/ dhcpy6d-%%{version}/redhat/init.d/dhcpy6d --strip-components=4&& rpmbuild -ta dhcpy6d-%%{version}.tar.gz&& rm -f ~/dhcpy6d 22 | Source1: %{name} 23 | 24 | BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) 25 | BuildArch: noarch 26 | 27 | BuildRequires: python3 28 | BuildRequires: python3-setuptools 29 | BuildRequires: python3-devel 30 | Requires: python3 31 | 32 | BuildRequires: systemd 33 | Requires: systemd 34 | 35 | %if 0%{?suse_version} 36 | Requires: python3-mysql 37 | Requires: python3-dnspython 38 | %else 39 | Requires: python3-distro 40 | Requires: python3-dns 41 | Requires: python3-PyMySQL 42 | %endif 43 | 44 | Requires: coreutils 45 | Requires: filesystem 46 | Requires(pre): /usr/sbin/useradd, /usr/sbin/groupadd 47 | Requires(post): coreutils, filesystem, systemd 48 | 49 | Requires(preun): coreutils, /usr/sbin/userdel, /usr/sbin/groupdel 50 | Requires: logrotate 51 | 52 | %description 53 | Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack transition, addresses may be generated randomly, by range, by DNS, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically. 54 | 55 | %prep 56 | %setup -q 57 | 58 | %build 59 | %py3_build 60 | #CFLAGS="%{optflags}" %{__python3} setup.py build 61 | 62 | 63 | %install 64 | %{__python3} setup.py install --skip-build --prefix=%{_prefix} --install-scripts=%{_sbindir} --root=%{buildroot} 65 | install -p -D -m 644 %{S:1} %{buildroot}%{_unitdir}/%{name}.service 66 | install -p -D -m 644 etc/logrotate.d/%{name} %{buildroot}%{_sysconfdir}/logrotate.d/%{name} 67 | /bin/chmod 0550 %{buildroot}%{_sbindir}/%{name} 68 | 69 | %pre 70 | # enable that only for non-root user! 71 | %if "%{dhcpy6d_uid}" != "root" 72 | /usr/sbin/groupadd -f -r %{dhcpy6d_gid} > /dev/null 2>&1 || : 73 | /usr/sbin/useradd -r -s /sbin/nologin -d /var/lib/%{name} -M \ 74 | -g %{dhcpy6d_gid} %{dhcpy6d_uid} > /dev/null 2>&1 || : 75 | %endif 76 | 77 | # backup existing volatile.sqlite 78 | file=/var/lib/%{name}/volatile.sqlite 79 | if [ -f ${file} ] 80 | then 81 | /bin/cp -a ${file} ${file}.backup-%{version}-%{release} 82 | fi 83 | 84 | %post 85 | file=/var/log/%{name}.log 86 | if [ ! -f ${file} ] 87 | then 88 | /bin/touch ${file} 89 | fi 90 | 91 | file=/var/lib/%{name}/volatile.sqlite 92 | # restore backup volatile.sqlite 93 | if [ -f ${file}.backup-%{version}-%{release} ] 94 | then 95 | /bin/mv ${file}.backup-%{version}-%{release} ${file} 96 | fi 97 | 98 | # set permissions on folder and create empty volatile.sqlite if it does not yet exist 99 | if [ ! -f ${file} ] 100 | then 101 | /bin/touch ${file} 102 | /bin/chmod 0775 %{_localstatedir}/lib/%{name} 103 | fi 104 | /bin/chown %{dhcpy6d_uid}:%{dhcpy6d_gid} ${file} 105 | /bin/chmod 0640 ${file} 106 | 107 | %preun 108 | if [ "$1" = "0" ]; then 109 | /bin/systemctl %{name}.service stop > /dev/null 2>&1 || : 110 | /bin/rm -f /var/lib/%{name}/pid > /dev/null 2>&1 || : 111 | %{?stop_on_removal: 112 | %{stop_on_removal %{name}} 113 | } 114 | %{!?stop_on_removal: 115 | # undefined 116 | /bin/systemctl disable %{name}.service 117 | } 118 | # enable that only for non-root user! 119 | %if "%{dhcpy6d_uid}" != "root" 120 | /usr/sbin/userdel %{dhcpy6d_uid} 121 | if [ ! `grep %{dhcpy6d_gid} /etc/group` = "" ]; then 122 | /usr/sbin/groupdel %{dhcpy6d_uid} 123 | fi 124 | %endif 125 | fi 126 | 127 | %postun 128 | if [ $1 -ge 1 ]; then 129 | %{?restart_on_update: 130 | %{restart_on_update %{name}} 131 | } 132 | %{!?restart_on_update: 133 | # undefined 134 | /bin/systemctl start %{name}.service > /dev/null 2>&1 || : 135 | } 136 | fi 137 | 138 | 139 | %files 140 | %doc 141 | %{_defaultdocdir}/* 142 | %{_mandir}/man?/* 143 | %{_sbindir}/%{name} 144 | %{python3_sitelib}/*dhcpy6* 145 | %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} 146 | %config(noreplace) %{_sysconfdir}/%{name}.conf 147 | %exclude %{_localstatedir}/log/%{name}.log 148 | %{_unitdir}/%{name}.service 149 | %dir %attr(0775,%{dhcpy6d_uid},%{dhcpy6d_gid}) %{_localstatedir}/lib/%{name} 150 | %config(noreplace) %attr(0644,%{dhcpy6d_uid},%{dhcpy6d_gid}) %{_localstatedir}/lib/%{name}/volatile.sqlite 151 | 152 | %changelog 153 | * Fri Jul 24 2020 Henri Wahl - 1.0.1-1 154 | - New upstream release 155 | 156 | * Fri Apr 03 2020 Henri Wahl - 1.0-1 157 | - New upstream release 158 | 159 | * Mon Apr 30 2018 Henri Wahl - 0.7-1 160 | - New upstream release 161 | 162 | * Fri Sep 15 2017 Henri Wahl - 0.6-1 163 | - New upstream release 164 | 165 | * Mon May 29 2017 Henri Wahl - 0.5-1 166 | - New upstream release 167 | 168 | * Sat Dec 26 2015 Henri Wahl - 0.4.3-1 169 | - New upstream release 170 | 171 | * Tue Aug 18 2015 Henri Wahl - 0.4.2-1 172 | - New upstream release 173 | 174 | * Tue Mar 17 2015 Henri Wahl - 0.4.1-1 175 | - New upstream release 176 | 177 | * Tue Oct 21 2014 Henri Wahl - 0.4-1 178 | - New upstream release 179 | 180 | * Sun Jun 09 2013 Marcin Dulak - 0.2-1 181 | - RHEL and openSUSE versions based on Christopher Meng's spec 182 | 183 | * Tue Jun 04 2013 Christopher Meng - 0.2-1 184 | - New upstream release. 185 | 186 | * Thu May 09 2013 Christopher Meng - 0.1.3-1 187 | - Initial Package. 188 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # dhcpy6d - DHCPv6 server 4 | # Copyright (C) 2012-2024 Henri Wahl 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the 18 | # 19 | # Free Software Foundation 20 | # 51 Franklin Street, Fifth Floor 21 | # Boston, MA 02110-1301 22 | # USA 23 | 24 | import os 25 | import os.path 26 | from setuptools import setup, find_packages 27 | import shutil 28 | 29 | # workaround to get dhcpy6d-startscript created 30 | try: 31 | if not os.path.exists('sbin'): 32 | os.mkdir('sbin') 33 | shutil.copyfile('main.py', 'sbin/dhcpy6d') 34 | os.chmod('sbin/dhcpy6d', 0o554) 35 | except: 36 | print('could not copy main.py to sbin/dhcpy6d') 37 | 38 | classifiers = [ 39 | 'Intended Audience :: System Administrators', 40 | 'Development Status :: 5 - Production/Stable', 41 | 'License :: OSI Approved :: GNU General Public License (GPL)', 42 | 'Operating System :: POSIX :: Linux', 43 | 'Operating System :: POSIX', 44 | 'Natural Language :: English', 45 | 'Programming Language :: Python', 46 | 'Topic :: System :: Networking' 47 | ] 48 | 49 | data_files = [('/var/lib/dhcpy6d', ['var/lib/volatile.sqlite']), 50 | ('/var/log', ['var/log/dhcpy6d.log']), 51 | ('/usr/share/doc/dhcpy6d', ['doc/clients-example.conf', 52 | 'doc/config.sql', 53 | 'doc/dhcpy6d-example.conf', 54 | 'doc/dhcpy6d-minimal.conf', 55 | 'doc/LICENSE', 56 | 'doc/volatile.sql', 57 | 'doc/volatile.postgresql']), 58 | ('/usr/share/man/man5', ['man/man5/dhcpy6d.conf.5', 59 | 'man/man5/dhcpy6d-clients.conf.5']), 60 | ('/usr/share/man/man8', ['man/man8/dhcpy6d.8']), 61 | ('/etc', ['etc/dhcpy6d.conf']), 62 | ('/usr/sbin', ['sbin/dhcpy6d']), 63 | ] 64 | 65 | setup(name='dhcpy6d', 66 | version='1.6.0', 67 | license='GNU GPL v2', 68 | description='DHCPv6 server daemon', 69 | long_description='Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack transition, addresses may be generated randomly, by range, by DNS, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically.', 70 | author='Henri Wahl', 71 | author_email='henri@dhcpy6d.de', 72 | url='https://dhcpy6d.de/', 73 | download_url='https://dhcpy6d.de/download', 74 | requires=['distro', 'dnspython'], 75 | packages=find_packages(), 76 | classifiers=classifiers, 77 | data_files=data_files 78 | ) 79 | -------------------------------------------------------------------------------- /var/lib/volatile.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenriWahl/dhcpy6d/684dce38b90c620f2d981a9ad23729b2fb7d08d8/var/lib/volatile.sqlite -------------------------------------------------------------------------------- /var/log/dhcpy6d.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenriWahl/dhcpy6d/684dce38b90c620f2d981a9ad23729b2fb7d08d8/var/log/dhcpy6d.log --------------------------------------------------------------------------------