pax_global_header00006660000000000000000000000064143747236100014521gustar00rootroot0000000000000052 comment=8471c006dd50ddcf30dc7a7934bc1115b7923937 dhcpy6d-1.2.3/000077500000000000000000000000001437472361000130655ustar00rootroot00000000000000dhcpy6d-1.2.3/.github/000077500000000000000000000000001437472361000144255ustar00rootroot00000000000000dhcpy6d-1.2.3/.github/workflows/000077500000000000000000000000001437472361000164625ustar00rootroot00000000000000dhcpy6d-1.2.3/.github/workflows/build-release-latest.yml000066400000000000000000000135561437472361000232260ustar00rootroot00000000000000name: build-release-latest on: push: tags-ignore: 'v*' branches: '**' env: release: stable repo_dir: dhcpy6d-jekyll/docs/repo jobs: build-debian: runs-on: ubuntu-latest env: dist: debian steps: # get source - uses: actions/checkout@v2 # build container image for package creation - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/*.sh # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ build-${{ github.job }} # upload results - uses: actions/upload-artifact@v2 with: path: ./*.deb retention-days: 1 build-centos: runs-on: ubuntu-latest env: dist: centos steps: # get source - uses: actions/checkout@v2 # build container image for package creation - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/*.sh # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ build-${{ github.job }} # upload results - uses: actions/upload-artifact@v2 with: path: ./*.rpm retention-days: 1 repo-debian: runs-on: ubuntu-latest needs: [build-debian] env: dist: debian steps: - uses: actions/checkout@v2 # get binaries created by other jobs - uses: actions/download-artifact@v2 # build container image for repo packaging, using the same as for building - run: /usr/bin/docker build -t ${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/entrypoint-*.sh # get secret signing key - run: echo "${{ secrets.DHCPY6D_SIGNING_KEY }}" > signing_key.asc # organize SSH deploy key for dhcp6d-jekyll repo - run: mkdir ~/.ssh - run: echo "${{ secrets.DHCPY6D_REPO_SSH_KEY }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare dhcpy6d-jekyll - run: git clone git@github.com:HenriWahl/dhcpy6d-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ --env RELEASE=${{ env.release }} \ ${{ github.job }} # commit and push new binaries to dhcpyd-jekyll - run: git config --global user.email "repo@dhcpy6d.de" && git config --global user.name "Dhcpy6d Repository" - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.dist }}" && git push repo-centos: runs-on: ubuntu-latest # has to wait for repo-debian to avoid parallel processing of git repo dhcpy6d-jekyll needs: [build-centos, repo-debian] env: dist: centos steps: - uses: actions/checkout@v2 # get binaries created by other jobs - uses: actions/download-artifact@v2 # build container image for repo packaging, using the same as for building - run: /usr/bin/docker build -t ${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/entrypoint-*.sh # get secret signing key - run: echo "${{ secrets.DHCPY6D_SIGNING_KEY }}" > signing_key.asc # organize SSH deploy key for dhcp6d-jekyll repo - run: mkdir ~/.ssh - run: echo "${{ secrets.DHCPY6D_REPO_SSH_KEY }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare dhcpy6d-jekyll - run: git clone git@github.com:HenriWahl/dhcpy6d-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ --env RELEASE=${{ env.release }} \ ${{ github.job }} # commit and push new binaries to dhcpyd-jekyll - run: git config --global user.email "repo@dhcpy6d.de" && git config --global user.name "Dhcpy6d Repository" - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.dist }}" && git push github-release: runs-on: ubuntu-latest needs: [build-debian, build-centos] steps: - uses: actions/download-artifact@v2 - run: cd artifact && md5sum *dhcpy6d* > md5sums.txt - run: cd artifact && sha256sum *dhcpy6d* > sha256sums.txt - uses: marvinpinto/action-automatic-releases@latest with: repo_token: "${{ secrets.GITHUB_TOKEN }}" automatic_release_tag: "latest" prerelease: true files: | artifact/* dhcpy6d-1.2.3/.github/workflows/build-release-stable.yml000066400000000000000000000134521437472361000231770ustar00rootroot00000000000000name: build-release-stable on: push: tags: 'v*' env: release: stable repo_dir: dhcpy6d-jekyll/docs/repo jobs: build-debian: runs-on: ubuntu-latest env: dist: debian steps: # get source - uses: actions/checkout@v2 # build container image for package creation - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/*.sh # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ build-${{ github.job }} # upload results - uses: actions/upload-artifact@v2 with: path: ./*.deb retention-days: 1 build-centos: runs-on: ubuntu-latest env: dist: centos steps: # get source - uses: actions/checkout@v2 # build container image for package creation - run: /usr/bin/docker build -t build-${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/*.sh # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ build-${{ github.job }} # upload results - uses: actions/upload-artifact@v2 with: path: ./*.rpm retention-days: 1 repo-debian: runs-on: ubuntu-latest needs: [build-debian] env: dist: debian steps: - uses: actions/checkout@v2 # get binaries created by other jobs - uses: actions/download-artifact@v2 # build container image for repo packaging, using the same as for building - run: /usr/bin/docker build -t ${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/entrypoint-*.sh # get secret signing key - run: echo "${{ secrets.DHCPY6D_SIGNING_KEY }}" > signing_key.asc # organize SSH deploy key for dhcp6d-jekyll repo - run: mkdir ~/.ssh - run: echo "${{ secrets.DHCPY6D_REPO_SSH_KEY }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare dhcpy6d-jekyll - run: git clone git@github.com:HenriWahl/dhcpy6d-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ --env RELEASE=${{ env.release }} \ ${{ github.job }} # commit and push new binaries to dhcpyd-jekyll - run: git config --global user.email "repo@dhcpy6d.de" && git config --global user.name "Dhcpy6d Repository" - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.dist }}" && git push repo-centos: runs-on: ubuntu-latest # has to wait for repo-debian to avoid parallel processing of git repo dhcpy6d-jekyll needs: [build-centos, repo-debian] env: dist: centos steps: - uses: actions/checkout@v2 # get binaries created by other jobs - uses: actions/download-artifact@v2 # build container image for repo packaging, using the same as for building - run: /usr/bin/docker build -t ${{ github.job }} -f build/Dockerfile-${{ env.dist }} . # make entrypoints executable - run: chmod +x build/entrypoint-*.sh # get secret signing key - run: echo "${{ secrets.DHCPY6D_SIGNING_KEY }}" > signing_key.asc # organize SSH deploy key for dhcp6d-jekyll repo - run: mkdir ~/.ssh - run: echo "${{ secrets.DHCPY6D_REPO_SSH_KEY }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare dhcpy6d-jekyll - run: git clone git@github.com:HenriWahl/dhcpy6d-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.release }}/${{ env.dist }} # execute container with matching entrypoint - run: | /usr/bin/docker run --volume ${{ github.workspace }}:/dhcpy6d \ --volume ${{ github.workspace }}/build/entrypoint-${{ github.job }}.sh:/entrypoint.sh \ --entrypoint /entrypoint.sh \ --env RELEASE=${{ env.release }} \ ${{ github.job }} # commit and push new binaries to dhcpyd-jekyll - run: git config --global user.email "repo@dhcpy6d.de" && git config --global user.name "Dhcpy6d Repository" - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.dist }}" && git push github-release: runs-on: ubuntu-latest needs: [build-debian, build-centos] steps: - uses: actions/download-artifact@v2 - run: cd artifact && md5sum *dhcpy6d* > md5sums.txt - run: cd artifact && sha256sum *dhcpy6d* > sha256sums.txt - uses: marvinpinto/action-automatic-releases@latest with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: true files: | artifact/* dhcpy6d-1.2.3/.gitignore000066400000000000000000000000451437472361000150540ustar00rootroot00000000000000*.conf *.pyc *.yaml *.db *.log .idea dhcpy6d-1.2.3/Changelog000066400000000000000000000073661437472361000147130ustar00rootroot00000000000000Changelog for dhcpy6d 2022-06-14 1.2.2 fixed class interface parsing 2022-05-10 1.2.1 fixed option 23 2022-04-04 1.2.0 new option to exclude interface fixed dynamic prefix injection fixed volatile.sqlite update trouble fixed Debian build dependencies fixed documentation fixed reuse lease 2021-11-21 1.0.9 fixed overwrite of SQLite DB when upgrading 2021-10-30 1.0.8 fixed acceptance of empty addresses in client requests 2021-10-01 1.0.7 fixed non-existing UserClass 2021-09-30 1.0.6 fixed empty client config file fixed DB updates 2021-08-11 1.0.5 fixed inability to use multiple MACs per host in DB 2021-08-10 1.0.4 fixed default behavior (route_link_local=no) in clients.conf 2020-11-20 1.0.3 added option DNS_USE_RNDC 2020-10-08 1.0.2 fixed NTP_SERVER_DICT 2020-07-24 1.0.1 fix mandatory logfile 2020-04-03 1.0 added EUI64 address category added PXE boot support added support for fixed prefix per client config added address category dns to retrieve client ipv6 from DNS added self-creation of database tables improved PostgreSQL support migrated to Python 3 code housekeeping fixes of course 2018-10-25 0.7.3 added ignore_mac option to work with ppp interfaces 2018-06-15 0.7.2 fix for MySQLdb.IntegrityError 2018-06-11 0.7.1 fixed recycling of prefixes 2018-04-30 0.7 added ntp_server option added request limits allow one to inject prefix - e.g. changed prefix from ISP optimized time requests ignore unknown clients fixes for prefix delegation 2017-09-29 0.6 Prefix delegation (PD) fixes 2017-05-29 0.5 Allow using PostgreSQL database for volatile and config storage Added category 'dns' for DNS-based IP-address retrieval Reply CONFIRM requests with NotOnLink to force clients to get new address Added --prefix option to be used for dynamic prefixes Systemd integration 2016-01-05 0.4.3 autocommit to MySQL fixed fixed addresses some optimization in tidy-up-thread small fixes 2015-08-18 0.4.2 fixed usage of fixed addresses in dhcpy6d-clients.conf fixed dns_update() to update default class clients too show warning if deprecated prefix_length is used in address definitions set socket to non-blocking to avoid freeze increase MAC/LLIP cache time from 30s to 300s because of laggy clients removed useless prefix length retry query on MySQL reconnect bugfix 2015-03-17 0.4.1 listen on VLAN interfaces now really works some code cleaning 2014-10-22 0.4 listen on VLAN interfaces access neighbor cache natively on Linux allows empty answers do not cache MAC/LLIP addresses longterm as default ability to generate server DUID complete manpages more complete configuration correctness checks fixed single address definition error 2013-07-29 0.3 added ability to run as non-privileged user 2013-05-31 0.2 Attention: leases database scheme changed. Possibly leases database has to be recreated! 'range' address lease management got more robust logging output changed 2013-05-23 0.1.5 fixed race condition bug in category 'range' address lease storage 2013-05-18 0.1.4.1 fixed lease storage bug 2013-05-17 0.1.4 fixed race condition bug with already advertised addresses 2013-05-07 0.1.3 added RFC 3646 compliant domain search list option 24 reuse addresses of category "range" in a sensible way fixed bug with case sensitive textfile client config options 2013-03-19 0.1.2 fixed multiple addresses renew bug 2013-01-15 0.1.1 reverted to Handler.finish() method to prevent empty extra answer packets 2013-01-11 0.1 initial stable release dhcpy6d-1.2.3/Dockerfile000066400000000000000000000006151437472361000150610ustar00rootroot00000000000000FROM python:3.9 LABEL maintainer=henri@dhcpy6d.de ARG HTTP_PROXY="" ENV HTTPS_PROXY $HTTP_PROXY ENV http_proxy $HTTP_PROXY ENV https_proxy $HTTP_PROXY RUN pip install distro \ dnspython \ mysqlclient \ psycopg2 RUN useradd --system --user-group dhcpy6d WORKDIR /dhcpy6d CMD python3 main.py --config dhcpy6d.conf --user dhcpy6d --group dhcpy6d dhcpy6d-1.2.3/LICENSE000066400000000000000000000431031437472361000140730ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. dhcpy6d-1.2.3/MANIFEST.in000066400000000000000000000010231437472361000146170ustar00rootroot00000000000000# necessary because of buggy distutils include Changelog include dhcpy6d include doc/LICENSE include doc/clients-example.conf include doc/config.sql include doc/dhcpy6d-example.conf include doc/dhcpy6d-minimal.conf include doc/volatile.sql include doc/volatile.postgresql include man/man5/dhcpy6d.conf.5 include man/man5/dhcpy6d-clients.conf.5 include man/man8/dhcpy6d.8 include var/lib/volatile.sqlite include var/log/dhcpy6d.log include etc/dhcpy6d.conf include etc/logrotate.d/dhcpy6d include lib/systemd/system/dhcpy6d.service dhcpy6d-1.2.3/README.md000066400000000000000000000013721437472361000143470ustar00rootroot00000000000000dhcpy6d ======= 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. Addresses may be generated randomly, by range, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically Range-based prefixes be delegated as well as fixed prefixes per client. Changing prefixes from ISP can be applied dynamically. Supported platforms include Linux, OpenBSD, FreeBSD, NetBSD and macOS. At any other POSIX OS it might work too. Homepage: https://dhcpy6d.de/ Documentation: https://dhcpy6d.de/documentation Container Image: https://hub.docker.com/r/henriwahl/dhcpy6d dhcpy6d-1.2.3/build.sh000077500000000000000000000045061437472361000145300ustar00rootroot00000000000000#!/bin/bash # # # simple build script for dhcpy6d # # OS=unknown function get_os() { if [ -f /etc/debian_version ]; then OS=debian elif [ -f /etc/redhat-release ]; then yum install -y sudo which OS=redhat fi } function create_manpages() { if ! which rst2man; then if [ "$OS" == "debian" ]; then sudo apt -y install python3-docutils fi if [ "$OS" == "redhat" ]; then sudo yum -y install python3-docutils fi fi echo "Creating manpages from RST files" rst2man doc/dhcpy6d.rst man/man8/dhcpy6d.8 rst2man doc/dhcpy6d.conf.rst man/man5/dhcpy6d.conf.5 rst2man doc/dhcpy6d-clients.conf.rst man/man5/dhcpy6d-clients.conf.5 } # find out where script runs at get_os if [ "$OS" == "debian" ]; then echo "Building .deb package" create_manpages # install missing packages if ! which debuild; then sudo apt -y install build-essential devscripts dh-python dh-systemd python3-setuptools fi if [ ! -d /usr/share/doc/python3-all ]; then sudo apt -y install python3-all fi debuild --no-tgz-check -- clean debuild --no-tgz-check -- binary-indep elif [ "$OS" == "redhat" ]; then echo "Building .rpm package" create_manpages # install missing packages if ! which rpmbuild; then sudo yum -y install python3-devel python3-setuptools rpm-build fi TOPDIR=$HOME/dhcpy6d.$$ SPEC=redhat/dhcpy6d.spec # create source folder for rpmbuild mkdir -p $TOPDIR/SOURCES # init needed in TOPDIR/SOURCES cp -pf lib/systemd/system/dhcpy6d.service $TOPDIR/SOURCES/dhcpy6d # use setup.py sdist build output to get package name FILE=$(python3 setup.py sdist --dist-dir $TOPDIR/SOURCES | grep "creating dhcpy6d-" | head -n1 | cut -d" " -f2) echo Source file: $FILE.tar.gz # version VERSION=$(echo $FILE | cut -d"-" -f 2) # replace version in the spec file sed -i "s|Version:.*|Version: $VERSION|" $SPEC # workaround for less changes, but achieve build with new GitHub source # TDDO: clean up that build process cp ${TOPDIR}/SOURCES/${FILE}.tar.gz ${TOPDIR}/SOURCES/v${VERSION}.tar.gz # finally build binary rpm rpmbuild -bb --define "_topdir $TOPDIR" $SPEC echo $TOPDIR # get rpm file cp -f $(find $TOPDIR/RPMS -name "$FILE-?.*noarch.rpm") . # clean #rm -rf $TOPDIR else echo "Package creation is only supported on Debian and RedHat derivatives." fi dhcpy6d-1.2.3/build/000077500000000000000000000000001437472361000141645ustar00rootroot00000000000000dhcpy6d-1.2.3/build/Dockerfile-centos000066400000000000000000000006401437472361000174470ustar00rootroot00000000000000FROM rockylinux:8 LABEL maintainer=henri@dhcpy6d.de # get build requirements RUN yum -y install createrepo \ git \ gpg \ python3-devel \ python3-docutils \ rpm-build \ rpm-sign \ sudo \ which # flexible entrypoint, mounted as volume ENTRYPOINT ["/entrypoint.sh"]dhcpy6d-1.2.3/build/Dockerfile-debian000066400000000000000000000011601437472361000173740ustar00rootroot00000000000000FROM debian:10 LABEL maintainer=henri@dhcpy6d.de # get build requirements RUN apt -y update RUN apt -y install apt-utils \ build-essential \ dpkg-dev \ devscripts \ dh-python \ git \ gpg \ python3-all \ python3-distro \ python3-distutils \ python3-docutils \ python3-lib2to3 \ python3-setuptools \ sudo # flexible entrypoint, mounted as volume ENTRYPOINT ["/entrypoint.sh"] dhcpy6d-1.2.3/build/entrypoint-build-centos.sh000066400000000000000000000002071437472361000213200ustar00rootroot00000000000000#!/bin/bash # # flexible entrypoint, mounted as volume # set -e # got to working directory cd /dhcpy6d # run build script ./build.sh dhcpy6d-1.2.3/build/entrypoint-build-debian.sh000066400000000000000000000003251437472361000212500ustar00rootroot00000000000000#!/bin/bash # # flexible entrypoint, mounted as volume # set -e # got to working directory cd /dhcpy6d # run build script ./build.sh # copy resulting Debian package back into working directory cp /*.deb /dhcpy6ddhcpy6d-1.2.3/build/entrypoint-repo-centos.sh000066400000000000000000000013371437472361000211730ustar00rootroot00000000000000#!/bin/bash # # create repo package files and sign them # set -e # go to working directory volume cd /dhcpy6d # import signing key, stored from GitHub secrets in workflow gpg --import signing_key.asc # put package to its later place cp -r artifact/*.rpm dhcpy6d-jekyll/docs/repo/${RELEASE}/centos # RELEASE is a runtime --env argument to make it easier to provide stable and latest reo cd dhcpy6d-jekyll/docs/repo/${RELEASE}/centos # create repo files + sign package gpg --output RPM-GPG-KEY-dhcpy6d --armor --export cp RPM-GPG-KEY-dhcpy6d /etc/pki/rpm-gpg rpm --import RPM-GPG-KEY-dhcpy6d echo "%_signature gpg" > ~/.rpmmacros echo "%_gpg_name dhcpy6d" >> ~/.rpmmacros rpm --resign *.rpm createrepo --update . rm -rf .rpmmacros dhcpy6d-1.2.3/build/entrypoint-repo-debian.sh000066400000000000000000000012331437472361000211150ustar00rootroot00000000000000#!/bin/bash # # create repo package files and sign them # set -e # go to working directory volume cd /dhcpy6d # import signing key, stored from GitHub secrets in workflow gpg --import signing_key.asc # put package to its later place cp -r artifact/*.deb dhcpy6d-jekyll/docs/repo/${RELEASE}/debian # RELEASE is a runtime --env argument to make it easier to provide stable and latest reo cd dhcpy6d-jekyll/docs/repo/${RELEASE}/debian # create repo files dpkg-scanpackages . > Packages gzip -k -f Packages apt-ftparchive release . > Release # sign package gpg -abs -o Release.gpg Release gpg --clearsign -o InRelease Release gpg --output key.gpg --armor --export dhcpy6d-1.2.3/debian/000077500000000000000000000000001437472361000143075ustar00rootroot00000000000000dhcpy6d-1.2.3/debian/changelog000066400000000000000000000315541437472361000161710ustar00rootroot00000000000000dhcpy6d (1.2.2-1) stable; urgency=medium * New upstream release + fixed class interface parsing -- Henri Wahl Tue, 14 Jun 2022 21:00:00 +0200 dhcpy6d (1.2.1-1) stable; urgency=medium * New upstream release + fixed option 23 -- Henri Wahl Mon, 10 May 2022 21:00:00 +0200 dhcpy6d (1.2.0-1) stable; urgency=medium * New upstream release + new option to exclude interface + fixed dynamic prefix injection + fixed volatile.sqlite update trouble + fixed Debian build dependencies + fixed documentation + fixed reuse lease -- Henri Wahl Mon, 04 Apr 2022 21:00:00 +0200 dhcpy6d (1.0.9-1) stable; urgency=medium * New upstream release + fixed overwrite of SQLite DB when upgrading -- Henri Wahl Mon, 01 Nov 2021 20:00:00 +0200 dhcpy6d (1.0.8-1) stable; urgency=medium * New upstream release + fixed acceptance of empty addresses in client requests -- Henri Wahl Sat, 30 Oct 2021 20:00:00 +0200 dhcpy6d (1.0.7-1) stable; urgency=medium * New upstream release + fixed non-existing UserClass -- Henri Wahl Fri, 01 Oct 2021 08:00:00 +0200 dhcpy6d (1.0.6-1) stable; urgency=medium * New upstream release + fixed empty client config file + fixed DB updates -- Henri Wahl Thu, 30 Sep 2021 21:00:00 +0200 dhcpy6d (1.0.5-1) stable; urgency=medium * New upstream release + fixed inability to use multiple MACs per host in DB -- Henri Wahl Wed, 11 Aug 2021 12:00:00 +0200 dhcpy6d (1.0.4-1) stable; urgency=medium * New upstream release + fixed default behavior (route_link_local=no) in clients.conf -- Henri Wahl Tue, 10 Aug 2021 12:00:00 +0200 dhcpy6d (1.0.3-1) stable; urgency=medium * New upstream release + added option DNS_USE_RNDC -- Henri Wahl Mon, 21 Dec 2020 17:30:00 +0200 dhcpy6d (1.0.2-1) stable; urgency=medium * New upstream release + fixed NTP_SERVER_DICT -- Henri Wahl Thu, 08 Oct 2020 07:30:00 +0200 dhcpy6d (1.0.1-1) stable; urgency=medium * New upstream release + fix mandatory logfile -- Henri Wahl Fri, 24 Jul 2020 14:30:00 +0200 dhcpy6d (1.0-1) stable; urgency=medium * New upstream release + added EUI64 address category + added PXE boot support + added support for fixed prefix per client config + added address category dns to retrieve client ipv6 from DNS + added self-creation of database tables + improved PostgreSQL support + migrated to Python 3 + code housekeeping + fixes of course -- Henri Wahl Fri, 03 Apr 2020 14:30:00 +0200 dhcpy6d (0.7.3-1) stable; urgency=medium * New upstream release + added ignore_mac option to work with ppp interfaces -- Henri Wahl Thu, 25 Oct 2018 14:30:00 +0200 dhcpy6d (0.7.2.-1) stable; urgency=medium * New upstream release + fix for MySQLdb.IntegrityError -- Henri Wahl Fri, 15 Jun 2018 7:30:00 +0200 dhcpy6d (0.7.1-1) stable; urgency=medium * New upstream release + fixed recycling of prefixes -- Henri Wahl Mon, 11 Jun 2018 10:30:00 +0200 dhcpy6d (0.7-1) stable; urgency=medium * New upstream release + added ntp_server option + added request limits + allow one to inject prefix - e.g. changed prefix from ISP + optimized time requests + ignore unknown clients + fixes for prefix delegation -- Henri Wahl Mon, 30 Apr 2018 20:30:00 +0200 dhcpy6d (0.6-1) stable; urgency=medium * New upstream release + prefix delegation + fixes -- Henri Wahl Fri, 15 Sep 2017 11:30:00 +0200 dhcpy6d (0.5-1) unstable; urgency=medium * New upstream release + Reply CONFIRM requests with NotOnLink to force clients to get new address + Added --prefix option to be used for dynamic prefixes + Allow using PostgreSQL database for volatile and config storage + Added category 'dns' for DNS-based IP-address retrieval + Systemd integration -- Henri Wahl Mon, 29 May 2017 10:00:00 +0200 dhcpy6d (0.4.3-1) unstable; urgency=medium * New upstream release + Added autocommit to MySQL + Fixed fixed addresses + Some optimization in tidy-up-thread + Small fixes -- Henri Wahl Sat, 26 Dec 2015 21:30:00 +0200 dhcpy6d (0.4.3~dev1-1) unstable; urgency=medium [ Henri Wahl ] * New upstream snapshot + removed client FQDN in log file [ Axel Beckert ] * Merge adduser and usermod calls in debian/dhcpy6d.postinst. Fixes false positive lintian warning maintainer-script-should-not-use-adduser-system-without-home. * Bump debhelper compatibility to 9 as recommended nowadays. + Update versioned debhelper build-dependency accordingly. -- Henri Wahl Fri, 21 Aug 2015 12:30:00 +0200 dhcpy6d (0.4.2-1) unstable; urgency=medium * New upstream snapshot + fixed usage of fixed addresses in dhcpy6d-clients.conf + fixed dns_update() to update default class clients too + show warning if deprecated prefix_length is used in address definitions + set socket to non-blocking to avoid freeze + increase MAC/LLIP cache time from 30s to 300s because of laggy clients + removed useless prefix length + retry query on MySQL reconnect bugfix -- Henri Wahl Tue, 18 Aug 2015 16:00:00 +0200 dhcpy6d (0.4.1-1) unstable; urgency=medium [ Henri Wahl ] * New upstream release + VLAN definitions now really work + several code cleaned + Removes unnecessary executable bits (see #769006) → reinstantiate debian/dhcpy6d.logrotate as symlink [ Axel Beckert ] * Fix postinst script to not expect preinst parameters (Closes: #768974) -- Henri Wahl Tue, 17 Mar 2015 08:50:00 +0100 dhcpy6d (0.4-2) unstable; urgency=medium * Handle /etc/default/dhcpy6d with ucf. (Closes: #767817) + Install file to /usr/share/dhcpy6d/default/dhcpy6d instead, remove symlink debian/dhcpy6d.default, add debian/dhcpy6d.install. + Depend on ucf. * Install volatile.sqlite into /usr/share/dhcpy6d/ and copy it to /var/lib/dhcpy6d/volatile.sqlite during postinst only if it doesn't yet exist. Remove it upon purge. (Closes: #768989) * Both fixes above together also remove unnecessary executable bits. (Else the fix for #767817 newly introduces the lintian warning executable-not-elf-or-script; closes: #769006) * Additionally replace symlink debian/dhcpy6d.logrotate with a patched copy of etc/logrotate.d/dhcpy6d to remove the executable bit also there. (Fixes another facet of #769006) -- Axel Beckert Thu, 13 Nov 2014 12:39:09 +0100 dhcpy6d (0.4-1) unstable; urgency=low [ Henri Wahl ] * New upstream release + new options: log_mac_llip, cache_mac_llip (avoids cache poisoning) [ Axel Beckert ] * Add get-orig-source target to debian/rules for easier snapshot packaging. * Depend on ${python:Depends} instead of a hardcoded python (>= 2.6). * Add "Pre-Depends: dpkg (>= 1.16.5) for "start-stop-daemon --no-close" * Drop dependency on iproute/iproute2 as /sbin/ip is no more used. -- Axel Beckert Wed, 22 Oct 2014 21:03:56 +0200 dhcpy6d (0.3.99+git2014.09.18-1) unstable; urgency=medium * New upstream release candidate + snapshot + allow VLAN interface definitions + check if used interfaces exist + improved usability with more clear mesages if there are configuration errors + full man pages dhcpy6d.8 and dhcpy6d.conf.5 added + added command line argument --generate-duid for DUID generation at setup [ Henri Wahl ] * Append generated DUID to /etc/default/dhcpy6d if not yet present * Added command line arguments --really-do-it and --duid to be configured in /etc/defaults/dhcpy6d [ Axel Beckert ] * Switch section from "utils" to "net" like most other DHCP servers. * Update debian/source/options to follow upstream directory name changes * Bump Standards-Version to 3.9.6 (no changes) -- Axel Beckert Thu, 02 Oct 2014 18:25:44 +0200 dhcpy6d (0.3+git2014.07.23-1) unstable; urgency=medium * New upstream snapshot. + Man pages moved from Debian to Upstream + Don't ship man pages installed to /usr/share/doc/dhcpy6d/ * Delete dhcpy6d's log files upon package purge. * Add missing dependency on iproute2 or iproute. Thanks Henri! * Complete the switch from now deprecated python-support to dh_python2. + Only debian/control changes. (debian/rules was fine already.) + Fixes lintian warning build-depends-on-obsolete-package. -- Axel Beckert Thu, 24 Jul 2014 14:27:31 +0200 dhcpy6d (0.3+git2014.03.21-1) unstable; urgency=low * New upstream snapshot * First upload to Debian (Closes: #715010) * Switch back to non-native packaging * Set myself as primary package maintainer * Switch to source format "3.0 (quilt)". + Remove now obsolete README.source * Drop unnecessary build-dependency on quilt. Fixes lintian warning quilt-build-dep-but-no-series-file. * Add machine-readable debian/copyright. Fixes lintian warning no-debian-copyright. * Add "set -e" to postinst script to bail out on any error. * Move adduser from Suggests to Depends. Used in the postinst script. * Don't ship additional LICENSE file installed by upstream. Fixes lintian warning extra-license-file. * Add a debian/watch file. Fixes lintian warning debian-watch-file-is-missing. * Use short description from GitHub as short description. * Don't ship empty log file, create it at install time. Fixes lintian warning file-in-unusual-dir. * Add minimal man page with pointer to online documentation. Fixes lintian warning binary-without-manpage. * Also fix the following lintian warnings: + maintainer-address-malformed + maintainer-also-in-uploaders + no-standards-version-field + maintainer-script-lacks-debhelper-token + debhelper-but-no-misc-depends + description-starts-with-package-name + description-synopsis-might-not-be-phrased-properly + description-too-long (refers to first line) + extended-description-is-empty * Apply wrap-and-sort -- Axel Beckert Wed, 21 May 2014 14:25:27 +0200 dhcpy6d (0.3) unstable; urgency=low * New upstream - running as non-root user/group dhcpy6d - deb improvements - rpm improvements -- Henri Wahl Mon, 29 Jul 2013 13:14:00 +0200 dhcpy6d (0.2-1) unstable; urgency=low * New upstream - next fix in 'range' lease storage, getting more robust - better logging -- Henri Wahl Fri, 31 May 2013 14:40:00 +0200 dhcpy6d (0.1.5-1) unstable; urgency=low * New upstream - fixed race condition in 'range' lease storage -- Henri Wahl Thu, 23 May 2013 11:00:00 +0200 dhcpy6d (0.1.4.1-1) unstable; urgency=low * New upstream - fixed lease storage bug -- Henri Wahl Sat, 18 May 2013 00:50:00 +0200 dhcpy6d (0.1.4-1) unstable; urgency=low * New upstream - fixed advertised address handling for categories 'range' and 'random' -- Henri Wahl Fri, 17 May 2013 14:50:00 +0200 dhcpy6d (0.1.3-1) unstable; urgency=low * New upstream - added domain_search_list option - fixed case-sensitive MAC address config -- Henri Wahl Mon, 06 May 2013 14:50:00 +0200 dhcpy6d (0.1.2-1) unstable; urgency=low * New upstream - fixed multiple addresses renew bug -- Henri Wahl Tue, 19 Mar 2013 9:02:00 +0200 dhcpy6d (0.1.1-1) unstable; urgency=low * New upstream - reverted to Handler.finish() -- Henri Wahl Tue, 15 Jan 2013 07:35:00 +0200 dhcpy6d (0.1-1) unstable; urgency=low * New upstream - inital stable release -- Henri Wahl Wed, 11 Jan 2013 14:10:00 +0200 dhcpy6d (20130111-1) unstable; urgency=low * New upstream - more polishing for rpm packaging support -- Henri Wahl Wed, 11 Jan 2013 13:18:00 +0200 dhcpy6d (20130109-1) unstable; urgency=low * New upstream - polishing packaging support -- Henri Wahl Wed, 09 Jan 2013 14:16:00 +0200 dhcpy6d (20121221-1) unstable; urgency=low * New upstream - finished Debian support -- Henri Wahl Thu, 21 Dec 2012 11:25:00 +0200 dhcpy6d (20121220-1) unstable; urgency=low * New upstream - testing Debian support -- Henri Wahl Thu, 20 Dec 2012 11:25:00 +0200 dhcpy6d (20121219-1) unstable; urgency=low * New upstream - testing Debian support -- Henri Wahl Wed, 19 Dec 2012 11:25:00 +0200 dhcpy6d-1.2.3/debian/compat000066400000000000000000000000021437472361000155050ustar00rootroot000000000000009 dhcpy6d-1.2.3/debian/control000066400000000000000000000021641437472361000157150ustar00rootroot00000000000000Source: dhcpy6d Section: net X-Python-Version: >= 3.7 X-Python3-Version: >= 3.7 Priority: optional Maintainer: Axel Beckert Build-Depends: debhelper (>= 12.1.1~), python3-all (>= 3.7.3-1~) Build-Depends-Indep: dh-python Homepage: https://dhcpy6d.de Vcs-Git: git://github.com/HenriWahl/dhcpy6d.git Vcs-Browser: https://github.com/HenriWahl/dhcpy6d Standards-Version: 4.2.1 Package: dhcpy6d Architecture: all Depends: adduser, lsb-base, python3-distro, python3-dnspython, ${misc:Depends}, ${python3:Depends}, ucf Pre-Depends: dpkg (>= 1.19.7) Suggests: python3-mysqldb, python3-psycopg2 Description: MAC address aware DHCPv6 server written in Python Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack transition, addresses may be generated randomly, by range, by arbitrary ID or MAC address. Clients can get more than one address, leases and client configuration can be stored in databases and DNS can be updated dynamically. dhcpy6d-1.2.3/debian/copyright000066400000000000000000000022431437472361000162430ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: dhcpy6d Source: https://dhcpy6d.de/ Files: * Copyright: 2012-2022 Henri Wahl License: GPL-2+ Files: debian/* Copyright: 2012-2022 Henri Wahl 2014 Axel Beckert License: GPL-2+ License: GPL-2+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA . On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. dhcpy6d-1.2.3/debian/dhcpy6d.dirs000066400000000000000000000000751437472361000165350ustar00rootroot00000000000000usr/share/dhcpy6d/ usr/share/dhcpy6d/default var/lib var/log dhcpy6d-1.2.3/debian/dhcpy6d.init000066400000000000000000000044321437472361000165400ustar00rootroot00000000000000#!/bin/sh ### BEGIN INIT INFO # Provides: dhcpy6d # Required-Start: $syslog $network $remote_fs # Required-Stop: $syslog $remote_fs # Should-Start: $local_fs # Should-Stop: $local_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start/Stop dhcpy6d DHCPv6 server # Description: (empty) ### END INIT INFO set -e PATH=/sbin:/bin:/usr/sbin:/usr/bin DHCPY6DBIN=/usr/sbin/dhcpy6d DHCPY6DCONF=/etc/dhcpy6d.conf DHCPY6DPID=/var/run/dhcpy6d.pid NAME="dhcpy6d" DESC="dhcpy6d DHCPv6 server" USER=dhcpy6d GROUP=dhcpy6d RUN=no DEFAULTFILE=/etc/default/dhcpy6d if [ -f $DEFAULTFILE ]; then . $DEFAULTFILE fi . /lib/lsb/init-functions check_status() { if [ ! -r "$DHCPY6DPID" ]; then test "$1" != -v || echo "$NAME is not running." return 3 fi if read pid < "$DHCPY6DPID" && ps -p "$pid" > /dev/null 2>&1; then test "$1" != -v || echo "$NAME is running." return 0 else test "$1" != -v || echo "$NAME is not running but $DHCPY6DPID exists." return 1 fi } test -x $DHCPY6DBIN || exit 0 case "$1" in start) if [ "$RUN" = "no" ]; then echo "dhcpy6d is disabled in /etc/default/dhcpy6d. Set RUN=yes to get it running." exit 0 fi log_daemon_msg "Starting $DESC $NAME" if ! check_status; then start-stop-daemon --start --make-pidfile --pidfile ${DHCPY6DPID} \ --background --oknodo --no-close --exec $DHCPY6DBIN -- --config $DHCPY6DCONF \ --user $USER \ --group $GROUP \ --duid $DUID \ --really-do-it $RUN sleep 2 if check_status -q; then log_end_msg 0 else log_end_msg 1 exit 1 fi else log_end_msg 1 exit 1 fi ;; stop) log_daemon_msg "Stopping $DESC $NAME" start-stop-daemon --stop --quiet --pidfile ${DHCPY6DPID} --oknodo log_end_msg $? rm -f $DHCPY6DPID ;; restart|force-reload) $0 stop sleep 2 $0 start if [ "$?" != "0" ]; then exit 1 fi ;; status) echo "Status of $NAME: " check_status -v exit "$?" ;; *) echo "Usage: $0 (start|stop|restart|force-reload|status)" exit 1 esac exit 0 dhcpy6d-1.2.3/debian/dhcpy6d.install000066400000000000000000000000571437472361000172420ustar00rootroot00000000000000etc/default/dhcpy6d usr/share/dhcpy6d/default/ dhcpy6d-1.2.3/debian/dhcpy6d.logrotate000066400000000000000000000001511437472361000175670ustar00rootroot00000000000000/var/log/dhcpy6d.log { weekly missingok rotate 4 compress notifempty create 660 dhcpy6d dhcpy6d } dhcpy6d-1.2.3/debian/dhcpy6d.postinst000066400000000000000000000046061437472361000174630ustar00rootroot00000000000000#!/bin/sh # # attempting to create lower privileged user/group for dhcpy6d # take from http://www.debian.org/doc/manuals/securing-debian-howto/ch9.en.html#s-bpp-lower-privs # set -e case "$1" in configure) # Sane defaults: [ -z "$SERVER_HOME" ] && SERVER_HOME=/var/lib/dhcpy6d [ -z "$SERVER_USER" ] && SERVER_USER=dhcpy6d [ -z "$SERVER_NAME" ] && SERVER_NAME="DHCPv6 server dhcpy6d" [ -z "$SERVER_GROUP" ] && SERVER_GROUP=dhcpy6d # Groups that the user will be added to, if undefined, then none. ADDGROUP="" # create user to avoid running server as root # 1. create group if not existing if ! getent group | grep -q "^$SERVER_GROUP:" ; then echo -n "Adding group $SERVER_GROUP.." addgroup --quiet --system $SERVER_GROUP 2>/dev/null ||true echo "..done" fi # 2. create homedir if not existing test -d $SERVER_HOME || mkdir $SERVER_HOME # 3. create user if not existing if ! getent passwd | grep -q "^$SERVER_USER:"; then echo -n "Adding system user $SERVER_USER.." adduser --quiet \ --system \ --ingroup $SERVER_GROUP \ --no-create-home \ --home $SERVER_HOME \ --gecos "$SERVER_NAME" \ --disabled-password \ $SERVER_USER 2>/dev/null || true echo "..done" fi # 4. adjust file and directory permissions chown -R $SERVER_USER:$SERVER_GROUP $SERVER_HOME chmod -R 0770 $SERVER_HOME if [ ! -e /var/log/dhcpy6d.log ]; then touch /var/log/dhcpy6d.log fi if [ ! -e /var/lib/dhcpy6d/volatile.sqlite ]; then touch /var/lib/dhcpy6d/volatile.sqlite fi chown $SERVER_USER:$SERVER_GROUP /var/log/dhcpy6d.log /var/lib/dhcpy6d/volatile.sqlite chmod 0660 /var/log/dhcpy6d.log /var/lib/dhcpy6d/volatile.sqlite # 6. add DUID entry to /etc/default/dhcpy6d if not yet existing TMPFILE=`mktemp` cat /usr/share/dhcpy6d/default/dhcpy6d > "${TMPFILE}" echo >> "${TMPFILE}" echo "# LLT DUID generated by Debian" >> "${TMPFILE}" if [ ! -e /etc/default/dhcpy6d ] || ! grep -q "DUID=" /etc/default/dhcpy6d; then echo "DUID=$(dhcpy6d --generate-duid)" >> "${TMPFILE}" else egrep "^DUID=" /etc/default/dhcpy6d >> "${TMPFILE}" fi ucf "${TMPFILE}" /etc/default/dhcpy6d ucfr dhcpy6d /etc/default/dhcpy6d ;; esac #DEBHELPER# dhcpy6d-1.2.3/debian/dhcpy6d.postrm000066400000000000000000000010571437472361000171210ustar00rootroot00000000000000#!/bin/sh # # Delete dhcpy6d's log files upon package purge. # set -e case "$1" in purge) rm -f /var/log/dhcpy6d.log* /var/lib/dhcpy6d/volatile.sqlite # Taken from ucf's postrm example for ext in '' '~' '%' .bak .ucf-new .ucf-old .ucf-dist; do rm -f "/etc/default/dhcpy6d$ext" done if which ucf >/dev/null; then ucf --purge /etc/default/dhcpy6d fi if which ucfr >/dev/null; then ucfr --purge dhcpy6d /etc/default/dhcpy6d fi ;; esac #DEBHELPER# dhcpy6d-1.2.3/debian/dhcpy6d.service000066400000000000000000000006251437472361000172350ustar00rootroot00000000000000[Unit] Description=DHCPv6 Server Daemon Documentation=man:dhcpy6d(8) man:dhcpy6d.conf(5) man:dhcpy6d-clients.conf(5) Wants=network-online.target After=network-online.target After=time-sync.target [Service] EnvironmentFile=/etc/default/dhcpy6d ExecStart=/usr/sbin/dhcpy6d --config /etc/dhcpy6d.conf --user dhcpy6d --group dhcpy6d --really-do-it ${RUN} --duid ${DUID} [Install] WantedBy=multi-user.target dhcpy6d-1.2.3/debian/manpages000066400000000000000000000001131437472361000160200ustar00rootroot00000000000000man/man8/dhcpy6d.8 man/man5/dhcpy6d.conf.5 man/man5/dhcpy6d-clients.conf.5 dhcpy6d-1.2.3/debian/rules000077500000000000000000000017701437472361000153740ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --buildsystem=pybuild --with python3 --with systemd override_dh_auto_install: dh_auto_install -- --install-args="--install-scripts=/usr/sbin --install-layout=deb" rm -f debian/dhcpy6d/usr/share/doc/dhcpy6d/LICENSE rm -f debian/dhcpy6d/var/log/dhcpy6d.log rm -f debian/dhcpy6d/usr/share/doc/dhcpy6d/*.[0-9] find debian/dhcpy6d/ -name __pycache__ -print0 | xargs -0 --no-run-if-empty rm -rv mv -v debian/dhcpy6d/usr/lib/python3.7 debian/dhcpy6d/usr/lib/python3 mv -v debian/dhcpy6d/var/lib/dhcpy6d/volatile.sqlite debian/dhcpy6d/usr/share/dhcpy6d/ override_dh_install: dh_install chmod 0644 debian/dhcpy6d/usr/share/dhcpy6d/default/dhcpy6d override_dh_installsystemd: dh_installsystemd --no-enable --no-start # make -f debian/rules get-orig-source get-orig-source: python setup.py sdist mv -v dist/dhcpy6d-*.tar.gz ../dhcpy6d_`dpkg-parsechangelog -SVersion | cut -d- -f1`.orig.tar.gz rm -r MANIFEST dist # there are no tests - build package anyway override_dh_auto_test: dhcpy6d-1.2.3/debian/source/000077500000000000000000000000001437472361000156075ustar00rootroot00000000000000dhcpy6d-1.2.3/debian/source/format000066400000000000000000000000141437472361000170150ustar00rootroot000000000000003.0 (quilt) dhcpy6d-1.2.3/debian/source/options000066400000000000000000000001651437472361000172270ustar00rootroot00000000000000extend-diff-ignore=MANIFEST\.in extend-diff-ignore=README\.md extend-diff-ignore=build\.sh extend-diff-ignore=redhat dhcpy6d-1.2.3/debian/watch000066400000000000000000000001261437472361000153370ustar00rootroot00000000000000version=4 https://github.com/HenriWahl/dhcpy6d/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gzdhcpy6d-1.2.3/dhcpy6d/000077500000000000000000000000001437472361000144265ustar00rootroot00000000000000dhcpy6d-1.2.3/dhcpy6d/__init__.py000066400000000000000000000057171437472361000165510ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Module dhcpy6d""" import socket import socketserver import struct from .config import cfg from .globals import (collected_macs, IF_NAME, IF_NUMBER, NC, OS, timer) from .helpers import (colonify_ip6, colonify_mac, correct_mac, decompress_ip6, NeighborCacheRecord) from .log import log from .storage import volatile_store class UDPMulticastIPv6(socketserver.UnixDatagramServer): """ modify server_bind to work with multicast add DHCPv6 multicast group ff02::1:2 """ def server_bind(self): """ multicast & python: http://code.activestate.com/recipes/442490/ """ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # multicast parameters # hop is one because it is all about the same subnet self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0) self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1) for i in cfg.INTERFACE: IF_NAME[i] = socket.if_nametoindex(i) IF_NUMBER[IF_NAME[i]] = i if_number = struct.pack('I', IF_NAME[i]) #mgroup = socket.inet_pton(socket.AF_INET6, cfg.MCAST) + if_number # no need vor variable.... the DHCPv6 multicast address is predefined mgroup = socket.inet_pton(socket.AF_INET6, 'ff02::1:2') + if_number # join multicast group - should work definitively if not ignoring interface at startup if cfg.IGNORE_INTERFACE: try: self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mgroup) except Exception as err: print(err) else: self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mgroup) # bind socket to server address self.socket.bind(self.server_address) # attempt to avoid blocking self.socket.setblocking(False) # some more requests? self.request_queue_size = 100 dhcpy6d-1.2.3/dhcpy6d/client/000077500000000000000000000000001437472361000157045ustar00rootroot00000000000000dhcpy6d-1.2.3/dhcpy6d/client/__init__.py000066400000000000000000000274111437472361000200220ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import re import sys import traceback from ..config import cfg from ..constants import CONST from ..globals import (DUMMY_MAC, EMPTY_OPTIONS, IGNORED_LOG_OPTIONS) from ..helpers import colonify_ip6 from ..log import log from ..storage import config_store from .default import default from .from_config import from_config from .parse_pattern import (parse_pattern_address, parse_pattern_prefix) from .reuse_lease import reuse_lease class Client: """ client object, generated from configuration database or on the fly """ def __init__(self, transaction=None): # Addresses, depending on class or fixed addresses self.addresses = list() # Bootfiles, depending on class and architecture self.bootfiles = list() # Last chosen Bootfile self.chosen_boot_file = '' # Prefixes, depending on class or fixed prefixes self.prefixes = list() # DUID self.duid = '' # Hostname self.hostname = '' # Class/role of client - sadly "class" is a keyword and "class_" is more error prone self.client_class = '' # MAC self.mac = '' # timestamp of last update self.last_update = '' # when an transaction is given build the client if transaction is not None: self.build(transaction) def get_options_string(self): """ all attributes in a string for logging """ options_string = '' # put own attributes into a string options = sorted(self.__dict__.keys()) # options.sort() for option in options: # ignore some attributes if option not in IGNORED_LOG_OPTIONS and self.__dict__[option] not in EMPTY_OPTIONS: if option == 'addresses': if 'addresses' in cfg.CLASSES[self.client_class].ADVERTISE: option_string = f'{option}:' for address in self.__dict__[option]: option_string += f' {colonify_ip6(address.ADDRESS)}' options_string = f'{options_string} | {option_string}' elif option == 'bootfiles': option_string = f'{option}:' for bootfile in self.__dict__[option]: option_string += f' {bootfile.BOOTFILE_URL}' options_string = f'{options_string} | {option_string}' elif option == 'prefixes': if 'prefixes' in cfg.CLASSES[self.client_class].ADVERTISE: option_string = f'{option}:' for p in self.__dict__[option]: option_string += f' {colonify_ip6(p.PREFIX)}/{p.LENGTH}' options_string = f'{options_string} | {option_string}' elif option == 'mac': if self.__dict__[option] != DUMMY_MAC: option_string = f'{option}: {self.__dict__[option]}' options_string = f'{options_string} | {option_string}' else: option_string = f'{option}: {self.__dict__[option]}' options_string = f'{options_string} | {option_string}' return options_string def build(self, transaction): """ builds client object of client config and transaction data checks if filters apply check if lease is still valid for RENEW and REBIND answers check if invalid addresses need to get deleted with lifetime 0 """ try: # configuration from client deriving from general config or filters - defaults to none client_config = None # list to collect filtered client information # if there are more than one entries that do not match the class is not uniquely identified filtered_class = {} # check if there are identification attributes of any class - classes are sorted by filter types for f in cfg.FILTERS: # look into all classes and their filters for c in cfg.FILTERS[f]: # check further only if class applies to interface if transaction.interface in c.INTERFACE: # MACs if c.FILTER_MAC != '': pattern = re.compile(c.FILTER_MAC) # if mac filter fits client mac address add client config if len(pattern.findall(transaction.mac)) > 0: client_config = config_store.get_client_config(hostname=transaction.hostname, mac=[transaction.mac], duid=transaction.duid, client_class=c.NAME) # add classname to dictionary - if there are more than one entry classes do not match # and thus are invalid filtered_class[c.NAME] = c # DUIDs if c.FILTER_DUID != '': pattern = re.compile(c.FILTER_DUID) # if duid filter fits client duid address add client config if len(pattern.findall(transaction.duid)) > 0: client_config = config_store.get_client_config(hostname=transaction.hostname, mac=[transaction.mac], duid=transaction.duid, client_class=c.NAME) # see above filtered_class[c.NAME] = c # HOSTNAMEs if c.FILTER_HOSTNAME != '': pattern = re.compile(c.FILTER_HOSTNAME) # if hostname filter fits client hostname address add client config if len(pattern.findall(transaction.hostname)) > 0: client_config = config_store.get_client_config(hostname=transaction.hostname, mac=[transaction.mac], duid=transaction.duid, client_class=c.NAME) # see above filtered_class[c.NAME] = c # if there are more than 1 different classes matching for the client they are not valid if len(filtered_class) != 1: client_config = None # if filters did not get a result try it the hard way if client_config is None: # check all given identification criteria - if they all match each other the client is identified id_attributes = [] # get client config that most probably seems to fit # only ask DB if store is a DB if cfg.STORE_CONFIG != 'file': config_store.build_config_from_db(transaction) # check every attribute which is required # depending on identificaton mode empty results are ignored or considered # finally all attributes are grouped in sets and for a correctly identified host # only one entry should appear at the end for identification in cfg.IDENTIFICATION: if identification == 'mac': # get all MACs for client from config macs = config_store.get_client_config_by_mac(transaction) if macs: macs = set(macs) id_attributes.append(macs) elif cfg.IDENTIFICATION_MODE == 'match_all': macs = set() id_attributes.append(macs) if identification == 'duid': duids = config_store.get_client_config_by_duid(transaction) if duids: duids = set(duids) id_attributes.append(duids) elif cfg.IDENTIFICATION_MODE == 'match_all': duids = set() id_attributes.append(duids) if identification == 'hostname': hostnames = config_store.get_client_config_by_hostname(transaction) if hostnames: hostnames = set(hostnames) id_attributes.append(hostnames) elif cfg.IDENTIFICATION_MODE == 'match_all': hostnames = set() id_attributes.append(hostnames) # get intersection of all sets of identifying attributes - even the empty ones if len(id_attributes) > 0: client_config = set.intersection(*id_attributes) # if exactly one client has been identified use that config if len(client_config) == 1: # reuse client_config, grab it out of the set client_config = client_config.pop() else: # in case there is no client config we should maybe log this? client_config = None else: client_config = None # If client gave some addresses previously for RENEW or REBIND consider them if (transaction.last_message_received_type is CONST.MESSAGE.RENEW or transaction.last_message_received_type is CONST.MESSAGE.REBIND) and \ not (len(transaction.addresses) == 0 and len(transaction.prefixes) == 0): # use already existing lease reuse_lease(client=self, client_config=client_config, transaction=transaction) # build IA addresses from config - fixed ones and dynamic elif client_config is not None: # build client from config from_config(client=self, client_config=client_config, transaction=transaction) else: # use default class if host is unknown default(client=self, client_config=client_config, transaction=transaction) except Exception as err: traceback.print_exc(file=sys.stdout) sys.stdout.flush() log.error('build(): ' + str(err)) return None dhcpy6d-1.2.3/dhcpy6d/client/default.py000066400000000000000000000073471437472361000177150ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from ..config import (Address, cfg, Prefix) from ..constants import CONST from ..domain import get_ip_from_dns from .parse_pattern import (parse_pattern_address, parse_pattern_prefix) def default(client=None, client_config=None, transaction=None): # use default class if host is unknown client.hostname = transaction.hostname client.client_class = 'default_' + transaction.interface # apply answer type of client to transaction - useful if no answer or no address available is configured transaction.answer = cfg.CLASSES[client.client_class].ANSWER if 'addresses' in cfg.CLASSES['default_' + transaction.interface].ADVERTISE and \ (3 or 4) in transaction.ia_options: for address in cfg.CLASSES['default_' + transaction.interface].ADDRESSES: # addresses of category 'dns' will be searched in DNS if cfg.ADDRESSES[address].CATEGORY == 'dns': a = get_ip_from_dns(client.hostname) else: a = parse_pattern_address(cfg.ADDRESSES[address], client, transaction) if a: ia = Address(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE, preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME, valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME, category=cfg.ADDRESSES[address].CATEGORY, aclass=cfg.ADDRESSES[address].CLASS, atype=cfg.ADDRESSES[address].TYPE, dns_update=cfg.ADDRESSES[address].DNS_UPDATE, dns_zone=cfg.ADDRESSES[address].DNS_ZONE, dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE, dns_ttl=cfg.ADDRESSES[address].DNS_TTL) client.addresses.append(ia) if 'prefixes' in cfg.CLASSES['default_' + transaction.interface].ADVERTISE and \ CONST.OPTION.IA_PD in transaction.ia_options: for prefix in cfg.CLASSES['default_' + transaction.interface].PREFIXES: p = parse_pattern_prefix(cfg.PREFIXES[prefix], client_config, transaction) # in case range has been exceeded p will be None if p: ia_pd = Prefix(prefix=p, length=cfg.PREFIXES[prefix].LENGTH, preferred_lifetime=cfg.PREFIXES[prefix].PREFERRED_LIFETIME, valid_lifetime=cfg.PREFIXES[prefix].VALID_LIFETIME, category=cfg.PREFIXES[prefix].CATEGORY, pclass=cfg.PREFIXES[prefix].CLASS, ptype=cfg.PREFIXES[prefix].TYPE, route_link_local=cfg.PREFIXES[prefix].ROUTE_LINK_LOCAL) client.prefixes.append(ia_pd) # given client has been modified successfully return True dhcpy6d-1.2.3/dhcpy6d/client/from_config.py000066400000000000000000000206111437472361000205460ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from ..config import (Address, cfg, Prefix) from ..constants import CONST from ..domain import get_ip_from_dns from .parse_pattern import (parse_pattern_address, parse_pattern_prefix) def from_config(client=None, client_config=None, transaction=None): # give client hostname + class client.hostname = client_config.HOSTNAME client.client_class = client_config.CLASS # apply answer type of client to transaction - useful if no answer or no address available is configured transaction.answer = cfg.CLASSES[client.client_class].ANSWER # continue only if request interface matches class interfaces if transaction.interface in cfg.CLASSES[client.client_class].INTERFACE: # if fixed addresses are given build them if client_config.ADDRESS is not None and \ CONST.OPTION.IA_NA in transaction.ia_options: for address in client_config.ADDRESS: if len(address) > 0: # fixed addresses are assumed to be non-temporary # # todo: lifetime of address should be set by config too # ia = Address(address=address, ia_type='na', preferred_lifetime=cfg.PREFERRED_LIFETIME, valid_lifetime=cfg.VALID_LIFETIME, category='fixed', aclass='fixed', atype='fixed') client.addresses.append(ia) # if fixed prefixes are given and requested build them if client_config.PREFIX is not None and \ CONST.OPTION.IA_PD in transaction.ia_options: for prefix in client_config.PREFIX: ia_pd = Prefix(prefix=prefix['address'], length=prefix['length'], preferred_lifetime=cfg.PREFERRED_LIFETIME, valid_lifetime=cfg.VALID_LIFETIME, route_link_local=False) client.prefixes.append(ia_pd) if not client_config.CLASS == '': # add all addresses which belong to that class for address in cfg.CLASSES[client_config.CLASS].ADDRESSES: # addresses of category 'dns' will be searched in DNS if cfg.ADDRESSES[address].CATEGORY == 'dns': a = get_ip_from_dns(client.hostname) else: a = parse_pattern_address(cfg.ADDRESSES[address], client_config, transaction) # in case range has been exceeded a will be None if a: ia = Address(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE, preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME, valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME, category=cfg.ADDRESSES[address].CATEGORY, aclass=cfg.ADDRESSES[address].CLASS, atype=cfg.ADDRESSES[address].TYPE, dns_update=cfg.ADDRESSES[address].DNS_UPDATE, dns_zone=cfg.ADDRESSES[address].DNS_ZONE, dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE, dns_ttl=cfg.ADDRESSES[address].DNS_TTL) client.addresses.append(ia) # add all bootfiles which belong to that class for bootfile in cfg.CLASSES[client_config.CLASS].BOOTFILES: client_architecture = cfg.BOOTFILES[bootfile].CLIENT_ARCHITECTURE user_class = cfg.BOOTFILES[bootfile].USER_CLASS # check if transaction attributes matches the bootfile defintion if (not client_architecture or transaction.client_architecture == client_architecture or transaction.known_client_architecture == client_architecture) and \ (not user_class or transaction.user_class == user_class): client.bootfiles.append(cfg.BOOTFILES[bootfile]) # if prefixes are advertised in this class and the client demands a prefix (trough IA_PD) if 'prefixes' in cfg.CLASSES[client_config.CLASS].ADVERTISE and \ CONST.OPTION.IA_PD in transaction.ia_options: for prefix in cfg.CLASSES[client_config.CLASS].PREFIXES: p = parse_pattern_prefix(cfg.PREFIXES[prefix], client_config, transaction) # in case range has been exceeded p will be None if p: ia_pd = Prefix(prefix=p, length=cfg.PREFIXES[prefix].LENGTH, preferred_lifetime=cfg.PREFIXES[prefix].PREFERRED_LIFETIME, valid_lifetime=cfg.PREFIXES[prefix].VALID_LIFETIME, category=cfg.PREFIXES[prefix].CATEGORY, pclass=cfg.PREFIXES[prefix].CLASS, ptype=cfg.PREFIXES[prefix].TYPE, route_link_local=cfg.PREFIXES[prefix].ROUTE_LINK_LOCAL) client.prefixes.append(ia_pd) if client_config.ADDRESS == client_config.CLASS == '': # use default class if no class or address is given for address in cfg.CLASSES['default_' + transaction.interface].ADDRESSES: client.client_class = 'default_' + transaction.interface # addresses of category 'dns' will be searched in DNS if cfg.ADDRESSES[address].CATEGORY == 'dns': a = get_ip_from_dns(client.hostname) else: a = parse_pattern_address(cfg.ADDRESSES[address], client_config, transaction) if a: ia = Address(address=a, ia_type=cfg.ADDRESSES[address].IA_TYPE, preferred_lifetime=cfg.ADDRESSES[address].PREFERRED_LIFETIME, valid_lifetime=cfg.ADDRESSES[address].VALID_LIFETIME, category=cfg.ADDRESSES[address].CATEGORY, aclass=cfg.ADDRESSES[address].CLASS, atype=cfg.ADDRESSES[address].TYPE, dns_update=cfg.ADDRESSES[address].DNS_UPDATE, dns_zone=cfg.ADDRESSES[address].DNS_ZONE, dns_rev_zone=cfg.ADDRESSES[address].DNS_REV_ZONE, dns_ttl=cfg.ADDRESSES[address].DNS_TTL) client.addresses.append(ia) for bootfile in cfg.CLASSES['default_' + transaction.interface].BOOTFILES: client_architecture = bootfile.CLIENT_ARCHITECTURE user_class = bootfile.USER_CLASS # check if transaction attributes match the bootfile definition if (not client_architecture or transaction.client_architecture == client_architecture or transaction.known_client_architecture == client_architecture) and \ (not user_class or transaction.user_class == user_class): client.bootfiles.append(bootfile) # given client has been modified successfully return True dhcpy6d-1.2.3/dhcpy6d/client/parse_pattern.py000066400000000000000000000377101437472361000211350ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import random from ..config import cfg from ..helpers import (convert_mac_to_eui64, decompress_ip6) from ..log import log from ..storage import volatile_store def parse_pattern_address(address, client_config, transaction): """ parse address pattern and replace variables with current values """ # parse all pattern parts a = address.PATTERN # if dhcpy6d got a new (mostly dynamic) prefix at start insert it here if cfg.PREFIX is not None: a = a.replace('$prefix$', cfg.PREFIX) # check different client address categories - to be extended! if address.CATEGORY == 'mac': macraw = ''.join(transaction.mac.split(':')) a = a.replace('$mac$', ':'.join((macraw[0:4], macraw[4:8], macraw[8:12]))) elif address.CATEGORY == 'eui64': # https://tools.ietf.org/html/rfc4291#section-2.5.1 mac = transaction.mac a = a.replace('$eui64$', convert_mac_to_eui64(mac)) elif address.CATEGORY in ['fixed', 'dns']: # No patterns for fixed address, let's bail return None elif address.CATEGORY == 'id': # if there is an ID build address if str(client_config.ID) != '': a = a.replace('$id$', str(client_config.ID)) else: return None elif address.CATEGORY == 'random': # first check if address already has been advertised advertised_address = volatile_store.check_advertised_lease(transaction, category='random', atype=address.TYPE) # when address already has been advertised for this client use it if advertised_address: a = advertised_address else: ra = str(hex(random.getrandbits(64)))[2:][:-1] ra = ':'.join((ra[0:4], ra[4:8], ra[8:12], ra[12:16])) # subject to change.... a = a.replace('$random64$', ra) elif address.CATEGORY == 'range': range_from, range_to = address.RANGE.split('-') if len(range_from) < 4: range_from = '0' * (4-len(range_from)) + range_from if len(range_to) < 4: range_to = '0' * (4-len(range_to)) + range_to if range_from > range_to: range_from, range_to = range_to, range_from # expecting range-range at the last octet, 'prefix' means the first seven octets here # - is just shorter than the_first_seven_octets prefix = decompress_ip6(a.replace('$range$', '0000'))[:28] # the following steps are done to find a collision-free lease in given range # check if address already has been advertised - important for REPLY after SOLICIT-ADVERTISE-REQUEST advertised_address = volatile_store.check_advertised_lease(transaction, category='range', atype=address.TYPE) # when address already has been advertised for this client use it if advertised_address: a = advertised_address else: # check if requesting client still has an active lease that could be reused lease = volatile_store.get_range_lease_for_recycling(prefix=prefix, range_from=range_from, range_to=range_to, duid=transaction.duid, mac=transaction.mac) # the found lease has to be in range - important after changed range boundaries if lease is not None and range_from <= lease[28:].lower() <= range_to: a = ':'.join((lease[0:4], lease[4:8], lease[8:12], lease[12:16], lease[16:20], lease[20:24], lease[24:28], lease[28:32])) else: # get highest active lease to increment address about 1 lease = volatile_store.get_highest_range_lease(prefix=prefix, range_from=range_from, range_to=range_to) # check if highest active lease still fits into range if lease is not None and range_from <= lease[28:].lower() < range_to: # if highest lease + 1 would not fit range limit is reached if lease[28:].lower() >= range_to: # try to get one of the inactive old leases lease = volatile_store.get_oldest_inactive_range_lease(prefix=prefix, range_from=range_from, range_to=range_to) if lease is None: # if none is available limit is reached and nothing returned log.critical(f'Address space {prefix}[{range_from}-{range_to}] exceeded') return None else: # if lease is OK use it a = lease else: # otherwise increase current maximum range limit by 1 a = a.replace('$range$', str(hex(int(lease[28:], 16) + 1)).split('x')[1]) else: # if there is no lease yet or range limit is reached try to reactivate an old inactive lease lease = volatile_store.get_oldest_inactive_range_lease(prefix=prefix, range_from=range_from, range_to=range_to) if lease is None: # if there are no leases stored yet initiate lease storage # this will be done only once - the first time if there is no other lease yet # so it is safe to start from range_from if volatile_store.check_number_of_leases(prefix, range_from, range_to) <= 1: a = a.replace('$range$', range_from) else: # if none is available limit is reached and nothing returned log.critical(f'Address space {prefix}[{range_from}-{range_to}] exceeded') return None else: # if there is a lease it might be used a = lease return decompress_ip6(a) def parse_pattern_prefix(pattern, client_config, transaction): """ parse address pattern and replace variables with current values """ # parse all pattern parts p = pattern.PATTERN # if dhcpy6d got a new (mostly dynamic) prefix at start insert it here p = p.replace('$prefix$', cfg.PREFIX) if pattern.CATEGORY == 'id': # if there is an ID build address if str(client_config.ID) != '': p = p.replace('$id$', str(client_config.ID)) else: return None elif pattern.CATEGORY == 'range': range_from, range_to = pattern.RANGE.split('-') if len(range_from) < 4: range_from = '0' * (4-len(range_from)) + range_from if len(range_to) < 4: range_to = '0' * (4-len(range_to)) + range_to if range_from > range_to: range_from, range_to = range_to, range_from # contrary to addresses the prefix $range$ part of the pattern is expected # somewhere at the left part of the pattern # here the 128 Bit sum up to 32 characters in address/prefix string so prefix_range_index has to be calculated # as first character of range part of prefix - assuming steps of width 4 prefix_range_index = int(pattern.LENGTH)//4-4 # prefix itself has a prefix - the first part of the prefix pattern prefix_prefix = decompress_ip6(p.replace('$range$', '0000'))[:prefix_range_index] # the following steps are done to find a collision-free lease in given range # check if address already has been advertised - important for REPLY after SOLICIT-ADVERTISE-REQUEST advertised_prefix = volatile_store.check_advertised_prefix(transaction, category='range', ptype=pattern.TYPE) # when address already has been advertised for this client use it if advertised_prefix: p = advertised_prefix else: # check if requesting client still has an active prefix that could be reused prefix = volatile_store.get_range_prefix_for_recycling(prefix=prefix_prefix, length=pattern.LENGTH, range_from=range_from, range_to=range_to, duid=transaction.duid, mac=transaction.mac) # the found prefix has to be in range - important after changed range boundaries if prefix is not None: if range_from <= prefix[prefix_range_index:prefix_range_index+4].lower() <= range_to: p = ':'.join((prefix[0:4], prefix[4:8], prefix[8:12], prefix[12:16], prefix[16:20], prefix[20:24], prefix[24:28], prefix[28:32])) else: # if prefixes are exceeded or something went wrong with from/to ranges return none log.critical('Prefix address space %s[%s-%s] exceeded or something is wrong with from/to ranges' % (prefix_prefix, range_from, range_to)) return None else: # get highest active lease to increment address about 1 prefix = volatile_store.get_highest_range_prefix(prefix=prefix_prefix, length=pattern.LENGTH, range_from=range_from, range_to=range_to) # check if highest active lease still fits into range if prefix is not None: if range_from <= prefix[prefix_range_index:prefix_range_index+4].lower() < range_to: # if highest lease + 1 would not fit range limit is reached if prefix[prefix_range_index:prefix_range_index+4].lower() >= range_to: # try to get one of the inactive old leases prefix = volatile_store.get_oldest_inactive_range_prefix(prefix=prefix_prefix, length=pattern.LENGTH, range_from=range_from, range_to=range_to) if prefix is None: # if none is available limit is reached and nothing returned log.critical(f'Prefix address space {prefix_prefix}[{range_from}-{range_to}] exceeded') return None else: # if lease is OK use it p = prefix else: # otherwise increase current maximum range limit by 1 p = p.replace('$range$', str(hex(int(prefix[prefix_range_index:prefix_range_index+4].lower(), 16) + 1)).split('x')[1]) else: # if there is no lease yet or range limit is reached try to reactivate an old inactive lease prefix = volatile_store.get_oldest_inactive_range_prefix(prefix=prefix_prefix, length=pattern.LENGTH, range_from=range_from, range_to=range_to) if prefix is None: # if there are no leases stored yet initiate lease storage # this will be done only once - the first time if there is no other lease yet # so it is safe to start from range_from if volatile_store.check_number_of_prefixes(prefix=prefix_prefix, length=pattern.LENGTH, range_from=range_from, range_to=range_to) <= 1: p = p.replace('$range$', range_from) else: # if none is available limit is reached and nothing returned log.critical( f'Prefix address space {prefix_prefix}[{range_from}-{range_to}] exceeded') return None else: # if there is a lease it might be used p = prefix else: # if there is no lease yet or range limit is reached try to reactivate an old inactive lease prefix = volatile_store.get_oldest_inactive_range_prefix(prefix=prefix_prefix, length=pattern.LENGTH, range_from=range_from, range_to=range_to) if prefix is None: # if there are no leases stored yet initiate lease storage # this will be done only once - the first time if there is no other lease yet # so it is safe to start from range_from if volatile_store.check_number_of_prefixes(prefix=prefix_prefix, length=pattern.LENGTH, range_from=range_from, range_to=range_to) <= 1: p = p.replace('$range$', range_from) else: # if none is available limit is reached and nothing returned log.critical(f'Prefix address space {prefix_prefix}[{range_from}-{range_to}] exceeded') return None else: # if there is a lease it might be used p = prefix return decompress_ip6(p) dhcpy6d-1.2.3/dhcpy6d/client/reuse_lease.py000066400000000000000000000443331437472361000205610ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from ..config import (Address, cfg, Prefix) from ..constants import CONST from ..globals import timer from ..helpers import (decompress_ip6, decompress_prefix, split_prefix) from ..storage import volatile_store from .parse_pattern import parse_pattern_address def reuse_lease(client=None, client_config=None, transaction=None): """ Reuse already existing lease information """ if client_config is not None: # give client hostname client.hostname = client_config.HOSTNAME client.client_class = client_config.CLASS # apply answer type of client to transaction - useful if no answer or no address available is configured transaction.answer = cfg.CLASSES[client.client_class].ANSWER else: # use default class if host is unknown client.hostname = transaction.hostname client.client_class = 'default_' + transaction.interface # apply answer type of client to transaction - useful if no answer or no address available is configured transaction.answer = cfg.CLASSES[client.client_class].ANSWER if 'addresses' in cfg.CLASSES[client.client_class].ADVERTISE and \ (CONST.OPTION.IA_NA or CONST.OPTION.IA_TA) in transaction.ia_options: for address in transaction.addresses: # check_lease returns hostname, address, type, category, ia_type, class, preferred_until of leased address answer = volatile_store.check_lease(address, transaction) if answer: if len(answer) > 0: for item in answer: a = dict(list( zip(('hostname', 'address', 'type', 'category', 'ia_type', 'class', 'preferred_until'), item))) # if lease exists but no configured client set class to default if client_config is None: client.hostname = transaction.hostname client.client_class = 'default_' + transaction.interface # check if address type of lease still exists in configuration # and if request interface matches that of class if a['class'] in cfg.CLASSES and client.client_class == a['class'] and \ transaction.interface in cfg.CLASSES[client.client_class].INTERFACE: # type of address must be defined in addresses for this class # or fixed/dns - in which case it is not class related if a['type'] in cfg.CLASSES[a['class']].ADDRESSES or a['type'] in ['fixed']: # flag for lease usage use_lease = True # test lease validity against address prototype pattern only if not fixed or from DNS if not a['category'] in ['fixed', 'dns']: # test if address matches pattern for identification in range(len(address)): if address[identification] != cfg.ADDRESSES[a['type']].PROTOTYPE[ identification] and \ cfg.ADDRESSES[a['type']].PROTOTYPE[identification] != 'x': use_lease = False break elif a['category'] == 'fixed' and client_config.ADDRESS is not None: if address not in client_config.ADDRESS: use_lease = False elif a['category'] == 'dns': use_lease = False # only use lease if it still matches prototype if use_lease: # when category is range, test if it still applies if a['category'] == 'range': # borrowed from parse_pattern_address to find out if lease is still in # a meanwhile maybe changed range range_from, range_to = cfg.ADDRESSES[a['type']].RANGE.split('-') # correct possible misconfiguration if len(range_from) < 4: range_from = '0' * (4 - len(range_from)) + range_from if len(range_to) < 4: range_to = '0' * (4 - len(range_to)) + range_to if range_from > range_to: range_from, range_to = range_to, range_from # if lease is still inside range boundaries use it if range_from <= address[28:].lower() < range_to: # build IA partly of leases db, partly of config db ia = Address(address=a['address'], atype=a['type'], preferred_lifetime=cfg.ADDRESSES[a['type']].PREFERRED_LIFETIME, valid_lifetime=cfg.ADDRESSES[a['type']].VALID_LIFETIME, category=a['category'], ia_type=a['ia_type'], aclass=a['class'], dns_update=cfg.ADDRESSES[a['type']].DNS_UPDATE, dns_zone=cfg.ADDRESSES[a['type']].DNS_ZONE, dns_rev_zone=cfg.ADDRESSES[a['type']].DNS_REV_ZONE, dns_ttl=cfg.ADDRESSES[a['type']].DNS_TTL) client.addresses.append(ia) # de-preferred random address has to be deleted and replaced elif a['category'] == 'random' and timer.time > a['preferred_until']: # create new random address if old one is depreferred random_address = parse_pattern_address(cfg.ADDRESSES[a['type']], client_config, transaction) # create new random address if old one is de-preferred # do not wait until it is invalid if random_address is not None: ia = Address(address=random_address, ia_type=cfg.ADDRESSES[a['type']].IA_TYPE, preferred_lifetime=cfg.ADDRESSES[a['type']].PREFERRED_LIFETIME, valid_lifetime=cfg.ADDRESSES[a['type']].VALID_LIFETIME, category='random', aclass=cfg.ADDRESSES[a['type']].CLASS, atype=cfg.ADDRESSES[a['type']].TYPE, dns_update=cfg.ADDRESSES[a['type']].DNS_UPDATE, dns_zone=cfg.ADDRESSES[a['type']].DNS_ZONE, dns_rev_zone=cfg.ADDRESSES[a['type']].DNS_REV_ZONE, dns_ttl=cfg.ADDRESSES[a['type']].DNS_TTL) client.addresses.append(ia) # set de-preferred address invalid client.addresses.append(Address(address=a['address'], valid=False, preferred_lifetime=0, valid_lifetime=0)) else: # build IA partly of leases db, partly of config db ia = Address(address=a['address'], atype=a['type'], preferred_lifetime=cfg.ADDRESSES[a['type']].PREFERRED_LIFETIME, valid_lifetime=cfg.ADDRESSES[a['type']].VALID_LIFETIME, category=a['category'], ia_type=a['ia_type'], aclass=a['class'], dns_update=cfg.ADDRESSES[a['type']].DNS_UPDATE, dns_zone=cfg.ADDRESSES[a['type']].DNS_ZONE, dns_rev_zone=cfg.ADDRESSES[a['type']].DNS_REV_ZONE, dns_ttl=cfg.ADDRESSES[a['type']].DNS_TTL) client.addresses.append(ia) # important indent here, has to match for...addresses-loop! # look for addresses in transaction that are invalid and add them # to client addresses with flag invalid and a RFC-compliant lifetime of 0 for a in set(transaction.addresses).difference( [decompress_ip6(x.ADDRESS) for x in client.addresses]): client.addresses.append(Address(address=a, valid=False, preferred_lifetime=0, valid_lifetime=0)) if 'prefixes' in cfg.CLASSES[client.client_class].ADVERTISE and \ CONST.OPTION.IA_PD in transaction.ia_options: for prefix in transaction.prefixes: # split prefix of prefix from length, separated by / prefix_prefix, prefix_length = split_prefix(prefix) # check_prefix returns hostname, prefix, length, type, category, class, preferred_until of leased address answer = volatile_store.check_prefix(prefix_prefix, prefix_length, transaction) if answer: if len(answer) > 0: for item in answer: p = dict(list( zip(('hostname', 'prefix', 'length', 'type', 'category', 'class', 'preferred_until'), item))) # if lease exists but no configured client set class to default if client_config is None: client.hostname = transaction.hostname client.client_class = 'default_' + transaction.interface # check if address type of lease still exists in configuration # and if request interface matches that of class if p['class'] in cfg.CLASSES and client.client_class == p['class'] and \ transaction.interface in cfg.CLASSES[client.client_class].INTERFACE: # type of address must be defined in addresses for this class # or fixed/dns - in which case it is not class related if p['type'] in cfg.CLASSES[p['class']].PREFIXES: # flag for lease usage use_lease = True # test if prefix matches pattern for identification in range(len(prefix_prefix)): if prefix_prefix[identification] != cfg.PREFIXES[p['type']].PROTOTYPE[ identification] and \ cfg.PREFIXES[p['type']].PROTOTYPE[identification] != 'x': use_lease = False break # only use prefix if it still matches prototype if use_lease: # when category is range, test if it still applies if p['category'] == 'range': # borrowed from parse_pattern_prefix to find out if lease # is still in a meanwhile maybe changed range range_from, range_to = cfg.PREFIXES[p['type']].RANGE.split('-') # correct possible misconfiguration if len(range_from) < 4: range_from = '0' * (4 - len(range_from)) + range_from if len(range_to) < 4: range_to = '0' * (4 - len(range_to)) + range_to if range_from > range_to: range_from, range_to = range_to, range_from # contrary to addresses the prefix $range$ part of the pattern is expected # somewhere at the left part of the pattern # here the 128 Bit sum up to 32 characters in address/prefix string so # prefix_range_index has to be calculated as first character of range part of # prefix - assuming steps of width 4 prefix_range_index = int(cfg.PREFIXES[p['type']].LENGTH) // 4 - 4 # prefix itself has a prefix - the first part of the prefix pattern prefix_prefix = decompress_ip6(p['prefix'].replace('$range$', '0000'))[ :prefix_range_index + 4] # if lease is still inside range boundaries use it if range_from <= prefix_prefix[ prefix_range_index:prefix_range_index + 4].lower() < range_to: # build IA partly of leases db, partly of config db ia = Prefix(prefix=p['prefix'], length=p['length'], ptype=p['type'], preferred_lifetime=cfg.PREFIXES[p['type']].PREFERRED_LIFETIME, valid_lifetime=cfg.PREFIXES[p['type']].VALID_LIFETIME, category=p['category'], pclass=p['class'], route_link_local=cfg.PREFIXES[p['type']].ROUTE_LINK_LOCAL) client.prefixes.append(ia) # add prefixes which are bound to client to advertised prefixes else: ia = Prefix(prefix=p['prefix'], length=p['length'], ptype=p['type'], preferred_lifetime=cfg.PREFERRED_LIFETIME, valid_lifetime=cfg.VALID_LIFETIME, category=p['category'], pclass=p['class'], route_link_local=False) client.prefixes.append(ia) # important indent here, has to match for...prefixes-loop! # look for prefixes in transaction that are invalid and add them # to client prefixes with flag invalid and a RFC-compliant lifetime of 0 if len(client.prefixes) > 0: for p in set(transaction.prefixes).difference( [decompress_prefix(x.PREFIX, x.LENGTH) for x in client.prefixes]): prefix, length = split_prefix(p) client.prefixes.append(Prefix(prefix=prefix, length=length, valid=False, preferred_lifetime=0, valid_lifetime=0)) del (prefix, length) # given client has been modified successfully return True dhcpy6d-1.2.3/dhcpy6d/config.py000066400000000000000000001643641437472361000162630ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import configparser import copy import getopt import grp import os import os.path import platform import pwd import re import shlex import stat import sys import time import uuid from .helpers import (decompress_ip6, error_exit, get_interfaces, listify_option, LOCALHOST_INTERFACES, send_control_message) # needed for boolean options BOOLPOOL = {'0': False, '1': True, 'no': False, 'yes': True, 'false': False, 'true': True, False: False, True: True, 'on': True, 'off': False} # whitespace for options with more than one value WHITESPACE = ' ,' # empty default prefix - if needed given by command line argument PREFIX = '' # default usage text - to be extended USAGE = ''' dhcpy6d - DHCPv6 server Usage: dhcpy6d --config [--user ] [--group ] [--duid ] [--prefix ] [--really-do-it |] dhcpy6d --message '' dhcpy6d --generate-duid See manpage dhcpy6d(8) for details. ''' class Config: """ general settings """ def __init__(self): """ define defaults """ # access dynamic PREFIX global PREFIX self.PREFIX = PREFIX # default settings # Server cfg.INTERFACE + addresses self.INTERFACE = '' self.ADDRESS = '2001:db8::1' # issue #50 proposes an except-interface option like in dnsmasq - interfaces not to listen at self.EXCLUDE_INTERFACE = '' # effective user and group - will have to be set mainly by distribution package self.USER = 'root' self.GROUP = 'root' # lets make the water turn black... or build a shiny server DUID # in case someone will ever debug something here: Wireshark shows # year 2042 even if it is 2012 - time itself is OK self.SERVERDUID = generate_duid() self.NAMESERVER = '' # domain for FQDN hostnames self.DOMAIN = 'domain' # domain search list for option 24, according to RFC 3646 # defaults to DOMAIN self.DOMAIN_SEARCH_LIST = '' # IA_NA Options # Default preferred lifetime for addresses self.PREFERRED_LIFETIME = '5400' # Default valid lifetime for addresses self.VALID_LIFETIME = '7200' # T1 RENEW self.T1 = '2700' # T2 REBIND self.T2 = '4050' # Server Preference self.SERVER_PREFERENCE = '255' # SNTP SERVERS Option 31 self.SNTP_SERVERS = '' # NTP server Option 56 self.NTP_SERVER = '' # Auxiliary options, derived from self.NTP_SERVER self.NTP_SERVER_DICT = {'SRV': [], 'MC': [], 'FQDN': []} # INFORMATION REFRESH TIME option 32 for option 11 (INFORMATION REQUEST) # see RFC http://tools.ietf.org/html/rfc4242 self.INFORMATION_REFRESH_TIME = '6000' # config type # one of file, mysql, sqlite or none self.STORE_CONFIG = 'none' # one of mysql, postgresql or sqlite self.STORE_VOLATILE = 'sqlite' # file for client information self.STORE_FILE_CONFIG = '/etc/dhcpy6d-clients.conf' # DB data self.STORE_DB_HOST = 'localhost' self.STORE_DB_DB = 'dhcpy6d' self.STORE_DB_USER = 'user' self.STORE_DB_PASSWORD = 'password' self.STORE_SQLITE_CONFIG = 'config.sqlite' self.STORE_SQLITE_VOLATILE = '/var/lib/dhcpy6d/volatile.sqlite' # whether MAC-LLIP pairs should be stored forever or retrieved freshly if needed self.CACHE_MAC_LLIP = 'False' # DNS Update settings self.DNS_UPDATE = 'False' self.DNS_UPDATE_NAMESERVER = '::1' self.DNS_TTL = 86400 self.DNS_USE_RNDC = 'True' self.DNS_RNDC_KEY = 'rndc-key' self.DNS_RNDC_SECRET = '0000000000000000000000000000000000000000000000000000000000000' # DNS RFC 4704 client DNS wishes # use client supplied hostname self.DNS_USE_CLIENT_HOSTNAME = 'False' # ignore client ideas about DNS (if at all, what name to use, self-updating...) self.DNS_IGNORE_CLIENT = 'True' # Log ot not self.LOG = 'False' # Log level self.LOG_LEVEL = 'INFO' # Log on console self.LOG_CONSOLE = 'False' # Logfile self.LOG_FILE = '' # Log to syslog self.LOG_SYSLOG = 'False' # Syslog facility self.LOG_SYSLOG_FACILITY = 'daemon' # Local syslog socket or server:port if platform.system() in ['Linux', 'OpenBSD']: self.LOG_SYSLOG_DESTINATION = '/dev/log' else: self.LOG_SYSLOG_DESTINATION = '/var/run/log' # log newly found MAC addresses - if CACHE_MAC_LLIP is false this might be way too much self.LOG_MAC_LLIP = 'False' # some 128 bits self.AUTHENTICATION_INFORMATION = '00000000000000000000000000000000' # for debugging - if False nothing is done self.REALLY_DO_IT = 'False' # interval for TidyUp thread - time to sleep in TidyUpThread self.CLEANING_INTERVAL = 10 # address, bootfile and class schemes self.ADDRESSES = {} self.BOOTFILES = {} self.CLASSES = {} self.PREFIXES = {} # how to identify clients self.IDENTIFICATION = 'mac' self.IDENTIFICATION_MODE = 'match_all' # allow one to ignore IAIDs which play no big role at all for server self.IGNORE_IAID = 'False' # ignore clients which do no appear in the neighbor cache table self.IGNORE_UNKNOWN_CLIENTS = 'True' # ignore MAC addresses as identifier - useful for neighbor-cache-less interfaces like ppp0 self.IGNORE_MAC = 'False' # ignore interface to be able to listen on dynamically created interfaces like ppp self.IGNORE_INTERFACE = 'False' # allow setting request rate limits to put clients onto blacklist self.REQUEST_LIMIT = 'no' self.REQUEST_LIMIT_TIME = '60' self.REQUEST_LIMIT_COUNT = '20' self.REQUEST_LIMIT_RELEASE_TIME = '7200' self.REQUEST_LIMIT_IDENTIFICATION = 'llip' # restore still valid routes at startup and remove inactive ones self.MANAGE_ROUTES_AT_START = 'False' # regexp filters for hostnames etc. self.FILTERS = {'mac': [], 'duid': [], 'hostname': []} # define a fallback default class and address scheme self.ADDRESSES['default'] = Address(ia_type='na', category='mac', pattern='fdef::$mac$', aclass='default', atype='default', prototype='fdef0000000000000000xxxxxxxxxxxx') # define dummy address scheme for fixed addresses # pattern and prototype are not really needed as this # addresses are fixed self.ADDRESSES['fixed'] = Address(ia_type='na', category='fixed', pattern='fdef::1', aclass='default', atype='fixed', prototype='fdef0000000000000000000000000000') self.PREFIXES['default'] = Prefix(pattern='fdef:$range$::', prange='1000-1fff', category='range') self.CLASSES['default'] = Class() self.CLASSES['default'].ADDRESSES.append('default') self.CLASSES['default'].PREFIXES.append('default') # config file from command line # default config file and cli values configfile = self.cli_options = self.cli_user = self.cli_group = self.cli_duid = self.cli_really_do_it = None # get multiple options try: self.cli_options, cli_remains = getopt.gnu_getopt(sys.argv[1:], 'c:u:g:d:r:p:m:G', ['config=', 'user=', 'group=', 'duid=', 'really-do-it=', 'prefix=', 'message=', 'generate-duid']) for opt, arg in self.cli_options: if opt in ('-c', '--config'): configfile = arg if opt in ('-g', '--group'): self.cli_group = arg if opt in ('-u', '--user'): self.cli_user = arg if opt in ('-d', '--duid'): self.cli_duid = arg if opt in ('-r', '--really-do-it'): self.cli_really_do_it = arg if opt in ('-p', '--prefix'): PREFIX = arg self.PREFIX = PREFIX if opt in ('-m', '--message'): send_control_message(arg) sys.exit(0) if opt in ('-G', '--generate-duid'): print(generate_duid()) sys.exit(0) except getopt.GetoptError as err: print(err) print(USAGE) sys.exit(0) if configfile is None: error_exit('No config file given - please use --config ') if os.path.exists(configfile): if not (os.path.isfile(configfile) or os.path.islink(configfile)): error_exit(f"Configuration file '{configfile}' is no file or link.") else: error_exit(f"Configuration file '{configfile}' does not exist.") # read config at once self.read_config(configfile) def read_config(self, configfile): """ read configuration from file, should work with included files too - at least this is the plan """ # instantiate Configparser config = configparser.ConfigParser() config.read(configfile) # error message prefix for check config some lines later msg_prefix = 'General configuration:' # whyever sections classes get overwritten sometimes and so some configs had been missing # so create classes and addresses here for section in config.sections(): # global PXE boot url schemes if section.startswith('bootfile_'): self.BOOTFILES[section.split('bootfile_')[1]] = BootFile(name=section.split('bootfile_')[1].strip()) if section.startswith('class_'): self.CLASSES[section.split('class_')[1]] = Class(name=section.split('class_')[1].strip()) if section.startswith('address_'): self.ADDRESSES[section.split('address_')[1].strip()] = Address() if section.startswith('prefix_'): self.PREFIXES[section.split('prefix_')[1].strip()] = Prefix() for section in config.sections(): # go through all items for item in config.items(section): if section.upper() == 'DHCPY6D': # check for legacy settings - STORE_MYSQL_* will be replaced by STORE_DB_* since 0.4.2 # see https://github.com/HenriWahl/dhcpy6d/issues/3 if item[0].upper() in ('STORE_MYSQL_HOST', 'STORE_MYSQL_DB', 'STORE_MYSQL_USER', 'STORE_MYSQL_PASSWORD'): sys.stderr.write(f"\nWARNING: Keyword '{item[0]}' in section '[{section}]' " f"is deprecated and should be replaced " f"by '{item[0].lower().replace('mysql', 'db')}'.\n\n") object.__setattr__(self, item[0].upper().replace('MYSQL', 'DB'), str(item[1]).strip()) # keyword is deprecated elif item[0].upper() in ('MCAST', 'PORT'): sys.stderr.write(f"\nWARNING: Keyword '{item[0]}' in section '[{section}]' " f"is deprecated and can safely be removed.\n") # check if keyword is known - if not, exit elif not self.__dict__: error_exit(f"Keyword '{item[0]}' in section '[{section}]' " f"of configuration file '{configfile}' is unknown.") # ConfigParser seems to be not case sensitive so settings get normalized else: object.__setattr__(self, item[0].upper(), str(item[1]).strip()) # treat interface exceptions here already to be able to use non-excluded interfaces in other sections # to be able to set used interfaces the except_interfaces has to be evaluated here if item[0].upper() in ['INTERFACE', 'EXCLUDE_INTERFACE']: # get interfaces as list self.INTERFACE = listify_option(self.INTERFACE) self.EXCLUDE_INTERFACE = listify_option(self.EXCLUDE_INTERFACE) if self.EXCLUDE_INTERFACE: # just make sure there is either interface or except_interface set if self.INTERFACE: error_exit(f"{msg_prefix} Interface and excluded interface are mutually exclusive.") else: # check if interface is not excluded and not localhost interface for interface in self.EXCLUDE_INTERFACE: if not interface in get_interfaces(): error_exit(f"{msg_prefix} Excluded interface '{interface}' is unknown.") # reset interface to be filled with non-excluded interfaces self.INTERFACE = [] # set non-excluded interfaces for interface in get_interfaces(): if not interface in self.EXCLUDE_INTERFACE and \ not interface in LOCALHOST_INTERFACES: self.INTERFACE.append(interface) else: # global PXE boot url schemes if section.lower().startswith('bootfile_'): if not item[0].upper() in self.BOOTFILES[section.lower().split('bootfile_')[1]].__dict__: error_exit(f"Keyword '{item[0]}' in section '[{section}]' " f"of configuration file '{configfile}' is unknown.") self.BOOTFILES[section.lower().split('bootfile_')[1]].__setattr__(item[0].upper(), str(item[1]).strip()) # global address schemes if section.lower().startswith('address_'): # check if keyword is known - if not, exit if item[0].upper() == 'PREFIX_LENGTH': # Show a warning because there are no prefix lengths in DHCPv6 sys.stderr.write(f"\nWARNING: Keyword '{item[0]}' in section '{section}' is deprecated " "and should be removed.\n\n") else: if not item[0].upper() in self.ADDRESSES[section.lower().split('address_')[1]].__dict__: error_exit(f"Keyword '{item[0]}' in section '[{section}]' " f"of configuration file '{configfile}' is unknown.") self.ADDRESSES[section.lower().split('address_')[1]].__setattr__(item[0].upper(), str(item[1]).strip()) # global prefix schemes if section.lower().startswith('prefix_'): if not item[0].upper() in self.PREFIXES[section.lower().split('prefix_')[1]].__dict__: error_exit(f"Keyword '{item[0]}' in section '[{section}]' " f"of configuration file '{configfile}' is unknown.") self.PREFIXES[section.lower().split('prefix_')[1]].__setattr__(item[0].upper(), str(item[1]).strip()) # global classes with their addresses elif section.lower().startswith('class_'): # check if keyword is known - if not, exit if not item[0].upper() in self.CLASSES[section.lower().split('class_')[1]].__dict__: error_exit(f"Keyword '{item[0]}' in section '[{section}]' " f"of configuration file '{configfile}' is unknown.") if item[0].upper() == 'ADDRESSES': # strip whitespace and separators of addresses lex = shlex.shlex(item[1]) lex.whitespace = WHITESPACE lex.wordchars += ':.' for address in lex: if len(address) > 0: self.CLASSES[section.lower().split('class_')[1]].ADDRESSES.append(address) elif item[0].upper() == 'BOOTFILES': # strip whitespace and separators of bootfiles lex = shlex.shlex(item[1]) lex.whitespace = WHITESPACE lex.wordchars += ':.' for bootfile in lex: if len(bootfile) > 0: self.CLASSES[section.lower().split('class_')[1]].BOOTFILES.append(bootfile) elif item[0].upper() == 'PREFIXES': # strip whitespace and separators of prefixes lex = shlex.shlex(item[1]) lex.whitespace = WHITESPACE lex.wordchars += ':.' for prefix in lex: if len(prefix) > 0: self.CLASSES[section.lower().split('class_')[1]].PREFIXES.append(prefix) elif item[0].upper() == 'ADVERTISE': # strip whitespace and separators of advertised IAs lex = shlex.shlex(item[1]) lex.whitespace = WHITESPACE lex.wordchars += ':.' self.CLASSES[section.lower().split('class_')[1]].ADVERTISE[:] = [] for advertise in lex: if len(advertise) > 0: self.CLASSES[section.lower().split('class_')[1]].ADVERTISE.append(advertise) elif item[0].upper() == 'INTERFACE': # strip whitespace and separators of interfaces lex = shlex.shlex(item[1]) lex.whitespace = WHITESPACE lex.wordchars += ':.' for interface in lex: if interface not in self.INTERFACE: error_exit(f"Interface '{interface}' used in section '[{section}]' " f"of configuration file '{configfile}' is not " "defined in general settings.") self.CLASSES[section.lower().split('class_')[1]].INTERFACE.append(interface) else: self.CLASSES[section.lower().split('class_')[1]].__setattr__(item[0].upper(), str(item[1]).strip()) # The next paragraphs contain finetuning self.IDENTIFICATION = listify_option(self.IDENTIFICATION) # at least something interfacy should be configured if not self.INTERFACE and \ not self.EXCLUDE_INTERFACE: error_exit(f"{msg_prefix} Neither interface or excluded interface is defined.") # create default classes for each interface - if not defined # derive from default 'default' class for interface in self.INTERFACE: if not 'default_' + interface in self.CLASSES: self.CLASSES['default_' + interface] = copy.copy(self.CLASSES['default']) self.CLASSES['default_' + interface].NAME = 'default_' + interface self.CLASSES['default_' + interface].INTERFACE = [interface] # lower storage self.STORE_CONFIG = self.STORE_CONFIG.lower() self.STORE_VOLATILE = self.STORE_VOLATILE.lower() # boolize none-config-store if self.STORE_CONFIG.lower() == 'none': self.STORE_CONFIG = False # if no domain search list has been given use DOMAIN if len(self.DOMAIN_SEARCH_LIST) == 0: self.DOMAIN_SEARCH_LIST = self.DOMAIN # domain search list has to be a list self.DOMAIN_SEARCH_LIST = listify_option(self.DOMAIN_SEARCH_LIST) # get nameservers as list if len(self.NAMESERVER) > 0: self.NAMESERVER = listify_option(self.NAMESERVER) # option 31 quite probably is obsolete but might still be used, so just take its values from newer option 56 # client dhcpcd for example uses this option when asking for NTP server if len(self.SNTP_SERVERS) > 0: self.SNTP_SERVERS = listify_option(self.SNTP_SERVERS) elif len(self.NTP_SERVER) > 0: self.SNTP_SERVERS = listify_option(self.NTP_SERVER) # get NTP servers as list if len(self.NTP_SERVER) > 0: self.NTP_SERVER = listify_option(self.NTP_SERVER) # convert to boolean values for option in ['DNS_IGNORE_CLIENT', 'DNS_USE_CLIENT_HOSTNAME', 'DNS_USE_RNDC', 'DNS_UPDATE', 'REALLY_DO_IT', 'LOG', 'LOG_CONSOLE', 'LOG_SYSLOG', 'CACHE_MAC_LLIP', 'LOG_MAC_LLIP', 'IGNORE_IAID', 'IGNORE_UNKNOWN_CLIENTS', 'IGNORE_MAC', 'IGNORE_INTERFACE', 'REQUEST_LIMIT', 'MANAGE_ROUTES_AT_START']: try: self.__dict__[option] = BOOLPOOL[self.__dict__[option].lower()] except: error_exit(f"Option '{option.lower()}' only allows boolean values like 'yes' and 'no'.") # upperize for syslog self.LOG_SYSLOG_FACILITY = self.LOG_SYSLOG_FACILITY.upper() self.LOG_LEVEL = self.LOG_LEVEL.upper() # index of classes which add some identification rules etc. for c in list(self.CLASSES.values()): if c.FILTER_MAC != '': self.FILTERS['mac'].append(c) if c.FILTER_DUID != '': self.FILTERS['duid'].append(c) if c.FILTER_HOSTNAME != '': self.FILTERS['hostname'].append(c) if c.NAMESERVER != '': c.NAMESERVER = listify_option(c.NAMESERVER) if c.NTP_SERVER != '': c.NTP_SERVER = listify_option(c.NTP_SERVER) if len(c.INTERFACE) == 0: # use general setting if none specified c.INTERFACE = self.INTERFACE # use default T1 and T2 if not defined if c.T1 == 0: c.T1 = self.T1 if c.T2 == 0: c.T2 = self.T2 # check advertised IA types - if empty default to ['addresses'] if len(c.ADVERTISE) == 0: c.ADVERTISE = ['addresses', 'prefixes'] # set type properties for addresses for a in self.ADDRESSES: # name for address, important for leases db self.ADDRESSES[a].TYPE = a if self.ADDRESSES[a].VALID_LIFETIME == 0: self.ADDRESSES[a].VALID_LIFETIME = self.VALID_LIFETIME if self.ADDRESSES[a].PREFERRED_LIFETIME == 0: self.ADDRESSES[a].PREFERRED_LIFETIME = self.PREFERRED_LIFETIME # normalize ranges self.ADDRESSES[a].RANGE = self.ADDRESSES[a].RANGE.lower() # convert boolean string to boolean value self.ADDRESSES[a].DNS_UPDATE = BOOLPOOL[self.ADDRESSES[a].DNS_UPDATE] if self.ADDRESSES[a].DNS_ZONE == '': self.ADDRESSES[a].DNS_ZONE = self.DOMAIN if self.ADDRESSES[a].DNS_TTL == '0': self.ADDRESSES[a].DNS_TTL = self.DNS_TTL # add prototype for later fast validity comparison of rebinding leases # also use as proof of validity of address patterns self.ADDRESSES[a].build_prototype() # set type properties for prefixes for p in self.PREFIXES: # name for address, important for leases db self.PREFIXES[p].TYPE = p if self.PREFIXES[p].VALID_LIFETIME == 0: self.PREFIXES[p].VALID_LIFETIME = self.VALID_LIFETIME if self.PREFIXES[p].PREFERRED_LIFETIME == 0: self.PREFIXES[p].PREFERRED_LIFETIME = self.PREFERRED_LIFETIME # normalize ranges self.PREFIXES[p].RANGE = self.PREFIXES[p].RANGE.lower() # route via Link Local Address self.PREFIXES[p].ROUTE_LINK_LOCAL = BOOLPOOL[self.PREFIXES[p].ROUTE_LINK_LOCAL] # add prototype for later fast validity comparison of rebinding leases # also use as proof of validity of address patterns self.PREFIXES[p].build_prototype() # check if some options are set by cli options if self.cli_user is not None: self.USER = self.cli_user if not self.cli_group is None: self.GROUP = self.cli_group if not self.cli_duid is None: self.SERVERDUID = self.cli_duid if not self.cli_really_do_it is None: self.REALLY_DO_IT = BOOLPOOL[self.cli_really_do_it.lower()] # check user and group try: pwd.getpwnam(self.USER) except: error_exit(f"{msg_prefix} User '{self.USER}' does not exist") try: grp.getgrnam(self.GROUP) except: error_exit(f"{msg_prefix} Group '{self.GROUP}' does not exist") # check interface if not self.IGNORE_INTERFACE: for i in self.INTERFACE: # also accept Linux VLAN and other definitions but interface must exist if not i in get_interfaces() or not re.match('^[a-z0-9_:.%-]*$', i, re.IGNORECASE): error_exit(f"{msg_prefix} Interface '{i}' is unknown.") # check server's address try: decompress_ip6(self.ADDRESS) except Exception as err: error_exit(f"{msg_prefix} Server address '{err}' is invalid.") # check server duid if not self.SERVERDUID.isalnum(): error_exit(f"{msg_prefix} Server DUID '{self.SERVERDUID}' must be alphanumeric.") # check nameserver to be given to client for nameserver in self.NAMESERVER: try: decompress_ip6(nameserver) except Exception as err: error_exit(f"{msg_prefix} Name server address '{err}' is invalid.") # split NTP server types into possible 3 (address, multicast, FQDN) # more details about this madness are available at https://tools.ietf.org/html/rfc5908 for ntp_server in self.NTP_SERVER: try: decompress_ip6(ntp_server) # if decompressing worked it must be an address if ntp_server.lower().startswith('ff'): self.NTP_SERVER_DICT['MC'].append(ntp_server.lower()) else: self.NTP_SERVER_DICT['SRV'].append(ntp_server.lower()) except Exception as err: if re.match('^[a-z0-9.-]*$', ntp_server, re.IGNORECASE): self.NTP_SERVER_DICT['FQDN'].append(ntp_server.lower()) else: error_exit(f"{msg_prefix} NTP server address '{ntp_server}' is invalid.") # partly check of domain name validity if not re.match('^[a-z0-9.-]*$', self.DOMAIN, re.IGNORECASE): error_exit(f"{msg_prefix} Domain name '{self.DOMAIN}' is invalid.") # partly check of domain name validity if not self.DOMAIN.lower()[0].isalpha() or \ not self.DOMAIN.lower()[-1].isalpha(): error_exit(f"{msg_prefix} Domain name '{self.DOMAIN}' is invalid.") # check domain search list domains for d in self.DOMAIN_SEARCH_LIST: # partly check of domain name validity if not re.match('^[a-z0-9.-]*$', d, re.IGNORECASE): error_exit(f"{msg_prefix} Domain search list domain name '{d}' is invalid.") # partly check of domain name validity if not d.lower()[0].isalpha() or \ not d.lower()[-1].isalpha(): error_exit(f"{msg_prefix} Domain search list domain name '{d}' is invalid.") # check if valid lifetime is a number if not self.VALID_LIFETIME.isdigit(): error_exit(f"{msg_prefix} Valid lifetime '{self.VALID_LIFETIME}' is invalid.") # check if preferred lifetime is a number if not self.PREFERRED_LIFETIME.isdigit(): error_exit(f"{msg_prefix} Preferred lifetime '{self.PREFERRED_LIFETIME}' is invalid.") # check if valid lifetime is longer than preferred lifetime if not int(self.VALID_LIFETIME) > int(self.PREFERRED_LIFETIME): error_exit(f"{msg_prefix} Valid lifetime '{self.VALID_LIFETIME}' is shorter " f"than preferred lifetime '{self.PREFERRED_LIFETIME}' and thus invalid.") # check if T1 is a number if not self.T1.isdigit(): error_exit(f"{msg_prefix} T1 '{self.T1}' is invalid.") # check if T2 is a number if not self.T2.isdigit(): error_exit(f"{msg_prefix} T2 '{self.T2}' is invalid.") # check T2 is not smaller than T1 if not int(self.T2) >= int(self.T1): error_exit(f"{msg_prefix} T2 '{self.T2}' is shorter than T1 '{self.T1}' and thus invalid.") # check if T1 <= T2 <= PREFERRED_LIFETIME <= VALID_LIFETIME if not (int(self.T1) <= int(self.T2) <= int(self.PREFERRED_LIFETIME) <= int(self.VALID_LIFETIME)): error_exit(f"{msg_prefix} Time intervals T1 '{self.T1}' <= T2 '{self.T2}' <= " f"preferred_lifetime '{self.PREFERRED_LIFETIME}' <= " f"valid_lifetime '{self.VALID_LIFETIME}' are wrong.") # check server preference if not self.SERVER_PREFERENCE.isdigit(): error_exit(f"{msg_prefix} Server preference '{self.SERVER_PREFERENCE}' is invalid.") elif not 0 <= int(self.SERVER_PREFERENCE) <= 255: error_exit(f"Server preference '{self.SERVER_PREFERENCE}' is invalid") # check information refresh time if not self.INFORMATION_REFRESH_TIME.isdigit(): error_exit(f"{msg_prefix} Information refresh time '{self.INFORMATION_REFRESH_TIME}' is invalid.") elif not 0 < int(self.INFORMATION_REFRESH_TIME): error_exit(f"{msg_prefix} Information refresh time preference " f"'{self.INFORMATION_REFRESH_TIME}' is pretty short.") # check validity of configuration source if self.STORE_CONFIG not in ['mysql', 'postgresql', 'sqlite', 'file', False]: error_exit(f"{msg_prefix} Unknown config storage type '{self.STORAGE}' is invalid.") # check which type of storage to use for leases if self.STORE_VOLATILE not in ['mysql', 'postgresql', 'sqlite']: error_exit(f"{msg_prefix} Unknown volatile storage type '{self.VOLATILE}' is invalid.") # check if database for config and volatile is equal - if any if self.STORE_CONFIG in ['mysql', 'postgresql'] and self.STORE_VOLATILE in ['mysql', 'postgresql']: if self.STORE_CONFIG != self.STORE_VOLATILE: error_exit(f"{msg_prefix} Storage types for database access have to be equal - " f"'{self.STORE_CONFIG}' != '{self.STORE_VOLATILE}'.") # check validity of config file if self.STORE_CONFIG == 'file': if os.path.exists(self.STORE_FILE_CONFIG): if not (os.path.isfile(self.STORE_FILE_CONFIG) or os.path.islink(self.STORE_FILE_CONFIG)): error_exit(f"{msg_prefix} Config file '{self.STORE_FILE_CONFIG}' is no file or link.") else: error_exit(f"{msg_prefix} Config file '{self.STORE_FILE_CONFIG}' does not exist.") # check validity of config db sqlite file if self.STORE_CONFIG == 'sqlite': if os.path.exists(self.STORE_SQLITE_CONFIG): if not (os.path.isfile(self.STORE_SQLITE_CONFIG) or os.path.islink(self.STORE_SQLITE_CONFIG)): error_exit(f"{msg_prefix} SQLite file '{self.STORE_SQLITE_CONFIG}' is no file or link.") else: error_exit(f"{msg_prefix} SQLite file '{self.STORE_SQLITE_CONFIG}' does not exist.") # check validity of volatile db sqlite file if self.STORE_VOLATILE == 'sqlite': if os.path.exists(self.STORE_SQLITE_VOLATILE): if not (os.path.isfile(self.STORE_SQLITE_VOLATILE) or os.path.islink(self.STORE_SQLITE_VOLATILE)): error_exit(f"{msg_prefix} SQLite file '{self.STORE_SQLITE_VOLATILE}' is no file or link.") else: error_exit(f"{msg_prefix} SQLite file '{self.STORE_SQLITE_VOLATILE}' does not exist.") # check log validity if self.LOG: if self.LOG_FILE != '': if os.path.exists(self.LOG_FILE): if not (os.path.isfile(self.LOG_FILE) or os.path.islink(self.LOG_FILE)): error_exit(f"{msg_prefix} Logfile '{self.LOG_FILE}' is no file or link.") else: error_exit(f"{msg_prefix} Logfile '{self.LOG_FILE}' does not exist.") # check ownership of logfile stat_result = os.stat(self.LOG_FILE) if not stat_result.st_uid == pwd.getpwnam(self.USER).pw_uid: error_exit(f"{msg_prefix} User {self.USER} is not owner of logfile '{self.LOG_FILE}'.") if not stat_result.st_gid == grp.getgrnam(self.GROUP).gr_gid: error_exit(f"{msg_prefix} Group {self.GROUP} is not owner of logfile '{self.LOG_FILE}'.") if self.LOG_LEVEL not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: error_exit(f"Log level '{self.LOG_LEVEL}' is invalid") if self.LOG_SYSLOG: if self.LOG_SYSLOG_FACILITY not in ['KERN', 'USER', 'MAIL', 'DAEMON', 'AUTH', 'LPR', 'NEWS', 'UUCP', 'CRON', 'SYSLOG', 'LOCAL0', 'LOCAL1', 'LOCAL2', 'LOCAL3', 'LOCAL4', 'LOCAL5', 'LOCAL6', 'LOCAL7']: error_exit(f"{msg_prefix} Syslog facility '{self.LOG_SYSLOG_FACILITY}' is invalid.") if self.LOG_SYSLOG_DESTINATION.startswith('/'): stat_result = os.stat(self.LOG_SYSLOG_DESTINATION) if not stat.S_ISSOCK(stat_result.st_mode): error_exit( f"{msg_prefix} Syslog destination '{self.LOG_SYSLOG_DESTINATION}' is no socket.") elif self.LOG_SYSLOG_DESTINATION.count(':') > 0: if self.LOG_SYSLOG_DESTINATION.count(':') > 1: error_exit(f"{msg_prefix} Syslog destination '{self.LOG_SYSLOG_DESTINATION}' " f"is no valid host:port destination.") # check authentification information if not self.AUTHENTICATION_INFORMATION.isalnum(): error_exit(f"{msg_prefix} Authentification information '{self.AUTHENTICATION_INFORMATION}' " f"must be alphanumeric.") # check validity of identification attributes for i in self.IDENTIFICATION: if i not in ['mac', 'hostname', 'duid']: error_exit(f"{msg_prefix} Identification must consist of 'mac', 'hostname' and/or 'duid'.") # check validity of identification mode if self.IDENTIFICATION_MODE.strip() not in ['match_all', 'match_some']: error_exit(f"{msg_prefix} Identification mode must be one of 'match_all' or 'match_some'.") # check if request rate limit seconds are a number if not self.REQUEST_LIMIT_TIME.isdigit(): error_exit(f"{msg_prefix} Request limit time '{self.REQUEST_LIMIT_TIME}' is invalid.") # check if request rate limit count is a number if not self.REQUEST_LIMIT_COUNT.isdigit(): error_exit(f"{msg_prefix} Request limit count '{self.REQUEST_LIMIT_COUNT}' is invalid.") # check if request rate limit blacklist release time seconds are a number if not self.REQUEST_LIMIT_RELEASE_TIME.isdigit(): error_exit(f"{msg_prefix} Request limit blacklist release time " f"'{self.REQUEST_LIMIT_RELEASE_TIME}' is invalid.") # check validity of identification attributes if self.REQUEST_LIMIT_IDENTIFICATION not in ['mac', 'llip']: error_exit(f"{msg_prefix} Request limit identification must be one of 'mac' or 'llip'.") # Make integers of number strings to avoid later repeated conversion # more to come... self.REQUEST_LIMIT_TIME = int(self.REQUEST_LIMIT_TIME) self.REQUEST_LIMIT_COUNT = int(self.REQUEST_LIMIT_COUNT) self.REQUEST_LIMIT_RELEASE_TIME = int(self.REQUEST_LIMIT_RELEASE_TIME) # cruise through classes # more checks to come... for c in self.CLASSES: msg_prefix = f"Class '{c}':" if self.CLASSES[c].ANSWER not in ['normal', 'noaddress', 'none']: error_exit(f"{msg_prefix} answer type must be one of 'normal', 'noaddress' and 'none'.") # check interface if not self.IGNORE_INTERFACE: for i in self.CLASSES[c].INTERFACE: # also accept Linux VLAN and other definitions but interface must exist if i not in get_interfaces() or not re.match('^[a-z0-9_:.%-]*$', i, re.IGNORECASE): error_exit(f"{msg_prefix} Interface '{i}' is invalid.") # check advertised IA types for i in self.CLASSES[c].ADVERTISE: if i not in ['addresses', 'prefixes']: error_exit("Only 'addresses' and 'prefixes' can be advertised.") # check nameserver to be given to client for nameserver in self.CLASSES[c].NAMESERVER: try: decompress_ip6(nameserver) except Exception as err: error_exit(f"{msg_prefix} Name server address '{err}' is invalid.") # split NTP server types into possible 3 (address, multicast, FQDN) # more details about this madness are available at https://tools.ietf.org/html/rfc5908 for ntp_server in self.CLASSES[c].NTP_SERVER: try: decompress_ip6(ntp_server) # if decompressing worked it must be an address if ntp_server.lower().startswith('ff'): self.CLASSES[c].NTP_SERVER_DICT['MC'].append(ntp_server.lower()) else: self.CLASSES[c].NTP_SERVER_DICT['SRV'].append(ntp_server.lower()) except Exception as err: if re.match('^[a-z0-9.-]*$', ntp_server, re.IGNORECASE): self.CLASSES[c].NTP_SERVER_DICT['FQDN'].append(ntp_server.lower()) else: error_exit(f"{msg_prefix} NTP server address '{ntp_server}' is invalid.") # check if T1 is a number if not self.CLASSES[c].T1.isdigit(): error_exit(f"{msg_prefix} T1 '{self.CLASSES[c].T1}' is invalid.") # check if T2 is a number if not self.CLASSES[c].T2.isdigit(): error_exit(f"{msg_prefix} T2 '{self.CLASSES[c].T2}' is invalid.") # check T2 is not smaller than T1 if not int(self.CLASSES[c].T2) >= int(self.CLASSES[c].T1): error_exit(f"{msg_prefix} T2 '{self.CLASSES[c].T2}' is shorter " f"than T1 '{self.CLASSES[c].T1}' and thus invalid.") # check every single address of a class for a in self.CLASSES[c].ADDRESSES: msg_prefix = f"Class '{c}' Address type '{a}':" # test if used addresses are defined if a not in self.ADDRESSES: error_exit(f"{msg_prefix} Address type '{a}' is not defined.") # test validity of category if self.ADDRESSES[a].CATEGORY.strip() not in ['eui64', 'fixed', 'range', 'random', 'mac', 'id', 'dns']: error_exit(f"{msg_prefix} Category '{self.ADDRESSES[a].CATEGORY}' is invalid. " f"Category must be one of 'eui64', 'fixed', 'range', 'random', 'mac', 'id' and 'dns'.") # test validity of pattern - has its own error output self.ADDRESSES[a].build_prototype() # test existence of category specific variable in pattern if self.ADDRESSES[a].CATEGORY == 'range': if not re.match('^[0-9a-f]{1,4}-[0-9a-f]{1,4}$', self.ADDRESSES[a].RANGE, re.IGNORECASE): error_exit(f"{msg_prefix} Range '{self.ADDRESSES[a].RANGE}' is not valid.") if not 0 < self.ADDRESSES[a].PATTERN.count('$range$') < 2: error_exit(f"{msg_prefix} Pattern '{self.ADDRESSES[a].PATTERN.strip()}' contains wrong " f"number of '$range$' variables for category 'range'.") elif not self.ADDRESSES[a].PATTERN.endswith('$range$'): error_exit(f"{msg_prefix} Pattern '{self.ADDRESSES[a].PATTERN.strip()}' must end " f"with '$range$' variable for category 'range'.") if self.ADDRESSES[a].CATEGORY == 'mac': if not 0 < self.ADDRESSES[a].PATTERN.count('$mac$') < 2: error_exit(f"{msg_prefix} Pattern '{self.ADDRESSES[a].PATTERN.strip()}' contains wrong " f"number of '$mac$' variables for category 'mac'.") if self.ADDRESSES[a].CATEGORY == 'id': if not self.ADDRESSES[a].PATTERN.count('$id$') == 1: error_exit(f"{msg_prefix} Pattern '{self.ADDRESSES[a].PATTERN.strip()}' contains wrong " f"number of '$id$' variables for category 'id'.") if self.ADDRESSES[a].CATEGORY == 'random': if not self.ADDRESSES[a].PATTERN.count('$random64$') == 1: error_exit(f"{msg_prefix} Pattern '{self.ADDRESSES[a].PATTERN.strip()}' contains wrong " f"number of '$random64$' variables for category 'random'.") if self.ADDRESSES[a].CATEGORY == 'dns': if not len(self.NAMESERVER) > 0: error_exit("Address of category 'dns' needs a set nameserver.") # check ia_type if not self.ADDRESSES[a].IA_TYPE.strip().lower() in ['na', 'ta']: error_exit(f"{msg_prefix}: IA type '{self.ADDRESSES[a].IA_TYPE.strip()}' " f"must be one of 'na' or 'ta'.") # check if valid lifetime is a number if not self.ADDRESSES[a].VALID_LIFETIME.isdigit(): error_exit(f"{msg_prefix} Valid lifetime '{self.ADDRESSES[a].VALID_LIFETIME}' is invalid.") # check if preferred lifetime is a number if not self.ADDRESSES[a].PREFERRED_LIFETIME.isdigit(): error_exit( f"{msg_prefix} Preferred lifetime '{self.ADDRESSES[a].PREFERRED_LIFETIME}' is invalid.") # check if valid lifetime is longer than preferred lifetime if not int(self.ADDRESSES[a].VALID_LIFETIME) >= int(self.ADDRESSES[a].PREFERRED_LIFETIME): error_exit(f"{msg_prefix} Valid lifetime '{self.ADDRESSES[a].VALID_LIFETIME}' is shorter " f"than preferred lifetime '{self.ADDRESSES[a].PREFERRED_LIFETIME}' and thus invalid.") # check if T1 <= T2 <= PREFERRED_LIFETIME <= VALID_LIFETIME if not (int(self.CLASSES[c].T1) <= int(self.CLASSES[c].T2) <= int(self.ADDRESSES[a].PREFERRED_LIFETIME) <= int(self.ADDRESSES[a].VALID_LIFETIME)): error_exit(f"{msg_prefix} Time intervals T1 '{self.CLASSES[c].T1}' <= " f"T2 '{self.CLASSES[c].T2}' <= " f"preferred_lifetime '{self.ADDRESSES[a].PREFERRED_LIFETIME}' <= " f"valid_lifetime '{self.ADDRESSES[a].VALID_LIFETIME}' are wrong.") # check every single bootfile of a class for b in self.CLASSES[c].BOOTFILES: msg_prefix = f"Bootfile '{c}' BOOTFILE type '{b}':" # test if used bootfiles are defined if b not in self.BOOTFILES: error_exit(f"{msg_prefix} Bootfile type '{b}' is not defined.") # check every single prefix of a class for p in self.CLASSES[c].PREFIXES: msg_prefix = f"Class '{c}' PREFIX type '{p}':" # test if used addresses are defined if p not in self.PREFIXES: error_exit(f"{msg_prefix} Prefix type '{p}' is not defined.") # test validity of category if self.PREFIXES[p].CATEGORY.strip() not in ['range', 'id']: error_exit(f"{msg_prefix} Category 'self.PREFIXES[p].CATEGORY' is invalid. " f"Category must be 'range' or 'id'.") # test validity of pattern - has its own error output self.PREFIXES[p].build_prototype() # test existence of category specific variable in pattern if self.PREFIXES[p].CATEGORY == 'range': if not re.match('^[0-9a-f]{1,4}-[0-9a-f]{1,4}$', self.PREFIXES[p].RANGE, re.IGNORECASE): error_exit(f"{msg_prefix} Range '{self.PREFIXES[p].RANGE}' is not valid.") if not 0 < self.PREFIXES[p].PATTERN.count('$range$') < 2: error_exit(f"{msg_prefix} Pattern '{self.PREFIXES[p].PATTERN.strip()}' contains wrong " f"number of '$range$' variables for category 'range'.") elif self.PREFIXES[p].PATTERN.endswith('$range$'): error_exit(f"{msg_prefix} Pattern '{self.PREFIXES[p].PATTERN.strip()}' must not end " f"with '$range$' variable for category 'range'.") # check if valid lifetime is a number if not self.PREFIXES[p].VALID_LIFETIME.isdigit(): error_exit(f"{msg_prefix} Valid lifetime '{self.PREFIXES[p].VALID_LIFETIME}' is invalid.") # check if preferred lifetime is a number if not self.PREFIXES[p].PREFERRED_LIFETIME.isdigit(): error_exit( f"{msg_prefix} Preferred lifetime '{self.PREFIXES[p].PREFERRED_LIFETIME}' is invalid.") # check if valid lifetime is longer than preferred lifetime if not int(self.PREFIXES[p].VALID_LIFETIME) >= int(self.PREFIXES[p].PREFERRED_LIFETIME): error_exit(f"{msg_prefix} Valid lifetime '{self.PREFIXES[p].VALID_LIFETIME}' is shorter " f"than preferred lifetime '{self.PREFIXES[p].PREFERRED_LIFETIME}' and thus invalid.") # check if T1 <= T2 <= PREFERRED_LIFETIME <= VALID_LIFETIME if not (int(self.CLASSES[c].T1) <= int(self.CLASSES[c].T2) <= int(self.PREFIXES[p].PREFERRED_LIFETIME) <= int(self.PREFIXES[p].VALID_LIFETIME)): error_exit(f"{msg_prefix} Time intervals T1 '{self.CLASSES[c].T1}' <= " f"T2 '{self.CLASSES[c].T2}' <= " f"preferred_lifetime '{self.PREFIXES[p].PREFERRED_LIFETIME}' <= " f"valid_lifetime '{self.PREFIXES[p].VALID_LIFETIME}' are wrong.") # check if prefix is a valid number if not self.PREFIXES[p].LENGTH.isdigit(): error_exit(f"{msg_prefix} Prefix length '{self.PREFIXES[p].LENGTH}' is invalid.") if not 0 <= int(self.PREFIXES[p].LENGTH) <= 128: error_exit(f"{msg_prefix} Prefix length '{self.PREFIXES[p].LENGTH}' must be in range 0-128.") # cruise through bootfiles # more checks to come... for b in self.BOOTFILES: msg_prefix = f"Bootfile '{b}':" bootfile_url = self.BOOTFILES[b].BOOTFILE_URL if bootfile_url is None or bootfile_url == '': error_exit(f"{msg_prefix} Bootfile url parameter must be set and is not allowed to be empty.") class ConfigObject: """ class providing methods both for addresses and prefixes """ def build_prototype(self, pattern=None): """ build prototype of pattern for later comparison with leases """ # if called with de-$prefix$-ed pattern use it if pattern is None: prototype = self.PATTERN else: prototype = pattern # inject prefix later so jump out here now if '$prefix$' in prototype: self.PROTOTYPE = prototype return # check different client address categories - to be extended! if self.CATEGORY in ['mac', 'id', 'range', 'random']: if self.CATEGORY == 'mac': prototype = prototype.replace('$mac$', 'xxxx:xxxx:xxxx') elif self.CATEGORY == 'id': prototype = prototype.replace('$id$', 'xxxx') elif self.CATEGORY == 'random': prototype = prototype.replace('$random64$', 'xxxx:xxxx:xxxx:xxxx') elif self.CATEGORY == 'range': prototype = prototype.replace('$range$', 'xxxx') try: # build complete 'address' and ignore all the Xs (strict=False) # all X will become x prototype = decompress_ip6(prototype, strict=False) except Exception as err: error_exit(f"Address type '{self.TYPE}' address pattern '{self.PATTERN}' is not valid: {err}") self.PROTOTYPE = prototype def inject_dynamic_prefix_into_prototype(self, dynamic_prefix): """ called from main to put then known dynamic prefix into protoype """ if '$prefix$' in self.PATTERN: prefix_pattern = self.PATTERN.replace('$prefix$', dynamic_prefix) self.build_prototype(prefix_pattern) def matches_prototype(self, address): """ test if given address matches prototype and therefore this address' DNS zone information might be used only used for address types, not client instances """ match = False # compare all chars of address and prototype, if they do match or # prototype has placeholder X return finally True, otherwise stop # at the first difference and give back False for i in range(32): if self.PROTOTYPE[i] == address[i] or self.PROTOTYPE[i] == 'x': match = True else: match = False break return match class Address(ConfigObject): """ class for address definition, used for config """ def __init__(self, address=None, ia_type='na', category='random', pattern='2001:db8::$random64$', preferred_lifetime=0, valid_lifetime=0, atype='default', aclass='default', prototype='', arange='', dns_update=False, dns_zone='', dns_rev_zone='8.b.d.0.1.0.0.2.ip6.arpa', dns_ttl='0', valid=True): self.CATEGORY = category self.PATTERN = pattern self.IA_TYPE = ia_type self.PREFERRED_LIFETIME = preferred_lifetime self.VALID_LIFETIME = valid_lifetime self.ADDRESS = address self.RANGE = arange.lower() # because 'class' is a python keyword we use 'client_class' here self.CLASS = aclass # same with type self.TYPE = atype # a prototypical address to be compared with leases given by # clients - if prototype and lease address kind of match # give back the lease as valid self.PROTOTYPE = prototype # flag for updating address in DNS or not self.DNS_UPDATE = dns_update # DNS zone data self.DNS_ZONE = dns_zone.lower() self.DNS_REV_ZONE = dns_rev_zone.lower() self.DNS_TTL = dns_ttl # flag invalid addresses as invalid, valid ones as valid self.VALID = valid class Prefix(ConfigObject): """ class for delegated prefix definition """ def __init__(self, prefix=None, pattern='2001:db8:$range$::', prange='1000-1fff', category='range', length='48', preferred_lifetime=0, valid_lifetime=0, ptype='default', pclass='default', valid=True, route_link_local=False): self.PREFIX = prefix self.PATTERN = pattern self.RANGE = prange.lower() self.CATEGORY = category self.LENGTH = length self.PREFERRED_LIFETIME = preferred_lifetime self.VALID_LIFETIME = valid_lifetime self.TYPE = ptype self.CLASS = pclass self.VALID = valid self.ROUTE_LINK_LOCAL = route_link_local class Class: """ class for class definition """ def __init__(self, name=''): self.NAME = name self.ADDRESSES = list() self.PREFIXES = list() self.BOOTFILES = list() self.NAMESERVER = '' self.NTP_SERVER = '' # Auxiliary options, derived from self.NTP_SERVER self.NTP_SERVER_DICT = {'SRV': [], 'MC': [], 'FQDN': []} self.FILTER_MAC = '' self.FILTER_HOSTNAME = '' self.FILTER_DUID = '' self.IDENTIFICATION_MODE = 'match_all' # RENEW time self.T1 = 0 # REBIND time self.T2 = 0 # at which interface this class of clients is served self.INTERFACE = list() # in certain cases it might be useful not to give any address to clients, for example if only a defined group # of hosts should get IPv6 addresses and others not. They will get a 'NoAddrsAvail' handler if this option # is set to 'noaddress' or no answer at all if set to 'none' self.ANSWER = 'normal' # which IA_* should this class supply - addresses, prefixes or both? # shouldn't be an empty list because in this case the class would not make sense at all # as default only addresses will be advertised self.ADVERTISE = ['addresses'] # commands or scripts to be called for setting and removing routes for prefixes self.CALL_UP = '' self.CALL_DOWN = '' class BootFile: """ class for netboot defintion """ def __init__(self, name=''): self.NAME = name # PXE client architecture (Option 61) self.CLIENT_ARCHITECTURE = '' # PXE bootfile URL (Option 59) self.BOOTFILE_URL = '' # User class (Option 15) self.USER_CLASS = '' def generate_duid(): """ Creates a DUID for the server - needed if none exists or is given :return: """ return f'00010001{int(time.time()):08x}{uuid.getnode():012x}' # singleton-like central instance cfg = Config() dhcpy6d-1.2.3/dhcpy6d/constants.py000066400000000000000000000133701437472361000170200ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import socket import struct # DHCPv6 MESSAGE = {1: 'SOLICIT', 2: 'ADVERTISE', 3: 'REQUEST', 4: 'CONFIRM', 5: 'RENEW', 6: 'REBIND', 7: 'REPLY', 8: 'RELEASE', 9: 'DECLINE', 10: 'RECONFIGURE', 11: 'INFORMATION-REQUEST', 12: 'RELAY-FORW', 13: 'RELAY-REPL'} # see http://www.iana.org/assignments/dhcpv6-parameters/ OPTION = {1: 'CLIENTID', 2: 'SERVERID', 3: 'IA_NA', 4: 'IA_TA', 5: 'IAADDR', 6: 'ORO', 7: 'PREFERENCE', 8: 'ELAPSED_TIME', 9: 'RELAY_MSG', 11: 'AUTH', 12: 'UNICAST', 13: 'STATUS_CODE', 14: 'RAPID_COMMIT', 15: 'USER_CLASS', 16: 'VENDOR_CLASS', 17: 'VENDOR_OPTS', 18: 'INTERFACE_ID', 19: 'RECONF_MSG', 20: 'RECONF_ACCEPT', 21: 'SIP_SERVER_D', 22: 'SIP_SERVER_A', 23: 'DNS_SERVERS', 24: 'DOMAIN_LIST', 25: 'IA_PD', 26: 'IAPREFIX', 27: 'NIS_SERVERS', 28: 'NISP_SERVERS', 29: 'NIS_DOMAIN_NAME', 30: 'NISP_DOMAIN_NAME', 31: 'SNTP_SERVERS', 32: 'INFORMATION_REFRESH_TIME', 33: 'BCMCS_SERVER_D', 34: 'BCMCS_SERVER_A', 36: 'GEOCONF_CIVIC', 37: 'REMOTE_ID', 38: 'SUBSCRIBER_ID', 39: 'CLIENT_FQDN', 40: 'PANA_AGENT', 41: 'NEW_POSIX_TIMEZONE', 42: 'NEW_TZDB_TIMEZONE', 43: 'ERO', 44: 'LQ_QUERY', 45: 'CLIENT_DATA', 46: 'CLT_TIME', 47: 'LQ_RELAY_DATA', 48: 'LQ_CLIENT_LINK', 49: 'MIP6_HNINF', 50: 'MIP6_RELAY', 51: 'V6_LOST', 52: 'CAPWAP_AC_V6', 53: 'RELAY_ID', 54: 'IPv6_Address_MoS', 55: 'Pv6_FQDN_MoS', 56: 'NTP_SERVER', 57: 'V6_ACCESS_DOMAIN', 58: 'SIP_UA_CS_LIST', 59: 'BOOTFILE_URL', 60: 'OPT_BOOTFILE_PARAM', 61: 'OPTION_CLIENT_ARCH_TYPE' } STATUS = {0: 'Success', 1: 'Failure', 2: 'No Addresses available', 3: 'No Binding', 4: 'Prefix not appropriate for link', 5: 'Use Multicast', 6: 'No Prefix available'} # see https://tools.ietf.org/html/rfc4578#section-2.1 ARCHITECTURE_TYPE = {0: 'Intel x86PC', 1: 'NEC / PC98', 2: 'EFI Itanium', 3: 'DEC Alpha', 4: 'Arc x86', 5: 'Intel Lean Client', 6: 'EFI IA32', 7: 'EFI BC', 8: 'EFI Xscale', 9: 'EFI x86 - 64'} # used for NETLINK in get_neighbor_cache_linux() access by Github/vokac RTM_NEWNEIGH = 28 RTM_DELNEIGH = 29 RTM_GETNEIGH = 30 NLM_F_REQUEST = 1 # Modifiers to GET request NLM_F_ROOT = 0x100 NLM_F_MATCH = 0x200 NLM_F_DUMP = (NLM_F_ROOT | NLM_F_MATCH) # NETLINK message is always the same except header seq MSG = struct.pack('B', socket.AF_INET6) # always the same length... MSG_HEADER_LENGTH = 17 # ...type... MSG_HEADER_TYPE = RTM_GETNEIGH # ...flags. MSG_HEADER_FLAGS = (NLM_F_REQUEST | NLM_F_DUMP) NLMSG_NOOP = 0x1 # /* Nothing. */ NLMSG_ERROR = 0x2 # /* Error */ NLMSG_DONE = 0x3 # /* End of a dump */ NLMSG_OVERRUN = 0x4 # /* Data lost */ NUD_INCOMPLETE = 0x01 # state of peer NUD_REACHABLE = 0x02 NUD_STALE = 0x04 NUD_DELAY = 0x08 NUD_PROBE = 0x10 NUD_FAILED = 0x20 NUD_NOARP = 0x40 NUD_PERMANENT = 0x80 NUD_NONE = 0x00 NDA = { 0: 'NDA_UNSPEC', 1: 'NDA_DST', 2: 'NDA_LLADDR', 3: 'NDA_CACHEINFO', 4: 'NDA_PROBES', 5: 'NDA_VLAN', 6: 'NDA_PORT', 7: 'NDA_VNI', 8: 'NDA_IFINDEX', } NLMSG_ALIGNTO = 4 NLA_ALIGNTO = 4 # collect most constants in a class for easier handling by calling numeric values via class properties # at the same time still available with integer keys and string values class Constants: """ contains various categories of constants """ class Category: """ category containing constants 'reverting' the dictionary because in certain parts for example the number of an option is referred to by its name as property """ def __init__(self, category): for key, value in category.items(): self.__dict__[value.replace('-', '_').replace(' ', '_').replace('/', 'or').upper()] = key def keys(self): # return key return self.__dict__.keys() def __init__(self): self.MESSAGE = self.Category(MESSAGE) self.STATUS = self.Category(STATUS) self.OPTION = self.Category(OPTION) # needed for logging - use original dict self.MESSAGE_DICT = MESSAGE # architecture types as dict self.ARCHITECTURE_TYPE_DICT = ARCHITECTURE_TYPE # Add constants for global access CONST = Constants() dhcpy6d-1.2.3/dhcpy6d/domain.py000066400000000000000000000077271437472361000162640ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import copy from dns.resolver import (NoAnswer, NoNameservers) from .config import cfg from .globals import (dns_query_queue, resolver_query) from .helpers import decompress_ip6 from .storage import volatile_store def dns_update(transaction, action='update'): """ update DNS entries on specified nameserver at the moment this only works with Bind uses all addresses of client if they want to be dynamically updated regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: - client wants to update DNS itself -> sends 0 0 0 - client wants server to update DNS -> sends 0 0 1 - client wants no server DNS update -> sends 1 0 0 """ if transaction.client: # if allowed use client supplied hostname, otherwise that from config if cfg.DNS_USE_CLIENT_HOSTNAME: # hostname from transaction hostname = transaction.hostname else: # hostname from client info built from configuration hostname = transaction.client.hostname # if address should be updated in DNS update it for a in transaction.client.addresses: if a.DNS_UPDATE and hostname != '' and a.VALID: if cfg.DNS_IGNORE_CLIENT or transaction.dns_s == 1: # put query into DNS query queue dns_query_queue.put((action, hostname, a)) return True else: return False def dns_delete(transaction, address='', action='release'): """ delete DNS entries on specified nameserver at the moment this only works with ISC Bind """ hostname, duid, mac, iaid = volatile_store.get_host_lease(address) # if address should be updated in DNS update it # local flag to check if address should be deleted from DNS delete = False for a in list(cfg.ADDRESSES.values()): # if there is any address type which prototype matches use its DNS ZONE if a.matches_prototype(address): # kind of RCF-compliant security measure - check if hostname and DUID from transaction fits them of store if duid == transaction.duid and \ iaid == transaction.iaid: delete = True # also check MAC address if MAC counts in general - not RFCish if 'mac' in cfg.IDENTIFICATION: if not mac == transaction.mac: delete = False if hostname != '' and delete: # use address from address types as template for the real # address to be deleted from DNS dns_address = copy.copy(a) dns_address.ADDRESS = address # put query into DNS query queue dns_query_queue.put((action, hostname, dns_address)) # enough break def get_ip_from_dns(hostname): """ Get IPv6 address from DNS for address category 'dns' """ try: answer = resolver_query.query(hostname, 'AAAA') return decompress_ip6(answer.rrset.to_text().split(' ')[-1]) except NoAnswer: return False except NoNameservers: return False dhcpy6d-1.2.3/dhcpy6d/globals.py000066400000000000000000000102121437472361000164170ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import queue import platform import time import dns.resolver import dns.tsigkeyring from .config import cfg from .constants import CONST # dummy value is None for DNS related resolver_query = None resolver_update = None keyring = None # if nameserver is given create resolver if len(cfg.NAMESERVER) > 0: # default nameservers for DNS queries resolver_query = dns.resolver.Resolver() resolver_query.nameservers = cfg.NAMESERVER # RNDC Key for DNS updates from ISC Bind /etc/rndc.key if cfg.DNS_UPDATE: if cfg.DNS_USE_RNDC: keyring = dns.tsigkeyring.from_text({cfg.DNS_RNDC_KEY: cfg.DNS_RNDC_SECRET}) # resolver for DNS updates resolver_update = dns.resolver.Resolver() resolver_update.nameservers = [cfg.DNS_UPDATE_NAMESERVER] class Timer: """ global object containing time set by TimerThread """ __time = 0 def __init__(self): self.time = time.time() @property def time(self): return self.__time @time.setter def time(self, new_time): self.__time = int(new_time) # global time variable, synchronized by TimerThread timer = Timer() # dictionary to store transactions - key is transaction ID, value a transaction object transactions = {} # collected MAC addresses from clients, mapping to link local IPs collected_macs = {} # queues for queries config_query_queue = queue.Queue() config_answer_queue = queue.Queue() volatile_query_queue = queue.Queue() volatile_answer_queue = queue.Queue() # queue for dns actualization dns_query_queue = queue.Queue() # queue for executing some script to modify routes after delegating prefixes route_queue = queue.Queue() # attempt to log connections and count them to find out which clients do silly crazy brute force requests = {} requests_blacklist = {} # save OS OS = platform.system() if 'BSD' in OS: OS = 'BSD' # platform-dependant neighbor cache call # every platform has its different output # dev, llip and mac are positions of output of call # len is minimal length a line has to have to be evaluable # # update: has been different to Linux which now access neighbor cache natively NC = {'BSD': {'call': '/usr/sbin/ndp -a -n', 'dev': 2, 'llip': 0, 'mac': 1, 'len': 3}, 'Darwin': {'call': '/usr/sbin/ndp -a -n', 'dev': 2, 'llip': 0, 'mac': 1, 'len': 3} } # libc access via ctypes, needed for interface handling, get it by helpers.get_libc() # obsolete in Python 3 # LIBC = get_libc() # index IF name > number, gets filled in UDPMulticastIPv6 IF_NAME = {} # index IF number > name IF_NUMBER = {} # IA_NA, IA_TA and IA_PD Options referred here in handler IA_OPTIONS = (CONST.OPTION.IA_NA, CONST.OPTION.IA_TA, CONST.OPTION.IA_PD) # options to be ignored when logging IGNORED_LOG_OPTIONS = ['options_raw', 'client', 'client_config_dicts', 'timestamp', 'iat1', 'iat2', 'id'] # empty options string test EMPTY_OPTIONS = [None, False, '', []] # dummy IAID for transactions DUMMY_IAID = '00000000' # dummy MAC for transactions DUMMY_MAC = '00:00:00:00:00:00' # store # because of thread trouble there should not be too much db connections at once # so we need to use the queryqueue way - subject to change # source of configuration of hosts # use client configuration only if needed config_store = volatile_store = None dhcpy6d-1.2.3/dhcpy6d/handler.py000066400000000000000000000746411437472361000164310ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import binascii from copy import deepcopy import socket import socketserver import sys import traceback from .client import Client from .config import cfg from .constants import CONST from .domain import (dns_delete, dns_update) from .globals import (collected_macs, DUMMY_MAC, IA_OPTIONS, requests, requests_blacklist, timer, transactions) from .helpers import (build_option, colonify_ip6, decompress_ip6, LOCALHOST, LOCALHOST_INTERFACES) from .log import log from .macs import collect_macs from .options import OPTIONS from .route import modify_route from .storage import (config_store, volatile_store) from .transaction import Transaction class Request: """ to be stored in requests dictionary to log client requests to be able to find brute force clients """ def __init__(self, client): self.client = client self.count = 1 self.timestamp = timer.time class RequestHandler(socketserver.DatagramRequestHandler): """ manage all incoming datagrams, builds clients from config and previous leases """ # empty dummy handler response = '' # most messages are no locally generated control messages is_control_message = False def handle(self): """ request handling happens here """ # empty dummy handler self.response = '' # raw address+interface, used for requests monitoring client_address = deepcopy(self.client_address[0].split('%')[0]) try: interface = socket.if_indextoname(self.client_address[3]) except OSError: # the interface index is 0 if sent to localhost - # even if 'lo' has the index 1 interface = '' # avoid processing requests of unknown clients which cannot be found in the neighbor cache table # only makes sense if classes are not ignored and thus the neighbor cache is used if not cfg.IGNORE_MAC and cfg.IGNORE_UNKNOWN_CLIENTS: if client_address in requests_blacklist: return False # check if we are limiting requests if cfg.REQUEST_LIMIT: if cfg.REQUEST_LIMIT_IDENTIFICATION == 'llip': # avoid further processing if client is known to be bad if client_address in requests_blacklist: return False # add client to requests tracker if not known, otherwise raise counter if client_address not in requests: requests[client_address] = Request(client_address) else: requests[client_address].count += 1 # otherwise a MAC address else: llip = decompress_ip6(client_address) if llip in collected_macs: mac = deepcopy(collected_macs[llip].mac) if mac in requests_blacklist: return False # add client to requests tracker if not known, otherwise raise counter if mac not in requests: requests[mac] = Request(mac) else: requests[mac].count += 1 del llip try: # convert raw message into ascii-bytes raw_bytes = binascii.hexlify(self.request[0]).decode() # local connection is a control message # for BSD there might be different localhost addresses if client_address == LOCALHOST and interface in LOCALHOST_INTERFACES: self.is_control_message = True # do nothing if interface is not configured if interface not in cfg.INTERFACE and not self.is_control_message: return False # bad or too short message is thrown away if not len(raw_bytes) > 8: pass elif self.is_control_message: self.control_message(raw_bytes) else: message_type = int(raw_bytes[0:2], 16) transaction_id = raw_bytes[2:8] raw_bytes_options = raw_bytes[8:] options = {} while len(raw_bytes_options) > 0: # option type and length are 2 bytes each option = int(raw_bytes_options[0:4], 16) length = int(raw_bytes_options[4:8], 16) # *2 because 2 bytes make 1 char value = raw_bytes_options[8:8 + length * 2] # Microsoft behaves a little bit different than the other # clients - in RENEW and REBIND request multiple addresses of an # IAID are not requested all in one option type 3 but # come in several options of type 3 what leads to some confusion # so allow all IA_OPTION (NA, TA, PD) to exist more than once # and create them as list if option not in IA_OPTIONS: options[option] = value else: if option in options: # if options list already exists append value options[option].append(value) else: # otherwise create list and put value in options[option] = [value] # cut off bytes worked on raw_bytes_options = raw_bytes_options[8 + length * 2:] # only valid messages will be processed if message_type in CONST.MESSAGE_DICT: client_llip = decompress_ip6(client_address) # 2. create Transaction object if not yet done identifier = client_llip + transaction_id if identifier not in transactions: transactions[identifier] = Transaction(transaction_id, client_llip, interface, message_type, options) # shortcut to transactions[identifier] transaction = transactions[identifier] # add client MAC address to transaction object if transaction.client_llip in collected_macs and not cfg.IGNORE_MAC: transaction.mac = collected_macs[transaction.client_llip].mac else: # shortcut to transactions[identifier] transaction = transactions[identifier] transaction.timestamp = timer.time transaction.last_message_received_type = message_type # log incoming messages log.info(f'{CONST.MESSAGE_DICT[message_type]} | ' f'transaction: {transaction.id}{transaction.get_options_string()}') # 3. answer requests # check if client sent a valid DUID (alphanumeric) if transaction.duid.isalnum(): # if request was not addressed to multicast do nothing but logging if transaction.interface == '': log.info(f'transaction: {transaction.id} | Multicast necessary ' f'but message came from {colonify_ip6(transaction.client_llip)}') # reset transaction counter transaction.counter = 0 else: # client will get answer if its LLIP & MAC is known if transaction.client_llip not in collected_macs: if not cfg.IGNORE_MAC: # complete MAC collection - will make most sense on Linux # and its native neighborcache access collect_macs(timer.time) # when still no trace of the client in neighbor cache then send silly signal back if transaction.client_llip not in collected_macs: # if not known send status code option failure to get # LLIP/MAC mapping from neighbor cache # status code 'Success' sounds silly but works best self.build_response(CONST.MESSAGE.REPLY, transaction, [CONST.OPTION.STATUS_CODE], status=CONST.STATUS.SUCCESS) # complete MAC collection collect_macs(timer.time) # if client cannot be found in collected MACs if transaction.client_llip not in collected_macs: if cfg.IGNORE_UNKNOWN_CLIENTS and client_address in requests: if requests[client_address].count > 1: requests_blacklist[client_address] = Request(client_address) log.info(f"Blacklisting unknown client {client_address}") return False # try to add client MAC address to transaction object try: transaction.mac = collected_macs[transaction.client_llip].mac except KeyError: # MAC not yet found :-( if cfg.LOG_MAC_LLIP: log.info(f'transaction: {transaction.id} | mac address for ' f'llip {colonify_ip6(transaction.client_llip)} unknown') # if finally there is some info about the client or MACs # it plays no role try to answer the request if transaction.client_llip in collected_macs or cfg.IGNORE_MAC: if not cfg.IGNORE_MAC: if transaction.mac == DUMMY_MAC: transaction.mac = collected_macs[transaction.client_llip].mac # ADVERTISE # if last request was a SOLICIT send an ADVERTISE (type 2) back if transaction.last_message_received_type == CONST.MESSAGE.SOLICIT \ and not transaction.rapid_commit: # preference option (7) is for free self.build_response(CONST.MESSAGE.ADVERTISE, transaction, transaction.ia_options + [CONST.OPTION.PREFERENCE] + transaction.options_request) # store leases for addresses and lock advertised address volatile_store.store(deepcopy(transaction), timer.time) # REQUEST # if last request was a REQUEST (type 3) send a REPLY (type 7) back elif transaction.last_message_received_type == CONST.MESSAGE.REQUEST or \ (transaction.last_message_received_type == CONST.MESSAGE.SOLICIT and transaction.rapid_commit): # preference option (7) is for free # if RapidCommit was set give it back if not transaction.rapid_commit: self.build_response(CONST.MESSAGE.REPLY, transaction, transaction.ia_options + [CONST.OPTION.PREFERENCE] + transaction.options_request) else: self.build_response(CONST.MESSAGE.REPLY, transaction, transaction.ia_options + [CONST.OPTION.PREFERENCE] + [CONST.OPTION.RAPID_COMMIT] + transaction.options_request) # store leases for addresses volatile_store.store(deepcopy(transaction), timer.time) # run external script for setting a route to the delegated prefix if CONST.OPTION.IA_PD in transaction.ia_options: modify_route(transaction, 'up') if cfg.DNS_UPDATE: dns_update(transaction) # CONFIRM # if last request was a CONFIRM (4) send a REPLY (type 7) back # Due to problems with different clients they will get a not-available-reply # but the next ADVERTISE will offer them the last known and still active # lease. This makes sense in case of fixed MAC-based, addresses, ranges and # ID-based addresses, Random addresses will be recalculated elif transaction.last_message_received_type == CONST.MESSAGE.CONFIRM: # the RFC 3315 is a little bit confusing regarding CONFIRM # messages so it won't hurt to simply let the client # solicit addresses again via answering 'NotOnLink' # thus client is forced in every case to solicit a new address which # might as well be the old one or a new if prefix has changed self.build_response(CONST.MESSAGE.REPLY, transaction, [CONST.OPTION.STATUS_CODE], status=CONST.STATUS.PREFIX_NOT_APPROPRIATE_FOR_LINK) # RENEW # if last request was a RENEW (type 5) send a REPLY (type 7) back elif transaction.last_message_received_type == CONST.MESSAGE.RENEW: self.build_response(CONST.MESSAGE.REPLY, transaction, transaction.ia_options + [CONST.OPTION.PREFERENCE] + transaction.options_request) # store leases for addresses volatile_store.store(deepcopy(transaction), timer.time) if cfg.DNS_UPDATE: dns_update(transaction) # REBIND # if last request was a REBIND (type 6) send a REPLY (type 7) back elif transaction.last_message_received_type == CONST.MESSAGE.REBIND: self.build_response(CONST.MESSAGE.REPLY, transaction, transaction.ia_options + [CONST.OPTION.PREFERENCE] + transaction.options_request) # store leases for addresses volatile_store.store(deepcopy(transaction), timer.time) # RELEASE # if last request was a RELEASE (type 8) send a REPLY (type 7) back elif transaction.last_message_received_type == CONST.MESSAGE.RELEASE: # build client to be able to delete it from DNS if transaction.client is None: transaction.client = Client(transaction) if cfg.DNS_UPDATE: for a in transaction.addresses: dns_delete(transaction, address=a, action='release') for a in transaction.addresses: # free lease volatile_store.release_lease(a, timer.time) for p in transaction.prefixes: # free prefix - without length volatile_store.release_prefix(p.split('/')[0], timer.time) # delete route to formerly requesting client modify_route(transaction, 'down') # send status code option (type 13) with success (type 0) self.build_response(CONST.MESSAGE.REPLY, transaction, [CONST.OPTION.STATUS_CODE], status=CONST.STATUS.SUCCESS) # DECLINE # if last request was a DECLINE (type 9) send a REPLY (type 7) back elif transaction.last_message_received_type == CONST.MESSAGE.DECLINE: # maybe has to be refined - now only a status code 'NoBinding' is answered self.build_response(CONST.MESSAGE.REPLY, transaction, [CONST.OPTION.STATUS_CODE], status=CONST.STATUS.NO_BINDING) # INFORMATION-REQUEST # if last request was an INFORMATION-REQUEST (type 11) send a REPLY (type 7) back elif transaction.last_message_received_type == CONST.MESSAGE.INFORMATION_REQUEST: self.build_response(CONST.MESSAGE.REPLY, transaction, transaction.options_request) # general error - statuscode 1 'Failure' else: # send Status Code Option (type 13) with status code 'UnspecFail' self.build_response(CONST.MESSAGE.REPLY, transaction, [CONST.OPTION.STATUS_CODE], status=CONST.STATUS.FAILURE) # count requests of transaction # if there will be too much something went wrong # may be evaluated to reset the whole transaction transaction.counter += 1 except Exception as err: traceback.print_exc(file=sys.stdout) sys.stdout.flush() log.error(f'handle(): {str(err)} | caused by: {client_address} | transaction: {transaction.id}') return None def build_response(self, message_type_response, transaction, options_request, status=0): """ creates answer and puts it into self.handler arguments: message_type_response - mostly 2 or 7 transaction option_request status - mostly 0 (OK) handler will be sent by self.finish() """ try: # should be asked before any responses are built if transaction.answer == 'none': self.response = '' return None # Header # handler type + transaction id response_string = f'{message_type_response:02x}' response_string += transaction.id # these options are always useful # Option 1 client identifier response_string += build_option(CONST.OPTION.CLIENTID, transaction.duid) # Option 2 server identifier response_string += build_option(CONST.OPTION.SERVERID, cfg.SERVERDUID) # list of options in answer to be logged options_answer = [] # build all requested options if they are handled for number in options_request: if number in OPTIONS: try: response_string_part, options_answer_part = OPTIONS[number].build(transaction=transaction, status=status) response_string += response_string_part if options_answer_part: options_answer.append(options_answer_part) except Exception: traceback.print_exc(file=sys.stdout) sys.stdout.flush() # if databases are not connected send error to client # if not (config_store.connected == volatile_store.connected == True): if not config_store.connected and not volatile_store.connected: # mark database errors - every database may add its error db_error = [] if not config_store.connected: db_error.append('config') config_store.db_connect() if not volatile_store.connected: db_error.append('volatile') volatile_store.db_connect() # create error handler - headers have to be recreated because # problems may have arisen while processing and these information # is not valid anymore # handler type + transaction id response_string = f'{CONST.MESSAGE.REPLY:02x}' response_string += transaction.id # always of interest # option 1 client identifier response_string += build_option(CONST.OPTION.CLIENTID, transaction.duid) # option 2 server identifier response_string += build_option(CONST.OPTION.SERVERID, cfg.SERVERDUID) # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string += build_option(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') log.error(f'{CONST.MESSAGE_DICT[message_type_response]} | ' f'transaction: {transaction.id} | ' f'DatabaseError: {" ".join(db_error)}') else: # log handler # if transaction.client is not None: if transaction.client: if len(transaction.client.addresses) == 0 and \ len(transaction.client.prefixes) == 0 and \ transaction.answer == 'normal' and \ transaction.last_message_received_type in [CONST.MESSAGE.SOLICIT, CONST.MESSAGE.REQUEST, CONST.MESSAGE.RENEW, CONST.MESSAGE.REBIND]: # create error handler - headers have to be recreated because # problems may have arisen while processing and these information # is not valid anymore # handler type + transaction id response_string = f'{CONST.MESSAGE.REPLY:02x}' response_string += transaction.id # always of interest # option 1 client identifier response_string += build_option(CONST.OPTION.CLIENTID, transaction.duid) # option 2 server identifier response_string += build_option(CONST.OPTION.SERVERID, cfg.SERVERDUID) # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string += build_option(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') # options in answer to be logged options_answer.append(CONST.OPTION.STATUS_CODE) # log warning message about unavailable addresses log.warning(f'REPLY | no addresses or prefixes available | ' f'transaction: {transaction.id} | ' f'client_llip: {colonify_ip6(transaction.client_llip)}') elif CONST.OPTION.IA_NA in options_request or \ CONST.OPTION.IA_TA in options_request or \ CONST.OPTION.IA_PD in options_request or \ CONST.OPTION.STATUS_CODE in options_request: options_answer = sorted(options_answer) log.info(f'{CONST.MESSAGE_DICT[message_type_response]} | ' f'transaction: {transaction.id} | ' f'options: {options_answer} {transaction.client.get_options_string()}') else: print(options_request) log.info('what else should I do?') else: options_answer = sorted(options_answer) log.info(f'{CONST.MESSAGE_DICT[message_type_response]} | ' f'transaction: {transaction.id} | ' f'options: {options_answer}') # handler self.response = binascii.unhexlify(response_string) except Exception as err: traceback.print_exc(file=sys.stdout) sys.stdout.flush() log.error('handler: ' + str(err)) # clear any handler self.response = '' return None def finish(self): """ send handler from self.handler """ # send only if there is anything to send if cfg.REALLY_DO_IT: if len(self.response) > 0: self.socket.sendto(self.response, self.client_address) else: log.error("Nothing sent - please set 'really_do_it = yes' in config file or as command line option.") @staticmethod def control_message(raw_bytes): """ execute commands sent in by control message @staticmethod proposed by PyCharm """ control_message = binascii.unhexlify(raw_bytes) control_message_fragments = control_message.decode().split(' ') # clean message control_message_clean = list() for count in range(len(control_message_fragments)): if control_message_fragments[count] != '': control_message_clean.append(control_message_fragments[count]) command = control_message_clean[0] arguments = control_message_clean[1:] # change dynamic prefix if command == 'prefix' and len(arguments) == 1: cfg.PREFIX = arguments[0] volatile_store.store_dynamic_prefix(cfg.PREFIX) # apply dynamic prefix to addresses and prefixes for a in cfg.ADDRESSES: cfg.ADDRESSES[a].inject_dynamic_prefix_into_prototype(cfg.PREFIX) for p in cfg.PREFIXES: cfg.PREFIXES[p].inject_dynamic_prefix_into_prototype(cfg.PREFIX) log.info(f'Control message \'{" ".join(control_message_clean)}\' received') dhcpy6d-1.2.3/dhcpy6d/helpers.py000066400000000000000000000213641437472361000164500ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import (hexlify, unhexlify) import shlex import socket import sys # whitespace for options with more than one value WHITESPACE = ' ,' # define address characters once - for decompress_ipv6 ADDRESS_CHARS_STRICT = ':0123456789abcdef' ADDRESS_CHARS_NON_STRICT = ':0123456789abcdefx' # localhost LOCALHOST = '::1' LOCALHOST_LLIP = '00000000000000000000000000000001' LOCALHOST_INTERFACES = ['', 'lo', 'lo0'] class Interface: """ hold interface information interface information comes in tuple from socket.if_nameindex() """ def __init__(self, interface_tuple): self.index, self.name = interface_tuple class NeighborCacheRecord: """ object for neighbor cache entries to be returned by get_neighbor_cache_linux() and in CollectedMACs .interface is only interesting for real neighbor cache records, to be ignored for collected MACs stored in DB """ def __init__(self, llip='', mac='', interface='', now=0): self.llip = llip self.mac = mac self.interface = interface self.timestamp = now def convert_dns_to_binary(name): """ convert domain name as described in RFC 1035, 3.1 """ binary = '' domain_parts = name.split('.') for domain_part in domain_parts: binary += f'{len(domain_part):02x}' # length of Domain Name Segments binary += hexlify(domain_part.encode()).decode() # final zero size octet following RFC 1035 binary += '00' return binary def convert_binary_to_dns(binary): """ convert domain name from hex like in RFC 1035, 3.1 """ name = '' binary_parts = binary while len(binary_parts) > 0: # RFC 1035 - domain names are sequences of labels separated by length octets length = int(binary_parts[0:2], 16) # lenght*2 because 2 charse represent a byte label = unhexlify(binary_parts[2:2 + length * 2]).decode() binary_parts = binary_parts[2 + length * 2:] name += label # insert '.' if this is not the last label of FQDN # >2 because last byte is the zero byte terminator if len(binary_parts) > 2: name += '.' return str(name) def build_option(number, payload): """ glue option with payload """ # option number and length take 2 byte each so the string has to be 4 chars long option = f'{number:04x}' # option number option += f'{len(payload) // 2:04x}' # payload length, /2 because 2 chars are 1 byte option += payload return option def correct_mac(mac): """ OpenBSD shortens MAC addresses in ndp output - here they grow again """ decompressed = [f'{(int(m, 16)):02x}' for m in mac.split(':')] return ':'.join(decompressed) def colonify_mac(mac): """ return complete MAC address with colons """ if type(mac) == bytes: mac = mac.decode() return ':'.join((mac[0:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:12])) def decompress_ip6(ip6, strict=True): """ decompresses shortened IPv6 address and returns it as ':'-less 32 character string additionally allows testing for prototype address with less strict set of allowed characters """ ip6 = ip6.lower() # cache some repeated calls colon_count1 = ip6.count(':') colon_count2 = ip6.count('::') colon_count3 = ip6.count(':::') # if in strict mode there are no hex numbers and ':' something is wrong if strict: for c in ip6: if c not in ADDRESS_CHARS_STRICT: raise Exception(f'{ip6} should consist only of : 0 1 2 3 4 5 6 7 8 9 a b c d e f') else: # used for comparison of leases with address pattern - X replace the dynamic part of the address for c in ip6: if c not in ADDRESS_CHARS_NON_STRICT: 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') # nothing to do if len(ip6) == 32 and colon_count1 == 0: return ip6 # larger heaps of :: smell like something wrong if colon_count2 > 1 or colon_count3 >= 1: raise Exception(f"{ip6} has too many accumulated ':'") # less than 7 ':' but no '::' also make a bad impression if colon_count1 < 7 and colon_count2 != 1: raise Exception(f"{ip6} is missing some ':'") # replace :: with :0000:: - the last ':' will be cut of finally while ip6.count(':') < 8 and ip6.count('::') == 1: ip6 = ip6.replace('::', ':0000::') # remaining ':' will be cut off ip6 = ip6.replace('::', ':') # ':' at the beginning have to be filled up with 0000 too if ip6.startswith(':'): ip6 = '0000' + ip6 # if a segment is shorter than 4 chars the gaps get filled with zeros ip6_segments_source = ip6.split(':') ip6_segments_target = list() for s in ip6_segments_source: if len(s) > 4: raise Exception(f"{ip6} has segment with more than 4 digits") else: ip6_segments_target.append(s.zfill(4)) # return with separator (mostly '') return ''.join(ip6_segments_target) def colonify_ip6(address): """ return complete IPv6 address with colons """ if address: if type(address) == bytes: address = address.decode() return ':'.join((address[0:4], address[4:8], address[8:12], address[12:16], address[16:20], address[20:24], address[24:28], address[28:32])) else: # return 'n/a' # provoke crash to see what happens with un-addresses print('Uncolonifyable address:', address) return False def convert_prefix_inline(prefix): """ check if supplied prefix is valid and convert it """ address, length = split_prefix(prefix) address = decompress_ip6(address) return {"address": address, "length": length} def combine_prefix_length(prefix, length): """ add prefix and length to 'prefix/length' string """ return f'{prefix}/{length}' def split_prefix(prefix): """ split prefix and length from 'prefix/length' notation """ return prefix.split('/') def decompress_prefix(prefix, length): """ return prefix with decompressed address part """ return combine_prefix_length(decompress_ip6(prefix), length) def error_exit(message='An error occured.', status=1): """ exit with given error message allow prefix, especially for spitting out section of configuration errors """ sys.stderr.write(f'\n{message}\n\n') sys.exit(status) def listify_option(option): """ return any comma or space separated option as list if erroneously a list has been given return it unchanged """ if option: if type(option) == str: lex = shlex.shlex(option) lex.whitespace = WHITESPACE lex.wordchars += ':.-/' return list(lex) elif type(option) == list: return(option) else: return False else: return None def send_control_message(message): """ Send a control message to the locally running dhcpy6d daemon """ # clean message of quotations marks message = message.strip('"').encode('utf8') socket_control = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) socket_control.sendto(message, ('::1', 547)) def convert_mac_to_eui64(mac): """ Convert a MAC address to a EUI64 address """ # http://tools.ietf.org/html/rfc4291#section-2.5.1 # only ':' come in MACs from get_neighbor_cache_linux() eui64 = mac.replace(':', '') eui64 = eui64[0:6] + 'fffe' + eui64[6:] eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:] split_string = lambda x, n: [x[i:i + n] for i in range(0, len(x), n)] return ':'.join(split_string(eui64, 4)) def get_interfaces(): """ return dict full of Interface objects :return: """ interfaces = {} for interface_tuple in socket.if_nameindex(): interface = Interface(interface_tuple) interfaces[interface.name] = interface return interfaces dhcpy6d-1.2.3/dhcpy6d/log.py000066400000000000000000000051511437472361000155630ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from grp import getgrnam import logging from logging import (Formatter, getLogger, StreamHandler) from logging.handlers import (SysLogHandler, WatchedFileHandler) from os import chown from pwd import getpwnam from socket import gethostname from .config import cfg # globally available logging instace log = getLogger('dhcpy6d') if cfg.LOG: formatter = Formatter('{asctime} {name} {levelname} {message}', style='{') log.setLevel(logging.__dict__[cfg.LOG_LEVEL]) if cfg.LOG_FILE != '': chown(cfg.LOG_FILE, getpwnam(cfg.USER).pw_uid, getgrnam(cfg.GROUP).gr_gid) log_handler = WatchedFileHandler(cfg.LOG_FILE) log_handler.setFormatter(formatter) log.addHandler(log_handler) # std err console output if cfg.LOG_CONSOLE: log_handler = StreamHandler() log_handler.setFormatter(formatter) log.addHandler(log_handler) if cfg.LOG_SYSLOG: # time should be added by syslog daemon hostname = gethostname().split('.')[0] formatter = Formatter(hostname + ' {name} {levelname} {message}', style='{') # if /socket/file is given use this as address if cfg.LOG_SYSLOG_DESTINATION.startswith('/'): destination = cfg.LOG_SYSLOG_DESTINATION # if host and port are defined use them... elif cfg.LOG_SYSLOG_DESTINATION.count(':') == 1: destination = tuple(cfg.LOG_SYSLOG_DESTINATION.split(':')) # ...otherwise add port 514 to given host address else: destination = (cfg.LOG_SYSLOG_DESTINATION, 514) log_handler = SysLogHandler(address=destination, facility=SysLogHandler.__dict__['LOG_' + cfg.LOG_SYSLOG_FACILITY]) log_handler.setFormatter(formatter) log.addHandler(log_handler) dhcpy6d-1.2.3/dhcpy6d/macs.py000066400000000000000000000311101437472361000157170ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Module dhcpy6d""" from binascii import hexlify from random import randint import select import shlex import socket import struct import subprocess import sys import traceback from .config import cfg from .constants import (RTM_DELNEIGH, RTM_GETNEIGH, RTM_NEWNEIGH, NLMSG_DONE, NLMSG_ERROR, MSG_HEADER_FLAGS, MSG_HEADER_LENGTH, MSG_HEADER_TYPE, NUD_FAILED, NUD_INCOMPLETE, NUD_NOARP, NDA, NLA_ALIGNTO, NLMSG_ALIGNTO) from .globals import (collected_macs, IF_NUMBER, NC, OS, timer) from .helpers import (colonify_ip6, colonify_mac, correct_mac, decompress_ip6, NeighborCacheRecord) from .log import log from .storage import volatile_store def get_neighbor_cache_linux(if_number, now): """ imported version of https://github.com/vokac/dhcpy6d https://github.com/vokac/dhcpy6d/commit/bd34d3efb18ba6016a2b3afea0b6a3fcdfb524a4 Thanks for donating! """ # open raw NETLINK socket # NETLINK_ROUTE has neighbor cache information too s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE) # PID 0 means AUTOPID, let socket choose s.bind((0, 0)) pid, groups = s.getsockname() # random sequence for NETLINK access seq = randint(0, pow(2, 31)) # netlink message header (struct nlmsghdr) msg_header = struct.pack('IHHII', MSG_HEADER_LENGTH, MSG_HEADER_TYPE, MSG_HEADER_FLAGS, seq, pid) # NETLINK message is always the same except header seq (struct ndmsg) msg = struct.pack('B', socket.AF_INET6) # send message with header s.send(msg_header + msg) # read all data from socket answer = b'' while True: r, w, e = select.select([s], [], [], 0.) if s not in r: break # no more data answer += s.recv(16384) result = {} curr_pos = 0 answer_pos = 0 answer_len = len(answer) nlmsghdr_fmt = 'IHHII' # struct nlmsghdr nlattr_fmt = 'HH' # struct nlattr ndmsg_fmt = 'BBHiHBB' # struct ndmsg nlmsg_header_len = (struct.calcsize(nlmsghdr_fmt) + NLMSG_ALIGNTO - 1) & ~(NLMSG_ALIGNTO - 1) # alignment to 4 nla_header_len = (struct.calcsize(nlattr_fmt) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1) # alignment to 4 # parse netlink answer to RTM_GETNEIGH try: while answer_pos < answer_len: curr_pos = answer_pos nlmsg_len, nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid = struct.unpack_from(f'<{nlmsghdr_fmt}', answer, answer_pos) # basic safety checks for received data (imitates NLMSG_OK) if nlmsg_len < struct.calcsize(f'<{nlmsghdr_fmt}'): log.warn('broken data from netlink (position {0}, nlmsg_len {1}): ' 'nlmsg_len is smaller than structure size'.format(answer_pos, nlmsg_len)) break if answer_len - answer_pos < struct.calcsize(f'<{nlmsghdr_fmt}'): log.warn(f'broken data from netlink (position {answer_pos}, length avail {answer_len - answer_pos}): ' 'received data size is smaller than structure size') break if answer_len - answer_pos < nlmsg_len: log.warn(f'broken data from netlink (position {answer_pos}, length avail {answer_len - answer_pos}): ' 'received dcolonify_ata size is smaller than nlmsg_len') break if pid != nlmsg_pid or seq != nlmsg_seq: log.warn(f'broken data from netlink (position {answer_pos}, length avail {answer_len - answer_pos}): ' f'invalid seq ({seq} x {nlmsg_seq}) or pid ({pid} x {nlmsg_pid})') break # data for this Routing/device hook record nlmsg_data = answer[answer_pos + nlmsg_header_len:answer_pos + nlmsg_len] if nlmsg_type == NLMSG_DONE: break if nlmsg_type == NLMSG_ERROR: nlmsgerr_error, nlmsgerr_len, nlmsgerr_type, nlmsgerr_flags, nlmsgerr_seq, nlmsgerr_pid = \ struct.unpack_from('= NC[OS]['len']: # get rid of %interface frags[NC[OS]['llip']] = decompress_ip6(frags[NC[OS]['llip']].split('%')[0]) if frags[NC[OS]['mac']] == '(incomplete)': continue # correct maybe shortened MAC frags[NC[OS]['mac']] = correct_mac(frags[NC[OS]['mac']]) # put non yet existing LLIPs into dictionary - if they have MACs if not frags[NC[OS]['llip']] in collected_macs and \ frags[NC[OS]['llip']].lower().startswith('fe80') and \ ':' in frags[NC[OS]['mac']]: collected_macs[frags[NC[OS]['llip']]] = NeighborCacheRecord(llip=frags[NC[OS]['llip']], mac=frags[NC[OS]['mac']], interface=frags[NC[OS]['dev']], now=now) if cfg.LOG_MAC_LLIP: log.info(f"collected mac {frags[NC[OS]['mac']]} for " f"llip {colonify_ip6(frags[NC[OS]['llip']])}") volatile_store.store_mac_llip(frags[NC[OS]['mac']], frags[NC[OS]['llip']], timer.time) except Exception as err: traceback.print_exc(file=sys.stdout) sys.stdout.flush() log.error('collect_macs(): ' + str(err)) dhcpy6d-1.2.3/dhcpy6d/options/000077500000000000000000000000001437472361000161215ustar00rootroot00000000000000dhcpy6d-1.2.3/dhcpy6d/options/__init__.py000066400000000000000000000051071437472361000202350ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import importlib.util import pathlib import re class OptionTemplate: """ Template to be used by derived options - default and custom ones """ number = 0 def __init__(self, number): self.number = number def build(self, **kwargs): """ to be filled with life by every single option every option has its special treatment of input and output data per request return default dummy values """ return '', False def initialize(self, **kwargs): """ to be filled with life by every single option every transaction has the opportunity to add options, depending on request """ pass @staticmethod def convert_to_string(number, payload): """ glue option number with payload """ # option number and length take 2 byte each so the string has to be 4 chars long option_string = f'{number:04x}' # option number option_string += f'{(len(payload)//2):04x}' # payload length, /2 because 2 chars are 1 byte option_string += payload return option_string # globally available options OPTIONS = {} options_path = pathlib.Path(__file__).parent pattern = re.compile('option_[0-9]{1,3}$') # get all option files in path and put them into options dict for path in options_path.glob('option_*.py'): # get rid of ".py" because this suffix won't be in option dict anyway name = path.name.rstrip(path.suffix) if re.match(pattern, name): # load option module spec = importlib.util.spec_from_file_location(name, path) option = importlib.util.module_from_spec(spec) spec.loader.exec_module(option) number = int(name.split('_')[1]) # add to global options constant OPTIONS[number] = option.Option(number) dhcpy6d-1.2.3/dhcpy6d/options/option_1.py000066400000000000000000000027651437472361000202350ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 1 Client Identifier Option """ def initialize(self, transaction=None, option=None, **kwargs): transaction.duid = option # See https://github.com/HenriWahl/dhcpy6d/issues/25 and DUID type is not used at all so just remove it # self.DUIDType = int(options[1][0:4], 16) # # DUID-EN can be retrieved from DUID # if self.DUIDType == 2: # # some HP printers seem to produce pretty bad requests, thus some cleaning is necessary # # e.g. '1 1 1 00020000000b0026b1f72a49' instead of '00020000000b0026b1f72a49' # self.DUID_EN = int(options[1].split(' ')[-1][4:12], 16) dhcpy6d-1.2.3/dhcpy6d/options/option_12.py000066400000000000000000000023241437472361000203060ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from socket import (AF_INET6, inet_pton) from dhcpy6d.config import cfg from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 12 Server Unicast Option """ def build(self, **kwargs): response_string_part = self.convert_to_string(self.number, hexlify(inet_pton(AF_INET6, cfg.ADDRESS)).decode()) return response_string_part, self.number dhcpy6d-1.2.3/dhcpy6d/options/option_13.py000066400000000000000000000021471437472361000203120ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 13 Status Code Option - statuscode is taken from dictionary """ def build(self, status=None, **kwargs): response_string_part = self.convert_to_string(self.number, f'{status:04x}') return response_string_part, self.number dhcpy6d-1.2.3/dhcpy6d/options/option_14.py000066400000000000000000000024161437472361000203120ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 14 Rapid Commit Option - necessary for REPLY to SOLICIT message with Rapid Commit """ def build(self, **kwargs): # no real content - just the existence of this option makes it work response_string_part = self.convert_to_string(self.number, '') return response_string_part, self.number def initialize(self, transaction=None, **kwargs): transaction.rapid_commit = True dhcpy6d-1.2.3/dhcpy6d/options/option_15.py000066400000000000000000000022001437472361000203020ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import unhexlify from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 15 User Class """ def initialize(self, transaction=None, option=None, **kwargs): # raw user class aka option is prefixed with null byte (00 in hex) and eot (04 in hex) transaction.user_class = unhexlify(option[4:]) dhcpy6d-1.2.3/dhcpy6d/options/option_16.py000066400000000000000000000021571437472361000203160ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import unhexlify from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 16 Vendor Class """ def initialize(self, transaction=None, option=None, **kwargs): transaction.vendor_class_en = int(option[0:8], 16) transaction.vendor_class_data = unhexlify(option[12:]).decode() dhcpy6d-1.2.3/dhcpy6d/options/option_23.py000066400000000000000000000043321437472361000203110ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from socket import (AF_INET6, inet_pton) from dhcpy6d.config import cfg from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 23 DNS recursive name server """ def build(self, transaction=None, **kwargs): # dummy empty defaults response_string_part = '' options_answer_part = None # should not be necessary to check if transactions.client exists but there are # crazy clients out in the wild which might become silent this way if transaction.client: if len(cfg.CLASSES[transaction.client.client_class].NAMESERVER) > 0: nameserver = b'' for ns in cfg.CLASSES[transaction.client.client_class].NAMESERVER: nameserver += inet_pton(AF_INET6, ns) response_string_part = self.convert_to_string(self.number, hexlify(nameserver).decode()) options_answer_part = self.number elif len(cfg.NAMESERVER) > 0: # in case several nameservers are given convert them all and add them nameserver = b'' for ns in cfg.NAMESERVER: nameserver += inet_pton(AF_INET6, ns) response_string_part = self.convert_to_string(self.number, hexlify(nameserver).decode()) options_answer_part = self.number return response_string_part, options_answer_part dhcpy6d-1.2.3/dhcpy6d/options/option_24.py000066400000000000000000000024521437472361000203130ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.config import cfg from dhcpy6d.helpers import convert_dns_to_binary from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 24 Domain Search List """ def build(self, **kwargs): converted_domain_search_list = '' for d in cfg.DOMAIN_SEARCH_LIST: converted_domain_search_list += convert_dns_to_binary(d) response_string_part = self.convert_to_string(self.number, converted_domain_search_list) return response_string_part, self.number dhcpy6d-1.2.3/dhcpy6d/options/option_25.py000066400000000000000000000202731437472361000203150ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from socket import (AF_INET6, inet_pton) from dhcpy6d import collected_macs from dhcpy6d.client import Client from dhcpy6d.config import cfg from dhcpy6d.constants import CONST from dhcpy6d.helpers import (colonify_ip6, combine_prefix_length) from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 25 Prefix Delegation """ def build(self, transaction=None, **kwargs): # dummy empty defaults response_string_part = '' options_answer_part = None # check if MAC of LLIP is really known if transaction.client_llip in collected_macs or cfg.IGNORE_MAC: # collect client information if transaction.client is None: transaction.client = Client(transaction) # Only if prefixes are provided if 'prefixes' in cfg.CLASSES[transaction.client.client_class].ADVERTISE: # check if only a short NoPrefixAvail answer or none at all is to be returned if not transaction.answer == 'normal': if transaction.answer == 'noprefix': # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') # clean client prefixes which not be deployed anyway transaction.client.prefixes[:] = [] # options in answer to be logged options_answer_part = self.number else: # if client could not be built because of database problems send # status message back if transaction.client: # embed option 26 into option 25 - several if necessary ia_prefixes = '' try: for prefix in transaction.client.prefixes: ipv6_prefix = hexlify(inet_pton(AF_INET6, colonify_ip6(prefix.PREFIX))).decode() if prefix.VALID: preferred_lifetime = f'{int(prefix.PREFERRED_LIFETIME):08x}' valid_lifetime = f'{int(prefix.VALID_LIFETIME):08x}' else: preferred_lifetime = f'{0:08x}' valid_lifetime = f'{0:08x}' length = f'{int(prefix.LENGTH):02x}' ia_prefixes += self.convert_to_string(CONST.OPTION.IAPREFIX, preferred_lifetime + valid_lifetime + length + ipv6_prefix) if transaction.client.client_class != '': t1 = f'{int(cfg.CLASSES[transaction.client.client_class].T1):08x}' t2 = f'{int(cfg.CLASSES[transaction.client.client_class].T2):08x}' else: t1 = f'{int(cfg.T1):08x}' t2 = f'{int(cfg.T2):08x}' # even if there are no prefixes server has to deliver an empty PD response_string_part = self.convert_to_string(self.number, transaction.iaid + t1 + t2 + ia_prefixes) # if no prefixes available a NoPrefixAvail status code has to be sent if ia_prefixes == '': # REBIND not possible if transaction.last_message_received_type == CONST.MESSAGE.REBIND: # Option 13 Status Code Option - statuscode is 3: 'NoBinding' response_string_part += self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_BINDING:04x}') else: # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' response_string_part += self.convert_to_string( # break because line too long CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') # options in answer to be logged options_answer_part = self.number except Exception as err: print(err) # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') # options in answer to be logged options_answer_part = self.number else: # Option 13 Status Code Option - statuscode is 6: 'No Prefix available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_PREFIX_AVAILABLE:04x}') # options in answer to be logged options_answer_part = self.number return response_string_part, options_answer_part def initialize(self, transaction=None, option=None, **kwargs): for payload in option: # iaid t1 t2 ia_prefix opt_length preferred validlt length prefix # 00000001 ffffffff ffffffff 001a 0019 00000e10 00001518 30 fd66123400.... # 8 16 24 28 32 40 48 50 82 transaction.iaid = payload[0:8] transaction.iat1 = int(payload[8:16], 16) transaction.iat2 = int(payload[16:24], 16) # Prefixes given by client if any for p in range(len(payload[32:])//50): prefix = payload[50:][(p*58):(p*58)+32] length = int(payload[48:][(p*58):(p*58)+2], 16) prefix_combined = combine_prefix_length(prefix, length) # in case a prefix is asked for twice by one host ignore the twin if prefix_combined not in transaction.prefixes: transaction.prefixes.append(prefix_combined) del(prefix, length, prefix_combined) transaction.ia_options.append(CONST.OPTION.IA_PD) dhcpy6d-1.2.3/dhcpy6d/options/option_3.py000066400000000000000000000163401437472361000202310ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from socket import (AF_INET6, inet_pton) from dhcpy6d import collected_macs from dhcpy6d.client import Client from dhcpy6d.config import cfg from dhcpy6d.constants import CONST from dhcpy6d.helpers import colonify_ip6 from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 3 + 5 Identity Association for Non-temporary Address """ def build(self, transaction=None, **kwargs): # dummy empty defaults response_string_part = '' options_answer_part = None # check if MAC of LLIP is really known if transaction.client_llip in collected_macs or cfg.IGNORE_MAC: # collect client information if transaction.client is None: transaction.client = Client(transaction) if 'addresses' in cfg.CLASSES[transaction.client.client_class].ADVERTISE and \ CONST.OPTION.IA_NA in transaction.ia_options: # check if only a short NoAddrAvail answer or none at all is to be returned if not transaction.answer == 'normal': if transaction.answer == 'noaddress': # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') # clean client addresses which not be deployed anyway transaction.client.addresses[:] = [] # options in answer to be logged options_answer_part = CONST.OPTION.STATUS_CODE else: # if client could not be built because of database problems send # status message back if transaction.client: # embed option 5 into option 3 - several if necessary ia_addresses = '' try: for address in transaction.client.addresses: if address.IA_TYPE == 'na': ipv6_address = hexlify(inet_pton(AF_INET6, colonify_ip6(address.ADDRESS))).decode() # if a transaction consists of too many requests from client - # - might be caused by going wild Windows clients - # reset all addresses with lifetime 0 # lets start with maximal transaction count of 10 if transaction.counter < 10: preferred_lifetime = f'{int(address.PREFERRED_LIFETIME):08x}' valid_lifetime = f'{int(address.VALID_LIFETIME):08x}' else: preferred_lifetime = '00000000' valid_lifetime = '00000000' ia_addresses += self.convert_to_string(CONST.OPTION.IAADDR, ipv6_address + preferred_lifetime + valid_lifetime) if ia_addresses != '': # # todo: default clients sometimes seem to have class '' # if transaction.client.client_class != '': t1 = f'{int(cfg.CLASSES[transaction.client.client_class].T1):08x}' t2 = f'{int(cfg.CLASSES[transaction.client.client_class].T2):08x}' else: t1 = f'{int(cfg.T1):08x}' t2 = f'{int(cfg.T2):08x}' response_string_part = self.convert_to_string(CONST.OPTION.IA_NA, transaction.iaid + t1 + t2 + ia_addresses) # options in answer to be logged options_answer_part = CONST.OPTION.IA_NA except: # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') # options in answer to be logged options_answer_part = CONST.OPTION.STATUS_CODE else: # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') # options in answer to be logged options_answer_part = CONST.OPTION.STATUS_CODE return response_string_part, options_answer_part def initialize(self, transaction=None, option=None, **kwargs): """ IA NA addresses of client """ for payload in option: transaction.iaid = payload[0:8] transaction.iat1 = int(payload[8:16], 16) transaction.iat2 = int(payload[16:24], 16) # addresses given by client if any for a in range(len(payload[32:]) // 44): address = payload[32:][(a * 56):(a * 56) + 32] # in case an address is asked for twice by one host ignore the twin # sometimes address seems to be EMPTY??? if address and colonify_ip6(address) and address not in transaction.addresses: transaction.addresses.append(address) transaction.ia_options.append(CONST.OPTION.IA_NA) dhcpy6d-1.2.3/dhcpy6d/options/option_31.py000066400000000000000000000027141437472361000203120ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from socket import (AF_INET6, inet_pton) from dhcpy6d.config import cfg from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 31 SNTP Servers """ def build(self, **kwargs): # dummy empty return value response_string_part = '' if cfg.SNTP_SERVERS != '': sntp_servers = b'' for s in cfg.SNTP_SERVERS: sntp_server = inet_pton(AF_INET6, s) sntp_servers += sntp_server response_string_part = self.convert_to_string(self.number, hexlify(sntp_servers).decode()) return response_string_part, self.number dhcpy6d-1.2.3/dhcpy6d/options/option_32.py000066400000000000000000000022351437472361000203110ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.config import cfg from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 32 Information Refresh Time """ def build(self, **kwargs): response_string_part = self.convert_to_string(self.number, f'{int(cfg.INFORMATION_REFRESH_TIME):08x}') # options in answer to be logged return response_string_part, self.number dhcpy6d-1.2.3/dhcpy6d/options/option_39.py000066400000000000000000000103651437472361000203230ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import re from dhcpy6d.config import cfg from dhcpy6d.helpers import (convert_binary_to_dns, convert_dns_to_binary) from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 39 FQDN http://tools.ietf.org/html/rfc4704#page-5 regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: - client wants to update DNS itself -> sends 0 0 0 - client wants server to update DNS -> sends 0 0 1 - client wants no server DNS update -> sends 1 0 0 """ def build(self, transaction=None, **kwargs): # dummy empty return value response_string_part = '' options_answer_part = None # http://tools.ietf.org/html/rfc4704#page-5 # regarding RFC 4704 5. there are 3 kinds of client behaviour for N O S: # - client wants to update DNS itself -> sends 0 0 0 # - client wants server to update DNS -> sends 0 0 1 # - client wants no server DNS update -> sends 1 0 0 if transaction.client: # flags for answer n, o, s = 0, 0, 0 # use hostname supplied by client if cfg.DNS_USE_CLIENT_HOSTNAME: hostname = transaction.hostname # use hostname from config else: hostname = transaction.client.hostname if not hostname == '': if cfg.DNS_UPDATE: # DNS update done by server - don't care what client wants if cfg.DNS_IGNORE_CLIENT: s = 1 o = 1 else: # honor the client's request for the server to initiate DNS updates if transaction.dns_s == 1: s = 1 # honor the client's request for no server-initiated DNS update elif transaction.dns_n == 1: n = 1 else: # no DNS update at all, not for server and not for client if transaction.dns_n == 1 or \ transaction.dns_s == 1: o = 1 # sum of flags nos_flags = n * 4 + o * 2 + s * 1 fqdn_binary = convert_dns_to_binary(f'{hostname}.{cfg.DOMAIN}') response_string_part = self.convert_to_string(self.number, f'{nos_flags:02x}{fqdn_binary}') else: # if no hostname given put something in and force client override fqdn_binary = convert_dns_to_binary(f'invalid-hostname.{cfg.DOMAIN}') response_string_part = self.convert_to_string(self.number, f'{3:02x}{fqdn_binary}') # options in answer to be logged options_answer_part = self.number return response_string_part, options_answer_part def initialize(self, transaction=None, option=None, **kwargs): bits = f'{int(option[1:2]):04d}' transaction.dns_n = int(bits[1]) transaction.dns_o = int(bits[2]) transaction.dns_s = int(bits[3]) # only hostname needed transaction.fqdn = convert_binary_to_dns(option[2:]) transaction.hostname = transaction.fqdn.split('.')[0].lower() # test if hostname is valid hostname_pattern = re.compile('^([a-z0-9-_]+)*$') if hostname_pattern.match(transaction.hostname) is None: transaction.hostname = '' del hostname_pattern dhcpy6d-1.2.3/dhcpy6d/options/option_4.py000066400000000000000000000147371437472361000202420ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from socket import (AF_INET6, inet_pton) from dhcpy6d import collected_macs from dhcpy6d.client import Client from dhcpy6d.config import cfg from dhcpy6d.constants import CONST from dhcpy6d.helpers import colonify_ip6 from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 4 + 5 Identity Association for Temporary Address """ def build(self, transaction=None, **kwargs): # dummy empty defaults response_string_part = '' options_answer_part = None # check if MAC of LLIP is really known if transaction.client_llip in collected_macs or cfg.IGNORE_MAC: # collect client information if transaction.client is None: transaction.client = Client(transaction) if 'addresses' in cfg.CLASSES[transaction.client.client_class].ADVERTISE and \ CONST.OPTION.IA_TA in transaction.ia_options: # check if only a short NoAddrAvail answer or none at all ist t be returned if not transaction.answer == 'normal': if transaction.answer == 'noaddress': # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') # clean client addresses which not be deployed anyway transaction.client.addresses[:] = [] # options in answer to be logged options_answer_part = CONST.OPTION.STATUS_CODE else: # if client could not be built because of database problems send # status message back if transaction.client: # embed option 5 into option 4 - several if necessary ia_addresses = '' try: for address in transaction.client.addresses: if address.IA_TYPE == 'ta': ipv6_address = hexlify(inet_pton(AF_INET6, colonify_ip6(address.ADDRESS))).decode() # if a transaction consists of too many requests from client - # - might be caused by going wild Windows clients - # reset all addresses with lifetime 0 # lets start with maximal transaction count of 10 if transaction.counter < 10: preferred_lifetime = f'{int(address.PREFERRED_LIFETIME):08x}' valid_lifetime = f'{int(address.VALID_LIFETIME):08x}' else: preferred_lifetime = '00000000' valid_lifetime = '00000000' ia_addresses += self.convert_to_string(CONST.OPTION.IAADDR, ipv6_address + preferred_lifetime + valid_lifetime) if ia_addresses != '': response_string_part = self.convert_to_string(CONST.OPTION.IA_TA, transaction.iaid + ia_addresses) # options in answer to be logged options_answer_part = CONST.OPTION.IA_TA except: # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') # options in answer to be logged options_answer_part = CONST.OPTION.STATUS_CODE else: # Option 13 Status Code Option - statuscode is 2: 'No Addresses available' response_string_part = self.convert_to_string(CONST.OPTION.STATUS_CODE, f'{CONST.STATUS.NO_ADDRESSES_AVAILABLE:04x}') # options in answer to be logged options_answer_part = CONST.OPTION.STATUS_CODE return response_string_part, options_answer_part def initialize(self, transaction=None, option=None, **kwargs): """ IA TA addresses of client """ for payload in option: transaction.iaid = payload[0:8] transaction.iat1 = int(payload[8:16], 16) transaction.iat2 = int(payload[16:24], 16) # addresses given by client if any for a in range(len(payload[32:]) // 44): address = payload[32:][(a * 56):(a * 56) + 32] # in case an address is asked for twice by one host ignore the twin # sometimes address seems to be EMPTY??? if address and colonify_ip6(address) and address not in transaction.addresses: transaction.addresses.append(address) transaction.ia_options.append(CONST.OPTION.IA_TA) dhcpy6d-1.2.3/dhcpy6d/options/option_56.py000066400000000000000000000044601437472361000203210ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from socket import (AF_INET6, inet_pton) from dhcpy6d.config import cfg from dhcpy6d.helpers import convert_dns_to_binary from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 56 NTP server https://tools.ietf.org/html/rfc5908 """ def build(self, **kwargs): # dummy empty defaults response_string_part = '' options_answer_part = None ntp_server_options = '' if len(cfg.NTP_SERVER) > 0: for ntp_server_type in list(cfg.NTP_SERVER_DICT.keys()): # ntp_server_suboption for ntp_server in cfg.NTP_SERVER_DICT[ntp_server_type]: ntp_server_suboption = '' if ntp_server_type == 'SRV': ntp_server_suboption = self.convert_to_string(1, hexlify(inet_pton(AF_INET6, ntp_server)).decode()) elif ntp_server_type == 'MC': ntp_server_suboption = self.convert_to_string(2, hexlify(inet_pton(AF_INET6, ntp_server)).decode()) elif ntp_server_type == 'FQDN': ntp_server_suboption = self.convert_to_string(3, convert_dns_to_binary(ntp_server)) ntp_server_options += ntp_server_suboption response_string_part = self.convert_to_string(self.number, ntp_server_options) # options in answer to be logged options_answer_part = self.number return response_string_part, options_answer_part dhcpy6d-1.2.3/dhcpy6d/options/option_59.py000066400000000000000000000035041437472361000203220ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from binascii import hexlify from dhcpy6d.client import Client from dhcpy6d.helpers import build_option from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 59 Network Boot https://tools.ietf.org/html/rfc5970 """ def build(self, transaction=None, **kwargs): # dummy empty defaults response_string_part = '' options_answer_part = None # build client if not done yet if transaction.client is None: transaction.client = Client(transaction) bootfiles = transaction.client.bootfiles if len(bootfiles) > 0: # TODO add preference logic bootfile_url = bootfiles[0].BOOTFILE_URL transaction.client.chosen_boot_file = bootfile_url bootfile_options = hexlify(bootfile_url).decode() response_string_part += build_option(self.number, bootfile_options) # options in answer to be logged options_answer_part = self.number return response_string_part, options_answer_part dhcpy6d-1.2.3/dhcpy6d/options/option_6.py000066400000000000000000000024161437472361000202330ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 6 Option Request Option """ def initialize(self, transaction=None, option=None, **kwargs): options_request = [] options = option[:] # cut given option (which contains all requested options) into pieces while len(options) > 0: options_request.append(int(options[0:4], 16)) options = options[4:] transaction.options_request = options_request dhcpy6d-1.2.3/dhcpy6d/options/option_61.py000066400000000000000000000027561437472361000203230ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.constants import CONST from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ 61 Client System Architecture Type """ def initialize(self, transaction=None, option=None, **kwargs): # raw client architecture is supplied as a 16-bit integer (e. g. 0007) # See https://tools.ietf.org/html/rfc4578#section-2.1 transaction.client_architecture = option # short number (0007 => 7 for dictionary usage) client_architecture_short = int(transaction.client_architecture) if client_architecture_short in CONST.ARCHITECTURE_TYPE_DICT: transaction.known_client_architecture = CONST.ARCHITECTURE_TYPE_DICT[client_architecture_short] dhcpy6d-1.2.3/dhcpy6d/options/option_7.py000066400000000000000000000021451437472361000202330ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.config import cfg from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 7 Server Preference """ def build(self, **kwargs): response_string_part = self.convert_to_string(self.number, f'{int(cfg.SERVER_PREFERENCE):02x}') return response_string_part, self.number dhcpy6d-1.2.3/dhcpy6d/options/option_8.py000066400000000000000000000021241437472361000202310ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from dhcpy6d.options import OptionTemplate class Option(OptionTemplate): """ Option 8 Elapsed Time RFC 3315: This time is expressed in hundredths of a second (10^-2 seconds). """ def initialize(self, transaction=None, option=None, **kwargs): transaction.elapsed_time = int(option[0:8], 16) dhcpy6d-1.2.3/dhcpy6d/route.py000066400000000000000000000073321437472361000161430ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from .config import cfg from .globals import (route_queue, timer) from .log import log from .storage import volatile_store class Route: """ store data of a route which should be given to an external application router is here the prefix requesting host """ def __init__(self, prefix, length, router): self.prefix = prefix self.length = length self.router = router def modify_route(transaction, mode): """ called when route has to be set - calls itself any external script or something like that """ # check if client is already set - otherwise crashes if transaction.client is not None: # only do anything if class of client has something defined to be called if (mode == 'up' and cfg.CLASSES[transaction.client.client_class].CALL_UP != '') or \ (mode == 'down' and cfg.CLASSES[transaction.client.client_class].CALL_DOWN != ''): # collect possible prefixes, lengths and router ip addresses in list routes = list() for prefix in transaction.client.prefixes: # use LinkLocal Address of client if wanted if prefix.ROUTE_LINK_LOCAL: router = transaction.client_llip else: if len(transaction.client.addresses) == 1: router = transaction.client.addresses[0].ADDRESS else: router = None log.error( 'modify_route: client needs exactly 1 address to be used as router to delegated prefix') if router is not None: routes.append(Route(prefix.PREFIX, prefix.LENGTH, router)) if mode == 'up': call = cfg.CLASSES[transaction.client.client_class].CALL_UP elif mode == 'down': call = cfg.CLASSES[transaction.client.client_class].CALL_DOWN else: # should not happen but just in case call = '' # call executables here for route in routes: route_queue.put((mode, call, route.prefix, route.length, route.router)) def manage_prefixes_routes(): """ delete or add inactive or active routes according to the prefixes in database """ volatile_store.release_free_prefixes(timer.time) inactive_prefixes = volatile_store.get_inactive_prefixes() active_prefixes = volatile_store.get_active_prefixes() for prefix in inactive_prefixes: length, router, pclass = volatile_store.get_route(prefix) if pclass in cfg.CLASSES: route_queue.put(('down', cfg.CLASSES[pclass].CALL_DOWN, prefix, length, router)) for prefix in active_prefixes: length, router, pclass = volatile_store.get_route(prefix) if pclass in cfg.CLASSES: route_queue.put(('up', cfg.CLASSES[pclass].CALL_UP, prefix, length, router)) dhcpy6d-1.2.3/dhcpy6d/storage/000077500000000000000000000000001437472361000160725ustar00rootroot00000000000000dhcpy6d-1.2.3/dhcpy6d/storage/__init__.py000066400000000000000000000072761437472361000202170ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import threading import traceback from ..config import cfg from ..globals import (config_answer_queue, config_query_queue, config_store, volatile_answer_queue, volatile_query_queue, volatile_store) from ..helpers import error_exit from .mysql import DBMySQL from .postgresql import DBPostgreSQL from .sqlite import SQLite from .store import (ClientConfig, ClientConfigDicts, Store) from .textfile import Textfile class QueryQueue(threading.Thread): """ Pump queries around """ def __init__(self, name='', store_type=None, query_queue=None, answer_queue=None): threading.Thread.__init__(self, name=name) self.query_queue = query_queue self.answer_queue = answer_queue self.store = store_type self.setDaemon(True) def run(self): """ receive queries and ask the DB interface for answers which will be put into answer queue """ while True: query = self.query_queue.get() try: answer = self.store.db_query(query) except Exception as error: traceback.print_exc(file=sys.stdout) sys.stdout.flush() answer = error self.answer_queue.put({query: answer}) # because of thread trouble there should not be too much db connections at once # so we need to use the queryqueue way - subject to change # source of configuration of hosts # use client configuration only if needed if cfg.STORE_CONFIG: if cfg.STORE_CONFIG == 'file': config_store = Textfile(config_query_queue, config_answer_queue) if cfg.STORE_CONFIG == 'mysql': config_store = DBMySQL(config_query_queue, config_answer_queue) if cfg.STORE_CONFIG == 'postgresql': config_store = DBPostgreSQL(config_query_queue, config_answer_queue) if cfg.STORE_CONFIG == 'sqlite': config_store = SQLite(config_query_queue, config_answer_queue, storage_type='config') else: # dummy configstore if no client config is needed config_store = Store(config_query_queue, config_answer_queue) # 'none' store is always connected config_store.connected = True # storage for changing data like leases, LLIPs, DUIDs etc. if cfg.STORE_VOLATILE == 'mysql': volatile_store = DBMySQL(volatile_query_queue, volatile_answer_queue) if cfg.STORE_VOLATILE == 'postgresql': volatile_store = DBPostgreSQL(volatile_query_queue, volatile_answer_queue) if cfg.STORE_VOLATILE == 'sqlite': volatile_store = SQLite(volatile_query_queue, volatile_answer_queue, storage_type='volatile') # do not start if no database connection exists if not config_store.connected: error_exit('Configuration database is not connected!') if not volatile_store.connected: error_exit('Database for volatile data is not connected!') dhcpy6d-1.2.3/dhcpy6d/storage/mysql.py000066400000000000000000000060321437472361000176120ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import traceback from ..config import cfg from ..helpers import error_exit from .store import DB class DBMySQL(DB): """ access MySQL and MariaDB """ QUERY_TABLES = f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{cfg.STORE_DB_DB}'" def db_connect(self): """ Connect to database server according to database type """ # try to get some MySQL/MariaDB-module imported try: if 'MySQLdb' not in sys.modules: import MySQLdb self.db_module = sys.modules['MySQLdb'] except: try: if 'pymsql' not in sys.modules: import pymysql self.db_module = sys.modules['pymysql'] except: error_exit('ERROR: Cannot find module MySQLdb or PyMySQL. Please install one of them to proceed.') try: self.connection = self.db_module.connect(host=cfg.STORE_DB_HOST, db=cfg.STORE_DB_DB, user=cfg.STORE_DB_USER, passwd=cfg.STORE_DB_PASSWORD) self.connection.autocommit(True) self.cursor = self.connection.cursor() self.connected = True except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() self.connected = False return self.connected def db_query(self, query): """ execute DB query """ try: self.cursor.execute(query) except self.db_module.IntegrityError: return 'INSERT_ERROR' except Exception as err: # try to reestablish database connection print(f'Error: {str(err)}') print(f'Query: {query}') if not self.db_connect(): return None else: try: self.cursor.execute(query) except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() self.connected = False return None result = self.cursor.fetchall() return result dhcpy6d-1.2.3/dhcpy6d/storage/postgresql.py000066400000000000000000000066331437472361000206570ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import traceback from ..config import cfg from ..helpers import error_exit from .schemas import POSTGRESQL_SCHEMA from .store import DB class DBPostgreSQL(DB): """ PostgreSQL connection - to be tested! """ # different to default derived MYSQL_SQLITE schema SCHEMAS = POSTGRESQL_SCHEMA QUERY_TABLES = f"SELECT table_name FROM information_schema.tables WHERE " \ f"table_schema = 'public' AND " \ f"table_catalog = '{cfg.STORE_DB_DB}'" def db_connect(self): """ Connect to database server according to database type """ try: if 'psycopg2' not in sys.modules: import psycopg2 self.db_module = sys.modules['psycopg2'] except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() error_exit('ERROR: Cannot find module psycopg2. Please install to proceed.') try: self.connection = self.db_module.connect(host=cfg.STORE_DB_HOST, database=cfg.STORE_DB_DB, user=cfg.STORE_DB_USER, password=cfg.STORE_DB_PASSWORD) self.connection.autocommit = True self.cursor = self.connection.cursor() self.connected = True except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() self.connected = False return self.connected def db_query(self, query): """ execute DB query """ try: self.cursor.execute(query) # catch impossible INSERTs except self.db_module.errors.UniqueViolation: return 'INSERT_ERROR' except self.db_module.errors.IntegrityError: return 'INSERT_ERROR' except Exception as err: # try to reestablish database connection print(f'Error: {str(err)}') if not self.db_connect(): return None else: try: self.cursor.execute(query) except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() self.connected = False return None try: result = self.cursor.fetchall() # If there is no result after a database reconnect a None would lead to eternal loop except self.db_module.ProgrammingError: return [] except Exception as err: return None return result dhcpy6d-1.2.3/dhcpy6d/storage/schemas.py000066400000000000000000000462771437472361000201070ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys from ..config import cfg # put SQL schemas here to be in reach of all storage types # generic mostly usable for SQLite and MySQL/MariaDB MYSQL_SQLITE = {} MYSQL_SQLITE['meta'] = ''' CREATE TABLE meta ( item_key varchar(255) NOT NULL, item_value varchar(255) NOT NULL, PRIMARY KEY (item_key) ); ''' MYSQL_SQLITE['leases'] = ''' CREATE TABLE leases ( address varchar(32) NOT NULL, active tinyint(4) NOT NULL, preferred_lifetime int(11) NOT NULL, valid_lifetime int(11) NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, ia_type varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int(11) NOT NULL DEFAULT 0, PRIMARY KEY (address) ); ''' MYSQL_SQLITE['macs_llips'] = ''' CREATE TABLE macs_llips ( mac varchar(17) NOT NULL, link_local_ip varchar(39) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (mac) ); ''' MYSQL_SQLITE['prefixes'] = ''' CREATE TABLE prefixes ( prefix varchar(32) NOT NULL, length tinyint(4) NOT NULL, active tinyint(4) NOT NULL, preferred_lifetime int(11) NOT NULL, valid_lifetime int(11) NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int(11) NOT NULL DEFAULT 0, PRIMARY KEY (prefix) ); ''' MYSQL_SQLITE['routes'] = ''' CREATE TABLE routes ( prefix varchar(32) NOT NULL, length tinyint(4) NOT NULL, router varchar(32) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (prefix) ); ''' # Postgresql has some differences and so its own schemas POSTGRESQL_SCHEMA = {} POSTGRESQL_SCHEMA['meta'] = ''' CREATE TABLE meta ( item_key varchar(255) NOT NULL, item_value varchar(255) NOT NULL, PRIMARY KEY (item_key) ); ''' POSTGRESQL_SCHEMA['leases'] = ''' CREATE TABLE leases ( address varchar(32) NOT NULL, active smallint NOT NULL, preferred_lifetime int NOT NULL, valid_lifetime int NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, ia_type varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int NOT NULL DEFAULT 0, PRIMARY KEY (address) ); ''' POSTGRESQL_SCHEMA['macs_llips'] = ''' CREATE TABLE macs_llips ( mac varchar(17) NOT NULL, link_local_ip varchar(39) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (mac) ); ''' POSTGRESQL_SCHEMA['prefixes'] = ''' CREATE TABLE prefixes ( prefix varchar(32) NOT NULL, length smallint NOT NULL, active smallint NOT NULL, preferred_lifetime int NOT NULL, valid_lifetime int NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int NOT NULL DEFAULT 0, PRIMARY KEY (prefix) ); ''' POSTGRESQL_SCHEMA['routes'] = ''' CREATE TABLE routes ( prefix varchar(32) NOT NULL, length smallint NOT NULL, router varchar(32) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (prefix) ); ''' # formerly part of store.py but might fit better here def legacy_adjustments(db): """ adjust some existing data to work with newer versions of dhcpy6d """ try: if db.query('SELECT last_message FROM leases LIMIT 1') is None: # row 'last_message' in schemas 'leases' does not exist yet, comes with version 0.1.6 db.query('ALTER TABLE leases ADD last_message INT NOT NULL DEFAULT 0') print("Adding row 'last_message' to table 'leases' in volatile storage succeeded.") except: print("\n'ALTER TABLE leases ADD last_message INT NOT NULL DEFAULT 0' on volatile database failed.") print('Please apply manually or grant necessary permissions.\n') sys.exit(1) # after 0.4.3 with working PostgreSQL support the timestamps have to be stores in epoch seconds, not datetime # also after 0.4.3 there will be a third table containing meta information - for a first start it should contain # a database version number try: try: # only newer databases contain a version number - real ones starting with 1 # non-existing version is False db_version = db.get_db_version() if db_version is None or int(db_version) == 0: db_operations = [] # only create meta table if it does not exist yet if not 'meta' in db.get_tables(): db_operations.append('CREATE TABLE meta (item_key varchar(255) NOT NULL,\ item_value varchar(255) NOT NULL, PRIMARY KEY (item_key))') # add version of database scheme db_operations.append("INSERT INTO meta (item_key, item_value) VALUES ('version', '1')") for db_operation in db_operations: db.query(db_operation) print(f"{db_operation} in volatile storage succeded.") except Exception as err: print(f"\n{db_operation} on volatile database failed.") print('Please apply manually or grant necessary permissions.\n') sys.exit(1) except Exception as err: print('\nSomething went wrong when retrieving version from database.\n') sys.exit(1) # find out if timestamps still are in datetime format - applies only to sqlite and mysql anyway if cfg.STORE_VOLATILE in ['sqlite', 'mysql']: db_datetime_test = db.query('SELECT last_update FROM leases LIMIT 1') if len(db_datetime_test) > 0: import datetime # flag to find out which update has to be done update_type = False # MySQL if type(db_datetime_test[0][0]) is datetime.datetime: update_type = 'mysql' # SQLite if type(db_datetime_test[0][0]) is str: if ' ' in db_datetime_test[0][0]: update_type = 'sqlite' if update_type: # add new columns with suffix *_new db_tables = {'leases': ['last_update', 'preferred_until', 'valid_until'], 'macs_llips': ['last_update']} if update_type == 'mysql': for table in db_tables: for column in db_tables[table]: db.query(f'ALTER TABLE {table} ADD COLUMN {column}_new bigint NOT NULL') print(f'ALTER TABLE {table} ADD COLUMN {column}_new bigint NOT NULL succeeded') # get old timestamps timestamps_old = db.query( 'SELECT address, last_update, preferred_until, valid_until FROM leases') for timestamp_old in timestamps_old: address, last_update, preferred_until, valid_until = timestamp_old # convert SQLite datetime values from unicode to Python datetime if update_type == 'sqlite': last_update = datetime.datetime.strptime(last_update, '%Y-%m-%d %H:%M:%S.%f') preferred_until = datetime.datetime.strptime(preferred_until, '%Y-%m-%d %H:%M:%S.%f') valid_until = datetime.datetime.strptime(valid_until, '%Y-%m-%d %H:%M:%S.%f') last_update_new = last_update.strftime('%s') preferred_until_new = preferred_until.strftime('%s') valid_until_new = valid_until.strftime('%s') db.query(f"UPDATE leases SET last_update_new = {last_update_new}, " f"preferred_until_new = {preferred_until_new}, " f"valid_until_new = {valid_until_new} " f"WHERE address = '{address}'") print('Converting timestamps of leases succeeded') timestamps_old = db.query('SELECT mac, last_update FROM macs_llips') for timestamp_old in timestamps_old: mac, last_update = timestamp_old last_update_new = last_update.strftime('%s') db.query(f"UPDATE macs_llips SET last_update_new = {last_update_new} WHERE mac = '{mac}'") print('Converting timestamps of macs_llips succeeded') for table in db_tables: for column in db_tables[table]: db.query(f'ALTER TABLE {table} DROP COLUMN {column}') db.query(f'ALTER TABLE {table} CHANGE COLUMN {column}_new {column} BIGINT NOT NULL') print(f'Moving column {column} of table {table} succeeded') if update_type == 'sqlite': for table in db_tables: db.query(f'ALTER TABLE {table} RENAME TO {table}_old') db.query('CREATE TABLE leases AS SELECT address,active,last_message,preferred_lifetime,' 'valid_lifetime,hostname,type,category,ia_type,' 'class,mac,duid,iaid ' 'FROM leases_old') db.query('CREATE TABLE macs_llips AS SELECT mac,link_local_ip FROM macs_llips_old') # add timestamp columns in bigint format instead of datetime for table in db_tables: for column in db_tables[table]: db.query(f'ALTER TABLE {table} ADD COLUMN {column} bigint') # get old timestamps timestamps_old = db.query( 'SELECT address, last_update, preferred_until, valid_until FROM leases_old') for timestamp_old in timestamps_old: address, last_update, preferred_until, valid_until = timestamp_old # convert SQLite datetime values from unicode to Python datetime if update_type == 'sqlite': last_update = datetime.datetime.strptime(last_update, '%Y-%m-%d %H:%M:%S.%f') preferred_until = datetime.datetime.strptime(preferred_until, '%Y-%m-%d %H:%M:%S.%f') valid_until = datetime.datetime.strptime(valid_until, '%Y-%m-%d %H:%M:%S.%f') last_update_new = last_update.strftime('%s') preferred_until_new = preferred_until.strftime('%s') valid_until_new = valid_until.strftime('%s') db.query(f"UPDATE leases SET last_update = {last_update_new}, " f"preferred_until = {preferred_until_new}, " f"valid_until = {valid_until_new} " f"WHERE address = '{address}'") print('Converting timestamps of leases succeeded') timestamps_old = db.query('SELECT mac, last_update FROM macs_llips_old') for timestamp_old in timestamps_old: mac, last_update = timestamp_old last_update_new = last_update.strftime('%s') db.query(f"UPDATE macs_llips SET last_update = {last_update_new} WHERE mac = '{mac}'") print('Converting timestamps of macs_llips succeeded') # Extend volatile database to handle prefixes - comes with database version 2 if int(db.get_db_version()) < 2: if not 'prefixes' in db.get_tables(): if cfg.STORE_VOLATILE in ['sqlite', 'mysql']: db.query('CREATE TABLE prefixes (\ prefix varchar(32) NOT NULL,\ length tinyint(4) NOT NULL,\ active tinyint(4) NOT NULL,\ preferred_lifetime int(11) NOT NULL,\ valid_lifetime int(11) NOT NULL,\ hostname varchar(255) NOT NULL,\ type varchar(255) NOT NULL,\ category varchar(255) NOT NULL,\ class varchar(255) NOT NULL,\ mac varchar(17) NOT NULL,\ duid varchar(255) NOT NULL,\ last_update bigint NOT NULL,\ preferred_until bigint NOT NULL,\ valid_until bigint NOT NULL,\ iaid varchar(8) DEFAULT NULL,\ last_message int(11) NOT NULL DEFAULT 0,\ PRIMARY KEY (prefix)\ )') elif cfg.STORE_VOLATILE == 'postgresql': db.query('CREATE TABLE prefixes (\ prefix varchar(32) NOT NULL,\ length smallint NOT NULL,\ active smallint NOT NULL,\ preferred_lifetime int NOT NULL,\ valid_lifetime int NOT NULL,\ hostname varchar(255) NOT NULL,\ type varchar(255) NOT NULL,\ category varchar(255) NOT NULL,\ class varchar(255) NOT NULL,\ mac varchar(17) NOT NULL,\ duid varchar(255) NOT NULL,\ last_update bigint NOT NULL,\ preferred_until bigint NOT NULL,\ valid_until bigint NOT NULL,\ iaid varchar(8) DEFAULT NULL,\ last_message int NOT NULL DEFAULT 0,\ PRIMARY KEY (prefix)\ )') # increase version to 2 db.query("UPDATE meta SET item_value='2' WHERE item_key='version'") # All OK print("Table 'prefixes' is OK") # Extend volatile database to handle routes - comes with database version 3 if int(db.get_db_version()) < 3: if not 'prefixes' in db.get_tables(): if cfg.STORE_VOLATILE in ['sqlite', 'mysql']: db.query('CREATE TABLE routes (\ prefix varchar(32) NOT NULL,\ length tinyint(4) NOT NULL,\ router varchar(32) NOT NULL,\ last_update bigint NOT NULL,\ PRIMARY KEY (prefix)\ )') elif cfg.STORE_VOLATILE == 'postgresql': db.query('CREATE TABLE routes (\ prefix varchar(32) NOT NULL,\ length smallint NOT NULL,\ router varchar(32) NOT NULL,\ last_update bigint NOT NULL,\ PRIMARY KEY (prefix)\ )') # increase version to 3 db.query("UPDATE meta SET item_value='3' WHERE item_key='version'") # All OK print("Table 'routes' is OK") dhcpy6d-1.2.3/dhcpy6d/storage/sqlite.py000066400000000000000000000061571437472361000177560ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import grp import os import pwd import sys import traceback from ..config import cfg from .store import Store class SQLite(Store): """ file-based SQLite database, might be an option for single installations """ QUERY_TABLES = "SELECT name FROM sqlite_master WHERE type='table'" def __init__(self, query_queue, answer_queue, storage_type='volatile'): Store.__init__(self, query_queue, answer_queue) self.connection = None try: self.db_connect(storage_type) except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() def db_connect(self, storage_type='volatile'): """ initialize DB connection """ # only import if needed if 'sqlite3' not in sys.modules: import sqlite3 self.db_module = sqlite3 try: if storage_type == 'volatile': storage = cfg.STORE_SQLITE_VOLATILE # set ownership of storage file according to settings os.chown(cfg.STORE_SQLITE_VOLATILE, pwd.getpwnam(cfg.USER).pw_uid, grp.getgrnam(cfg.GROUP).gr_gid) if storage_type == 'config': storage = cfg.STORE_SQLITE_CONFIG self.connection = self.db_module.connect(storage, check_same_thread = False) self.cursor = self.connection.cursor() self.connected = True except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() return None def db_query(self, query): """ execute query on DB """ try: self.cursor.execute(query) # commit only if explicitly wanted if query.startswith('INSERT'): self.connection.commit() elif query.startswith('UPDATE'): self.connection.commit() elif query.startswith('DELETE'): self.connection.commit() self.connected = True except self.db_module.IntegrityError: return 'INSERT_ERROR' except Exception as err: # try to reestablish database connection print(f'Error: {str(err.args[0])}') print(f'Query: {query}') if not self.db_connect(): return None result = self.cursor.fetchall() return result dhcpy6d-1.2.3/dhcpy6d/storage/store.py000066400000000000000000001263771437472361000176200ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import traceback from ..config import cfg from ..globals import collected_macs from ..helpers import (decompress_ip6, error_exit, listify_option, NeighborCacheRecord, convert_prefix_inline) from .schemas import (legacy_adjustments, MYSQL_SQLITE) class ClientConfig: """ static client settings object to be stuffed into Hosts dict of Textfile store """ def __init__(self, hostname='', client_class='default', duid='', address=None, prefix=None, mac=None, host_id=''): self.HOSTNAME = hostname # MACs if type(mac) == list: self.MAC = mac else: self.MAC = listify_option(mac) # fixed addresses if address: self.ADDRESS = list() if type(address) == list: addresses = address else: addresses = listify_option(address) for a in addresses: self.ADDRESS.append(decompress_ip6(a)) else: self.ADDRESS = None # fixed prefix if prefix: self.PREFIX = list() if type(prefix) == list: prefixes = prefix else: prefixes = listify_option(prefix) for p in prefixes: self.PREFIX.append(convert_prefix_inline(p)) else: self.PREFIX = None self.CLASS = client_class self.ID = host_id self.DUID = duid class ClientConfigDicts: """ class for storing client config snippet from DB - used in SQLite and MySQL Storage """ def __init__(self): self.hosts = {} self.index_mac = {} self.index_duid = {} class Store: """ abstract class to present MySQL or SQLite or Postgresql """ # put SQL schemas here to be in reach of all storage types SCHEMAS = MYSQL_SQLITE # query to get tables - different in every SQL storage # if no tables exist they will be created by create_tables() QUERY_TABLES = '' # increasing number of SQL schema versions version = 3 # link to used database module db_module = None # flag for config database prefix support config_prefix_support = False def __init__(self, query_queue, answer_queue): self.query_queue = query_queue self.answer_queue = answer_queue # table names used for database storage - MySQL additionally needs the database name self.table_leases = 'leases' self.table_prefixes = 'prefixes' self.table_macs_llips = 'macs_llips' self.table_hosts = 'hosts' self.table_routes = 'routes' # flag to check if connection is OK self.connected = False # storage of query answers self.answers = {} def query(self, query): """ put queries received into query queue and return the answers from answer queue """ if query in self.answers: answer = self.answers.pop(query) else: answer = None while answer is None: self.query_queue.put(query) self.answers.update(self.answer_queue.get()) # just make sure the right answer comes back if query in self.answers: answer = self.answers.pop(query) return answer def clean_query_answer(method): """ decorate repeatedly but not everywhere used cleaning of query answer """ def decoration_function(self, *args, **kwargs): # run decorated method answer = method(self, *args, **kwargs) # clean answer # SQLite returns list, MySQL tuple - in case someone wonders here... if not (answer == [] or answer == () or answer is None): return answer[0][0] else: return None return decoration_function @clean_query_answer def get_db_version(self): """ return stored version if dhcpy6d DB """ try: # will be cleaned by decorator so default answer value is a little bit unintuitive answer = [['0']] if 'meta' in self.get_tables(): answer = self.query("SELECT item_value FROM meta WHERE item_key = 'version'") return answer except: # no table 'meta' and no key 'version' return answer def get_tables(self): """ return tables - no turntables """ tables = [] # every DB type needs another query for tables query = self.QUERY_TABLES answer = self.query(query) if len(answer) > 0: tables = [x[0] for x in answer] return tables def create_tables(self): """ create tables in different storage types - information about schemas comes from schemas.py """ for table in self.SCHEMAS: query = self.SCHEMAS[table] self.cursor.execute(query) # set initial version self.cursor.execute(f"INSERT INTO meta (item_key, item_value) VALUES ('version', '{self.version}')") def check_storage(self): """ check if all databases/storage is ready and up to date """ tables = self.get_tables() # if there are no tables they have to be created if len(tables) == 0: self.create_tables() # otherwise they migth need just some updates else: legacy_adjustments(self) def check_config_prefixes_support(self): """ check if client config database contains prefixes - if not just do not ask for prefixes in the future """ # has to be checked only for databases if cfg.STORE_CONFIG and cfg.STORE_CONFIG != 'file': # first check if database has config information at all try: self.cursor.execute(f"SELECT hostname, mac, duid, class, address, id FROM {self.table_hosts} LIMIT 1") except Exception as error: error_exit(f"Config database has problem: {error.args[-1]}") # if config information exists check if it also has prefixes try: self.cursor.execute(f"SELECT hostname, mac, duid, class, address, prefix, id FROM {self.table_hosts} LIMIT 1") except: return False # when query was ok prefix support exists self.config_prefix_support = True return True def store(self, transaction, now): """ store lease in lease DB """ # only if client exists if transaction.client: for a in transaction.client.addresses: if a.ADDRESS is not None: query = f"SELECT address FROM {self.table_leases} WHERE address = '{a.ADDRESS}'" answer = self.query(query) if answer is not None: # if address is not leased yet add it if len(answer) == 0: query = f"INSERT INTO {self.table_leases} (address, active, last_message, " \ f"preferred_lifetime, valid_lifetime, hostname, type, category, ia_type, " \ f"class, mac, duid, iaid, last_update, preferred_until, valid_until) " \ f"VALUES ('{a.ADDRESS}', " \ f"'1', " \ f"'{transaction.last_message_received_type}', " \ f"'{a.PREFERRED_LIFETIME}', " \ f"'{a.VALID_LIFETIME}', " \ f"'{transaction.client.hostname}', " \ f"'{a.TYPE}', " \ f"'{a.CATEGORY}', " \ f"'{a.IA_TYPE}', " \ f"'{transaction.client.client_class}', " \ f"'{transaction.mac}', " \ f"'{transaction.duid}', " \ f"'{transaction.iaid}', " \ f"'{now}', " \ f"'{now + int(a.PREFERRED_LIFETIME)}', " \ f"'{now + int(a.VALID_LIFETIME)}')" answer = self.query(query) # for unknown reasons sometime a lease shall be inserted which already exists # in this case go further (aka continue) and do an update instead of an insert if answer == 'INSERT_ERROR': print('IntegrityError:', query) else: # jump to next item of loop continue # otherwise update it if not a random address if a.CATEGORY != 'random': query = f"UPDATE {self.table_leases} " \ f"SET active = 1, " \ f"last_message = {transaction.last_message_received_type}, " \ f"preferred_lifetime = '{a.PREFERRED_LIFETIME}', " \ f"valid_lifetime = '{a.VALID_LIFETIME}', " \ f"hostname = '{transaction.client.hostname}', " \ f"type = '{a.TYPE}', " \ f"category = '{a.CATEGORY}', " \ f"ia_type = '{a.IA_TYPE}', " \ f"class = '{transaction.client.client_class}', " \ f"mac = '{transaction.mac}', " \ f"duid = '{transaction.duid}', " \ f"iaid = '{transaction.iaid}', " \ f"last_update = '{now}', " \ f"preferred_until = '{now + int(a.PREFERRED_LIFETIME)}', " \ f"valid_until = '{now + int(a.VALID_LIFETIME)}' " \ f"WHERE address = '{a.ADDRESS}'" else: # set last message type of random address query = f"UPDATE {self.table_leases} " \ f"SET active = 1, " \ f"last_message = {transaction.last_message_received_type} " \ f"WHERE address = '{a.ADDRESS}'" self.query(query) for p in transaction.client.prefixes: if p.PREFIX is not None: query = f"SELECT prefix FROM {self.table_prefixes} WHERE prefix = '{p.PREFIX}'" answer = self.query(query) if answer is not None: # if prefix is not leased yet add it if len(answer) == 0: query = f"INSERT INTO {self.table_prefixes} (prefix, length, active, last_message, " \ f"preferred_lifetime, valid_lifetime, hostname, type, category, class, mac, duid, " \ f"iaid, last_update, preferred_until, valid_until) " \ f"VALUES ('{p.PREFIX}', " \ f"'{p.LENGTH}', " \ f"1, " \ f"'{transaction.last_message_received_type}', " \ f"'{p.PREFERRED_LIFETIME}', " \ f"'{p.VALID_LIFETIME}', " \ f"'{transaction.client.hostname}', " \ f"'{p.TYPE}', " \ f"'{p.CATEGORY}', " \ f"'{transaction.client.client_class}', " \ f"'{transaction.mac}', " \ f"'{transaction.duid}', " \ f"'{transaction.iaid}', " \ f"'{now}', " \ f"'{now + int(p.PREFERRED_LIFETIME)}', " \ f"'{now + int(p.VALID_LIFETIME)}')" answer = self.query(query) # for unknow reasons sometime a lease shall be inserted which already exists # in this case go further (aka continue) and do an update instead of an insert # doing this here for prefixes is just a precautional measure if answer != 'INSERT_ERROR': continue # otherwise update it if not a random prefix # anyway right now only the categories 'range' and 'id' exist if p.CATEGORY != 'random': query = f"UPDATE {self.table_prefixes} SET active = 1, " \ f"last_message = {transaction.last_message_received_type}, " \ f"preferred_lifetime = '{p.PREFERRED_LIFETIME}', " \ f"valid_lifetime = '{p.VALID_LIFETIME}', " \ f"hostname = '{transaction.client.hostname}', " \ f"type = '{p.TYPE}', " \ f"category = '{p.CATEGORY}', " \ f"class = '{transaction.client.client_class}', " \ f"mac = '{transaction.mac}', " \ f"duid = '{transaction.duid}', " \ f"iaid = '{transaction.iaid}', " \ f"last_update = '{now}', " \ f"preferred_until = '{now + int(p.PREFERRED_LIFETIME)}', " \ f"valid_until = '{now + int(p.VALID_LIFETIME)}' " \ f"WHERE prefix = '{p.PREFIX}'" else: # set last message type of random prefix query = f"UPDATE {self.table_prefixes} " \ f"SET last_message = {transaction.last_message_received_type}, " \ f"active = 1 " \ f"WHERE prefix = '{p.PREFIX}'" self.query(query) return True # if no client -> False return False @clean_query_answer def store_route(self, prefix, length, router, now): """ store route in database to keep track of routes and be able to delete them later """ query = f"SELECT prefix FROM {self.table_routes} WHERE prefix = '{prefix}'" if self.query is not None: if len(self.query(query)) == 0: query = f"INSERT INTO {self.table_routes} VALUES ('{prefix}', {length}, '{router}', {now})" return self.query(query) elif len(self.query(query)) == 1: query = f"UPDATE {self.table_routes} SET prefix = '{prefix}', length = {length}, " \ f"router = '{router}', last_update = {now} WHERE prefix = '{prefix}'" return self.query(query) return None else: return None @clean_query_answer def get_range_lease_for_recycling(self, prefix='', range_from='', range_to='', duid='', mac=''): """ ask DB for last known leases of an already known host to be recycled this is most useful for CONFIRM-requests that will get a not-available-answer but get an ADVERTISE with the last known-as-good address for a client SOLICIT message type is 1 """ query = f"SELECT address FROM {self.table_leases} WHERE " \ f"category = 'range' AND " \ f"'{prefix + range_from}' <= address AND " \ f"address <= '{prefix + range_to}' AND " \ f"duid = '{duid}' AND " \ f"mac = '{mac}' AND " \ f"last_message != 1 " \ f"ORDER BY last_update DESC LIMIT 1" return self.query(query) @clean_query_answer def get_range_prefix_for_recycling(self, prefix='', length='', range_from='', range_to='', duid='', mac=''): """ ask DB for last known prefixes of an already known host to be recycled this is most useful for CONFIRM-requests that will get a not-available-answer but get an ADVERTISE with the last known-as-good address for a client SOLICIT message type is 1 """ query = f"SELECT prefix FROM {self.table_prefixes} WHERE " \ f"category = 'range' AND " \ f"'{prefix + range_from + ((128 - int(length)) // 4) * '0'}' <= prefix AND " \ f"prefix <= '{prefix + range_to + ((128 - int(length)) // 4) * '0'}' AND " \ f"length = '{length}' AND " \ f"duid = '{duid}' AND " \ f"mac = '{mac}' AND " \ f"last_message != 1 " \ f"ORDER BY last_update DESC LIMIT 1" return self.query(query) @clean_query_answer def get_highest_range_lease(self, prefix='', range_from='', range_to=''): """ ask DB for highest known leases - if necessary range sensitive """ query = f"SELECT address FROM {self.table_leases} WHERE active = 1 AND " \ f"category = 'range' AND " \ f"'{prefix + range_from}' <= address and address <= '{prefix + range_to}' ORDER BY address DESC LIMIT 1" return self.query(query) @clean_query_answer def get_highest_range_prefix(self, prefix='', length='', range_from='', range_to=''): """ ask DB for highest known prefix - if necessary range sensitive """ query = f"SELECT prefix FROM {self.table_prefixes} WHERE active = 1 AND " \ f"category = 'range' AND " \ f"'{prefix + range_from + ((128 - int(length)) // 4) * '0'}' <= prefix AND " \ f"prefix <= '{prefix + range_to + ((128 - int(length)) // 4) * '0'}' AND " \ f"length = '{length}' ORDER BY prefix DESC LIMIT 1" return self.query(query) @clean_query_answer def get_oldest_inactive_range_lease(self, prefix='', range_from='', range_to=''): """ ask DB for oldest known inactive lease to minimize chance of collisions ordered by valid_until to get leases that are free as long as possible """ query = f"SELECT address FROM {self.table_leases} WHERE active = 0 AND " \ f"category = 'range' AND " \ f"'{prefix + range_from}' <= address AND " \ f"address <= '{prefix + range_to}' ORDER BY valid_until ASC LIMIT 1" return self.query(query) @clean_query_answer def get_oldest_inactive_range_prefix(self, prefix='', length='', range_from='', range_to=''): """ ask DB for oldest known inactive prefix to minimize chance of collisions ordered by valid_until to get leases that are free as long as possible """ query = f"SELECT prefix FROM {self.table_prefixes} WHERE active = 0 AND " \ f"category = 'range' AND " \ f"'{prefix + range_from + ((128 - int(length)) // 4) * '0'}' <= prefix AND " \ f"prefix <= '{prefix + range_to + ((128 - int(length)) // 4) * '0'}' AND " \ f"length = '{length}' " \ f"ORDER BY valid_until ASC LIMIT 1" return self.query(query) def get_host_lease(self, address): """ get the hostname, DUID, MAC and IAID to verify a lease to delete its address in the DNS """ query = f"SELECT DISTINCT hostname, duid, mac, iaid FROM {self.table_leases} WHERE address='{address}'" answer = self.query(query) if answer is not None: if len(answer) > 0: if len(answer[0]) > 0: return answer[0] else: # calling method expects quartet of hostname, duid, mac, iad - get None if nothing there return None, None, None, None else: return None, None, None, None else: return None, None, None, None def get_active_prefixes(self): """ get used prefixes to be able to reinstall their routes """ # query = "SELECT {0}.prefix FROM {0} INNER JOIN {1} ON {0}.prefix = {1}.prefix WHERE {0}.active = 1".format(self.table_prefixes, self.table_routes) query = f"SELECT {self.table_prefixes}.prefix FROM {self.table_prefixes} INNER JOIN {self.table_routes} ON " \ f"{self.table_prefixes}.prefix = {self.table_routes}.prefix WHERE {self.table_prefixes}.active = 1" answer = self.query(query) active_prefixes = list() if answer is not None: for prefix in answer: active_prefixes.append(prefix[0]) return active_prefixes def get_inactive_prefixes(self): """ get unused prefixes to be able to delete their routes """ # query = "SELECT {0}.prefix FROM {0} INNER JOIN {1} ON {0}.prefix = {1}.prefix WHERE {0}.active = 0".format(self.table_prefixes, self.table_routes) query = f"SELECT {self.table_prefixes}.prefix FROM {self.table_prefixes} INNER JOIN {self.table_routes} " \ f"ON {self.table_prefixes}.prefix = {self.table_routes}.prefix WHERE {self.table_prefixes}.active = 0" answer = self.query(query) inactive_prefixes = list() if answer is not None: for prefix in answer: inactive_prefixes.append(prefix[0]) return inactive_prefixes def get_route(self, prefix): """ get all route parameters plus class for a certain prefix - mostly to delete the route """ # query = "SELECT {0}.length, {0}.router, {1}.class FROM {0} INNER JOIN {1} WHERE {0}.prefix = {1}.prefix AND {1}.prefix = '{2}'".format(self.table_routes, self.table_prefixes, prefix) query = f"SELECT {self.table_routes}.length, {self.table_routes}.router, {self.table_prefixes}.class FROM " \ f"{self.table_routes} INNER JOIN {self.table_prefixes} WHERE {self.table_routes}.prefix = " \ f"{self.table_prefixes}.prefix AND {self.table_prefixes}.prefix = '{prefix}'" answer = self.query(query) if answer is not None: if len(answer) > 0: if len(answer[0]) > 0: return answer[0] else: # calling method expects triple of length, router and class - get None if nothing there return None, None, None else: return None, None, None else: return None, None, None @clean_query_answer def release_lease(self, address, now): """ release a lease via setting its active flag to False set last_message to 8 because of RELEASE messages having this message id """ query = f"UPDATE {self.table_leases} SET active = 0, last_message = 8, last_update = '{now}' WHERE address = '{address}'" self.query(query) @clean_query_answer def release_prefix(self, prefix, now): """ release a prefix via setting its active flag to False set last_message to 8 because of RELEASE messages having this message id """ query = f"UPDATE {self.table_prefixes} SET active = 0, last_message = 8, last_update = '{now}' WHERE prefix = '{prefix}'" self.query(query) @clean_query_answer def check_number_of_leases(self, prefix='', range_from='', range_to=''): """ check how many leases are stored - used to find out if address range has been exceeded """ query = f"SELECT COUNT(address) FROM {self.table_leases} WHERE address LIKE '{prefix}%' AND " \ f"'{prefix + range_from}' <= address AND address <= '{prefix + range_to}'" return self.query(query) @clean_query_answer def check_number_of_prefixes(self, prefix='', length='', range_from='', range_to=''): """ check how many leases are stored - used to find out if address range has been exceeded """ query = f"SELECT COUNT(prefix) FROM {self.table_prefixes} WHERE prefix LIKE '{prefix}%' AND " \ f"'{prefix + range_from + ((128 - int(length)) // 4) * '0'}' <= prefix AND " \ f"prefix <= '{prefix + range_to + ((128 - int(length)) // 4) * '0'}'" return self.query(query) def check_lease(self, address, transaction): """ check state of a lease for REBIND and RENEW messages """ # attributes to identify host and lease if cfg.IGNORE_IAID: query = f"SELECT DISTINCT hostname, address, type, category, ia_type, class, preferred_until " \ f"FROM {self.table_leases} WHERE active = 1 AND " \ f"address = '{address}' AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}'" else: query = f"SELECT DISTINCT hostname, address, type, category, ia_type, class, preferred_until " \ f"FROM {self.table_leases} WHERE active = 1 AND " \ f"address = '{address}' AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}' AND " \ f"iaid = '{transaction.iaid}'" return self.query(query) def check_prefix(self, prefix, length, transaction): """ check state of a prefix for REBIND and RENEW messages """ # attributes to identify host and lease if cfg.IGNORE_IAID: query = f"SELECT DISTINCT hostname, prefix, length, type, category, class, preferred_until " \ f"FROM {self.table_prefixes} WHERE active = 1 AND " \ f"prefix = '{prefix}' AND " \ f"length = '{length}' AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}'" else: query = f"SELECT DISTINCT hostname, prefix, length, type, category, class, preferred_until " \ f"FROM {self.table_prefixes} WHERE active = 1 AND " \ f"prefix = '{prefix}' AND " \ f"length = '{length}' AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}' AND " \ f"iaid = '{transaction.iaid}'" return self.query(query) def check_advertised_lease(self, transaction=None, category='', atype=''): """ check if there are already advertised addresses for client """ # attributes to identify host and lease if cfg.IGNORE_IAID: query = f"SELECT address FROM {self.table_leases} WHERE last_message = 1 AND " \ f"active = 1 AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}' AND " \ f"category = '{category}' AND " \ f"type = '{atype}'" else: query = f"SELECT address FROM {self.table_leases} WHERE last_message = 1 AND " \ f"active = 1 AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}' AND " \ f"iaid = '{transaction.iaid}' AND " \ f"category = '{category}' AND " \ f"type = '{atype}'" answer = self.query(query) if answer is not None: if len(answer) == 0: return False else: return answer[0][0] else: return False def check_advertised_prefix(self, transaction, category='', ptype=''): """ check if there is already an advertised prefix for client """ # attributes to identify host and lease if cfg.IGNORE_IAID: query = f"SELECT prefix, length FROM {self.table_prefixes} WHERE last_message = 1 AND " \ f"active = 1 AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}' AND " \ f"category = '{category}' AND " \ f"type = '{ptype}'" else: query = f"SELECT prefix, length FROM {self.table_prefixes} WHERE last_message = 1 AND " \ f"active = 1 AND " \ f"mac = '{transaction.mac}' AND " \ f"duid = '{transaction.duid}' AND " \ f"iaid = '{transaction.iaid}' AND " \ f"category = '{category}' AND " \ f"type = '{ptype}'" answer = self.query(query) if answer is not None: if len(answer) == 0: return False else: return answer[0][0] else: return False def release_free_leases(self, now): """ release all invalid leases via setting their active flag to False """ query = f"UPDATE {self.table_leases} SET active = 0, last_message = 0 WHERE valid_until < '{now}'" return self.query(query) def release_free_prefixes(self, now): """ release all invalid prefixes via setting their active flag to False """ query = f"UPDATE {self.table_prefixes} SET active = 0, last_message = 0 WHERE valid_until < '{now}'" return self.query(query) def remove_leases(self, now, category="random"): """ remove all leases of a certain category like random - they will grow the database but be of no further use """ query = f"DELETE FROM {self.table_leases} WHERE active = 0 AND " \ f"category = '{category}' AND valid_until < '{now}'" return self.query(query) def remove_route(self, prefix): """ remove a route which is not used anymore """ query = f"DELETE FROM {self.table_routes} WHERE prefix = '{prefix}'" return self.query(query) def unlock_unused_advertised_leases(self, now): """ unlock leases marked as advertised but apparently never been delivered let's say a client should have requested its formerly advertised address after 1 minute """ query = f"UPDATE {self.table_leases} SET last_message = 0 WHERE last_message = 1 AND last_update < '{now + 60}'" return self.query(query) def unlock_unused_advertised_prefixes(self, now): """ unlock prefixes marked as advertised but apparently never been delivered let's say a client should have requested its formerly advertised address after 1 minute """ query = f"UPDATE {self.table_prefixes} SET last_message = 0 WHERE last_message = 1 AND " \ f"last_update < '{now + 60}'" return self.query(query) def build_config_from_db(self, transaction): """ get client config from db and build the appropriate config objects and indices """ if transaction.client_config_dicts is None: # add client config which seems to fit to transaction transaction.client_config_dicts = ClientConfigDicts() if self.config_prefix_support: # 'mac LIKE' is necessary if multiple MACs are stored in config DB query = f"SELECT hostname, mac, duid, class, address, prefix, id FROM {self.table_hosts} WHERE " \ f"hostname = '{transaction.hostname}' OR " \ f"mac LIKE '%{transaction.mac}%' OR " \ f"duid = '{transaction.duid}'" answer = self.query(query) # read all sections of config file # a section here is a host # lowering MAC and DUID information in case they where upper in database for host in answer: hostname, mac, duid, client_class, address, prefix, host_id = host # lower some attributes to comply with values from request if mac: mac = listify_option(mac.lower()) if duid: duid = duid.lower() if address: address = listify_option(address.lower()) if prefix: prefix = listify_option(prefix.lower()) transaction.client_config_dicts.hosts[hostname] = ClientConfig(hostname=hostname, mac=mac, duid=duid, client_class=client_class, address=address, prefix=prefix, host_id=host_id) # and put the host objects into index if transaction.client_config_dicts.hosts[hostname].MAC: for m in transaction.client_config_dicts.hosts[hostname].MAC: if m not in transaction.client_config_dicts.index_mac: transaction.client_config_dicts.index_mac[m] = [transaction.client_config_dicts.hosts[hostname]] else: transaction.client_config_dicts.index_mac[m]. \ append(transaction.client_config_dicts.hosts[hostname]) # add DUIDs to IndexDUID if transaction.client_config_dicts.hosts[hostname].DUID != '': if transaction.client_config_dicts.hosts[hostname].DUID not in transaction.client_config_dicts.index_duid: transaction.client_config_dicts.index_duid[transaction.client_config_dicts.hosts[hostname].DUID] = \ [transaction.client_config_dicts.hosts[hostname]] else: transaction.client_config_dicts.index_duid[transaction.client_config_dicts.hosts[hostname].DUID]. \ append(transaction.client_config_dicts.hosts[hostname]) # some cleaning del host, mac, duid, address, prefix, client_class, host_id else: # 'mac LIKE' is necessary if multiple MACs are stored in config DB query = f"SELECT hostname, mac, duid, class, address, id FROM {self.table_hosts} WHERE " \ f"hostname = '{transaction.hostname}' OR " \ f"mac LIKE '%{transaction.mac}%' OR " \ f"duid = '{transaction.duid}'" answer = self.query(query) # read all sections of config file # a section here is a host # lowering MAC and DUID information in case they where upper in database for host in answer: hostname, mac, duid, client_class, address, host_id = host # lower some attributes to comply with values from request if mac: mac = listify_option(mac.lower()) if duid: duid = duid.lower() if address: address = listify_option(address.lower()) transaction.client_config_dicts.hosts[hostname] = ClientConfig(hostname=hostname, mac=mac, duid=duid, client_class=client_class, address=address, host_id=host_id) # and put the host objects into index if transaction.client_config_dicts.hosts[hostname].MAC: for m in transaction.client_config_dicts.hosts[hostname].MAC: if m not in transaction.client_config_dicts.index_mac: transaction.client_config_dicts.index_mac[m] = [transaction.client_config_dicts.hosts[hostname]] else: transaction.client_config_dicts.index_mac[m]. \ append(transaction.client_config_dicts.hosts[hostname]) # add DUIDs to IndexDUID if transaction.client_config_dicts.hosts[hostname].DUID != '': if transaction.client_config_dicts.hosts[hostname].DUID not in transaction.client_config_dicts.index_duid: transaction.client_config_dicts.index_duid[transaction.client_config_dicts.hosts[hostname].DUID] = \ [transaction.client_config_dicts.hosts[hostname]] else: transaction.client_config_dicts.index_duid[transaction.client_config_dicts.hosts[hostname].DUID]. \ append(transaction.client_config_dicts.hosts[hostname]) # some cleaning del host, mac, duid, address, client_class, host_id def get_client_config_by_mac(self, transaction): """ get host and its information belonging to that mac """ hosts = list() mac = transaction.mac if mac in transaction.client_config_dicts.index_mac: hosts.extend(transaction.client_config_dicts.index_mac[mac]) return hosts else: return None def get_client_config_by_duid(self, transaction): """ get host and its information belonging to that DUID """ # get client config that most probably seems to fit hosts = list() duid = transaction.duid if duid in transaction.client_config_dicts.index_duid: hosts.extend(transaction.client_config_dicts.index_duid[duid]) return hosts else: return None def get_client_config_by_hostname(self, transaction): """ get host and its information by hostname """ hostname = transaction.hostname if hostname in transaction.client_config_dicts.hosts: return [transaction.client_config_dicts.hosts[hostname]] else: return None def get_client_config(self, hostname='', client_class='', duid='', address=[], mac=[], host_id=''): """ give back ClientConfig object """ return ClientConfig(hostname=hostname, client_class=client_class, duid=duid, address=address, mac=mac, host_id=host_id) def store_mac_llip(self, mac, link_local_ip, now): """ store MAC-link-local-ip-mapping """ query = f"SELECT mac FROM macs_llips WHERE mac='{mac}'" db_entry = self.query(query) # if known already update timestamp of MAC-link-local-ip-mapping if not db_entry or db_entry == []: query = f"INSERT INTO macs_llips (mac, link_local_ip, last_update) " \ f"VALUES ('{mac}', '{link_local_ip}', '{now}')" self.query(query) else: query = f"UPDATE macs_llips SET link_local_ip = '{link_local_ip}', last_update = '{now}' WHERE mac = '{mac}'" self.query(query) @clean_query_answer def get_dynamic_prefix(self): query = "SELECT item_value FROM meta WHERE item_key = 'dynamic_prefix'" return self.query(query) def store_dynamic_prefix(self, prefix): """ store dynamic prefix to be persistent after restart of dhcpy6d """ query = "SELECT item_value FROM meta WHERE item_key = 'dynamic_prefix'" db_entry = self.query(query) # if already existing just update dynamic prefix if not db_entry or db_entry == []: query = f"INSERT INTO meta (item_key, item_value) VALUES ('dynamic_prefix', '{prefix}')" self.query(query) else: query = f"UPDATE meta SET item_value = '{prefix}' WHERE item_key = 'dynamic_prefix'" self.query(query) def collect_macs_from_db(self): """ collect all known MACs and link local addresses from database at startup to reduce attempts to read neighbor cache """ query = f'SELECT link_local_ip, mac, last_update FROM {self.table_macs_llips}' answer = self.query(query) if answer: for m in answer: try: # m[0] is LLIP, m[1] is the matching MAC # interface is ignored collected_macs[m[0]] = NeighborCacheRecord(llip=m[0], mac=m[1], now=m[2]) except Exception as err: print(err) traceback.print_exc(file=sys.stdout) sys.stdout.flush() return None def db_query(self, query): """ no not execute query on DB - dummy """ # return empty tuple as dummy return () class DB(Store): """ MySQL and PostgreSQL database interface for robustness http://stackoverflow.com/questions/207981/how-to-enable-mysql-client-auto-re-connect-with-mysqldb """ connection = False cursor = False def __init__(self, query_queue, answer_queue): Store.__init__(self, query_queue, answer_queue) self.connection = None try: self.db_connect() except Exception as err: print(err) def db_connect(self): """ Connect to database server according to database type """ pass dhcpy6d-1.2.3/dhcpy6d/storage/textfile.py000066400000000000000000000153561437472361000203020ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import configparser from ..config import cfg from ..helpers import (decompress_ip6, error_exit, listify_option, convert_prefix_inline) from .store import (ClientConfig, Store) class Textfile(Store): """ client config in text files """ def __init__(self, query_queue, answer_queue): Store.__init__(self, query_queue, answer_queue) self.connection = None # store config information of hosts self.hosts = {} self.index_mac = {} self.index_duid = {} # store IDs for ID-based hosts to check if there are duplicates self.ids = {} # instantiate a Configparser config = configparser.ConfigParser() config.read(cfg.STORE_FILE_CONFIG) # read all sections of config file # a section here is a host for section in config.sections(): hostname = config[section]['hostname'].lower() # only if section matches hostname the following steps are of any use if section.lower() == hostname: self.hosts[hostname] = ClientConfig() for item in config.items(hostname): # lowercase all MAC addresses, DUIDs, IPv6 addresses and prefixes if item[0].upper() in ['ADDRESS', 'DUID', 'HOSTNAME', 'MAC', 'PREFIX']: self.hosts[hostname].__setattr__(item[0].upper(), str(item[1]).lower()) else: self.hosts[hostname].__setattr__(item[0].upper(), str(item[1])) # Test if host has ID if self.hosts[hostname].CLASS in cfg.CLASSES: for a in cfg.CLASSES[self.hosts[hostname].CLASS].ADDRESSES: if cfg.ADDRESSES[a].CATEGORY == 'id' and self.hosts[hostname].ID == '': error_exit(f"Textfile client configuration: No ID given " f"for client '{self.hosts[hostname].HOSTNAME}'") else: error_exit(f"Textfile client configuration: Class '{self.hosts[hostname].CLASS}' " f"of host '{self.hosts[hostname].HOSTNAME}' is not defined") if self.hosts[hostname].ID != '': if self.hosts[hostname].ID in list(self.ids.keys()): error_exit(f"Textfile client configuration: ID '{self.hosts[hostname].ID}' " f"of client '{self.hosts[hostname].HOSTNAME}' is already used " f"by '{self.ids[self.hosts[hostname].ID]}'.") else: self.ids[self.hosts[hostname].ID] = self.hosts[hostname].HOSTNAME # in case of various MAC addresses split them... self.hosts[hostname].MAC = listify_option(self.hosts[hostname].MAC) # in case of various fixed addresses split them and avoid decompressing of ':'... self.hosts[hostname].ADDRESS = listify_option(self.hosts[hostname].ADDRESS) # Decompress IPv6-Addresses if self.hosts[hostname].ADDRESS is not None: self.hosts[hostname].ADDRESS = [decompress_ip6(x) for x in self.hosts[hostname].ADDRESS] # in case of multiple supplied prefixes convert them to list self.hosts[hostname].PREFIX = listify_option(self.hosts[hostname].PREFIX) # split prefix into address and length, verify address if self.hosts[hostname].PREFIX is not None: self.hosts[hostname].PREFIX = [convert_prefix_inline(x) for x in self.hosts[hostname].PREFIX] # and put the host objects into index if self.hosts[hostname].MAC: for m in self.hosts[hostname].MAC: if m not in self.index_mac: self.index_mac[m] = [self.hosts[hostname]] else: self.index_mac[m].append(self.hosts[hostname]) # add DUIDs to IndexDUID if not self.hosts[hostname].DUID == '': if not self.hosts[hostname].DUID in self.index_duid: self.index_duid[self.hosts[hostname].DUID] = [self.hosts[hostname]] else: self.index_duid[self.hosts[hostname].DUID].append(self.hosts[hostname]) else: error_exit(f"Textfile client configuration: section [{section.lower()}] " f"does not match hostname '{hostname}'") # not very meaningful in case of databaseless textfile config but for completeness self.connected = True def get_client_config_by_mac(self, transaction): """ get host(s?) and its information belonging to that mac """ hosts = list() mac = transaction.mac if mac in self.index_mac: hosts.extend(self.index_mac[mac]) return hosts else: return None def get_client_config_by_duid(self, transaction): """ get host and its information belonging to that DUID """ hosts = list() duid = transaction.duid if duid in self.index_duid: hosts.extend(self.index_duid[duid]) return hosts else: return None def get_client_config_by_hostname(self, transaction): """ get host and its information by hostname """ hostname = transaction.hostname if hostname in self.hosts: return [self.hosts[hostname]] else: return None def get_client_config(self, hostname='', client_class='', duid='', address=[], mac=[], host_id=''): """ give back ClientConfig object """ return ClientConfig(hostname=hostname, client_class=client_class, duid=duid, address=address, mac=mac, host_id=host_id) dhcpy6d-1.2.3/dhcpy6d/threads.py000066400000000000000000000236341437472361000164420ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import subprocess import sys from threading import Thread import time import traceback import dns.query import dns.resolver import dns.reversename import dns.update from .config import cfg from .globals import (collected_macs, dns_query_queue, keyring, requests, requests_blacklist, route_queue, resolver_update, timer, transactions) from .helpers import colonify_ip6 from .log import log from .storage import volatile_store class DNSQueryThread(Thread): """ thread for updating DNS entries of valid leases without blocking main thread """ def __init__(self): Thread.__init__(self, name='DNSQuery') self.setDaemon(True) def run(self): # wait for new queries in queue until the end of the world while True: action, hostname, a = dns_query_queue.get() # colonify address for DNS address = colonify_ip6(a.ADDRESS) try: # update AAAA record, delete old entry first update = dns.update.Update(a.DNS_ZONE, keyring=keyring) update.delete(hostname, 'AAAA') # if DNS should be updated do it - not the case if IP is released if action == 'update': update.add(hostname, a.DNS_TTL, 'AAAA', address) dns.query.tcp(update, cfg.DNS_UPDATE_NAMESERVER) # the reverse record will be first checked if it points # to the current hostname, if not, it will be deleted first update_rev = dns.update.Update(a.DNS_REV_ZONE, keyring=keyring) try: answer = resolver_update.query(dns.reversename.from_address(address), 'PTR') for rdata in answer: hostname_ns = str(rdata).split('.')[0] # if ip address is related to another host delete this one if hostname_ns != hostname: update_rev.delete(dns.reversename.from_address(address), 'PTR', hostname_ns + '.' + a.DNS_ZONE + '.') except dns.resolver.NXDOMAIN: log.error(f'Received NXDOMAIN when trying to resolve {address}') # if DNS should be updated do it - not the case if IP is released if action == 'update': update_rev.add(dns.reversename.from_address(address), a.DNS_TTL, 'PTR', hostname + '.' + a.DNS_ZONE + '.') elif action == 'release': update_rev.delete(dns.reversename.from_address(address), 'PTR') dns.query.tcp(update_rev, cfg.DNS_UPDATE_NAMESERVER) except Exception as err: traceback.print_exc(file=sys.stdout) sys.stdout.flush() log.error('DNSUPDATE: ' + str(err)) class TidyUpThread(Thread): """ clean leases and transactions if obsolete """ def __init__(self): Thread.__init__(self, name='TidyUp') self.setDaemon(True) def run(self): try: # counter for database cleaning interval dbcount = 0 # get and delete invalid leases while True: # transaction data can be deleted after transaction is finished for transaction in list(transactions.values()): try: if timer.time > transaction.timestamp + cfg.CLEANING_INTERVAL * 10: transactions.pop(transaction.client_llip + transaction.id) except Exception as err: log.error(f'TidyUp: transaction {str(err)} has already been deleted') traceback.print_exc(file=sys.stdout) sys.stdout.flush() # if disconnected try reconnect if not volatile_store.connected: volatile_store.db_connect() else: # cleaning database once per minute should be enough if dbcount > 60 // cfg.CLEANING_INTERVAL: # remove leases which might not be recycled like random addresses for example volatile_store.remove_leases(timer.time, 'random') # set leases and prefixes free whose valid lifetime is over volatile_store.release_free_leases(timer.time) volatile_store.release_free_prefixes(timer.time) # unlock advertised leases and prefixes remaining volatile_store.unlock_unused_advertised_leases(timer.time) volatile_store.unlock_unused_advertised_prefixes(timer.time) # remove routes with inactive prefixes self.check_routes() # check for brute force clients and put them into blacklist if necessary self.check_requests(timer.time) dbcount = 0 dbcount += 1 # clean collected MAC addresses after 300 seconds # some Linuxes seem to be pretty slow and run out of the previous 30 seconds if not cfg.CACHE_MAC_LLIP: timestamp = timer.time for record in list(collected_macs.values()): if record.timestamp + 60 * cfg.CLEANING_INTERVAL < timestamp: if cfg.LOG_MAC_LLIP: log.info(f'deleted mac {record.mac} for llip {colonify_ip6(record.llip)}') collected_macs.pop(record.llip) del timestamp time.sleep(cfg.CLEANING_INTERVAL) except: traceback.print_exc(file=sys.stdout) sys.stdout.flush() @staticmethod def check_routes(): """ remove routes with inactive prefixes thanks to PyCharm this might be a @staticmethod """ for prefix in volatile_store.get_inactive_prefixes(): length, router, pclass = volatile_store.get_route(prefix) # hopefully the class stored in database still exists if pclass in cfg.CLASSES: route_queue.put(('down', cfg.CLASSES[pclass].CALL_DOWN, prefix, length, router)) @staticmethod def check_requests(now): """ check for brute force clients and put them into blacklist if necessary get time as now from caller dito here regarding MAC addresses """ # clean blacklist for client in list(requests_blacklist.keys()): if now > requests_blacklist[client].timestamp + cfg.REQUEST_LIMIT_RELEASE_TIME: log.info(f"Releasing client {client} from blacklist") requests_blacklist.pop(client) # clean default requests list for client in list(requests.keys()): if now > requests[client].timestamp + cfg.REQUEST_LIMIT_TIME: if requests[client].count > cfg.REQUEST_LIMIT_COUNT: log.info(f"Blacklisting client {client} after {requests[client].count} requests") requests_blacklist[client] = requests.pop(client) else: requests.pop(client) class RouteThread(Thread): """ thread for updating routes without blocking main thread """ def __init__(self, route_queue): Thread.__init__(self, name='Route') self.setDaemon(True) self.route_queue = route_queue def run(self): """ wait for new queries in queue until the end of the world """ while True: mode, call, prefix, length, router = self.route_queue.get() call_real = call.replace('$prefix$', colonify_ip6(prefix)). \ replace('$length$', str(length)). \ replace('$router$', colonify_ip6(router)). \ replace('$mode$', mode) # subprocess needs list as argument which it gets by split() try: result = subprocess.call(call_real.split(' ')) except Exception as err: result = err # ignore result to avoid routes being set but not noted in database when a command like # 'ip -6 route delete' gives a return code not 0 because a route already exists if mode == 'up': volatile_store.store_route(prefix, length, router, timer.time) if mode == 'down': volatile_store.remove_route(prefix) log.info(f"Called '{call_real}' to modify route - result: {result}") class TimerThread(Thread): """ thread for timer, used in different places """ def __init__(self): Thread.__init__(self, name='Timer') self.setDaemon(True) def run(self): while True: # set globally available time here to new value timer.time = time.time() time.sleep(1) dhcpy6d-1.2.3/dhcpy6d/transaction.py000066400000000000000000000140701437472361000173270ustar00rootroot00000000000000# DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from .config import cfg from .constants import CONST from .globals import (DUMMY_IAID, DUMMY_MAC, EMPTY_OPTIONS, IGNORED_LOG_OPTIONS, timer) from .helpers import (colonify_ip6, combine_prefix_length, split_prefix) from .options import OPTIONS class Transaction: """ all data of one transaction, to be collected in Transactions """ def __init__(self, transaction_id, client_llip, interface, message_type, options): # Transaction ID self.id = transaction_id # Link Local IP of client self.client_llip = client_llip # Interface the request came in self.interface = interface # MAC address self.mac = DUMMY_MAC # last message for following the protocol self.last_message_received_type = message_type # dictionary for options self.options_raw = options # default dummy OptionsRequest self.options_request = list() # timestamp to manage/clean transactions self.timestamp = timer.time # dummy hostname self.fqdn = '' self.hostname = '' # DNS Options for option 39 self.dns_n = 0 self.dns_o = 0 self.dns_s = 0 # dummy IAID self.iaid = DUMMY_IAID # dummy IAT1 self.iat1 = cfg.T1 # dummy IAT2 self.iat2 = cfg.T2 # IA option - NA, TA or PD -> DHCPv6 option 3, 4 or 25 # to be used in option_requests in Handler.build_response() self.ia_options = [] # Addresses given by client, for example for RENEW or RELEASE requests self.addresses = [] # same with prefixes self.prefixes = [] # might be used against clients that are running wild # initial 1 as being increased after handling self.counter = 1 # temporary storage for client configuration from DB config # - only used if config comes from DB self.client_config_dicts = None # client config from config store self.client = None # Vendor Class Option self.vendor_class_en = None self.vendor_class_data = '' # Rapid Commit flag self.rapid_commit = False # answer type - take from class definition, one of 'normal', 'noaddress', 'noprefix' or 'none' # defaults to 'normal' as this is the main purpose of dhcpy6d self.answer = 'normal' # default DUID value self.duid = '' # Elapsed Time - option 8, at least sent by WIDE dhcp6c when requesting delegated prefix self.elapsed_time = 0 # Client architecture type (RFC 5970) self.client_architecture = '' # Known client architecture type (RFC 4578) (e.g. EFI x86 - 64) self.known_client_architecture = '' # UserClass (https://tools.ietf.org/html/rfc3315#section-22.15) self.user_class = '' # if the options have some treatment for transactions just apply it if there is an defined option # if ta options are discovered here, the ia_options value of this transaction instance will be set for option in options: if option in OPTIONS: OPTIONS[option].initialize(transaction=self, option=options[option]) def get_options_string(self): """ get all options in one string for debugging """ options_string = '' # put own attributes into a string options = sorted(self.__dict__.keys()) # options.sort() for option in options: # ignore some attributes if option not in IGNORED_LOG_OPTIONS and \ not self.__dict__[option] in EMPTY_OPTIONS: if option == 'addresses': if (CONST.OPTION.IA_NA or CONST.OPTION.IA_TA) in self.ia_options: option_string = f'{option}:' for address in self.__dict__[option]: option_string += f' {colonify_ip6(address)}' options_string = f'{options_string} | {option_string}' elif option == 'prefixes': if CONST.OPTION.IA_PD in self.ia_options: option_string = f'{option}:' for p in self.__dict__[option]: prefix, length = split_prefix(p) option_string += combine_prefix_length(colonify_ip6(prefix), length) elif option == 'client_llip': option_string = f'{option}: {colonify_ip6(self.__dict__[option])}' options_string = f'{options_string} | {option_string}' elif option == 'mac': if self.__dict__[option] != DUMMY_MAC: # option_string = f'{option}: {str(self.__dict__[option])}' option_string = f'{option}: {self.__dict__[option]}' options_string = f'{options_string} | {option_string}' else: # option_string = f'{option}: {str(self.__dict__[option])}' option_string = f'{option}: {self.__dict__[option]}' options_string = f'{options_string} | {option_string}' return options_string dhcpy6d-1.2.3/doc/000077500000000000000000000000001437472361000136325ustar00rootroot00000000000000dhcpy6d-1.2.3/doc/LICENSE000066400000000000000000000431031437472361000146400ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. dhcpy6d-1.2.3/doc/clients-example.conf000066400000000000000000000011571437472361000175770ustar00rootroot00000000000000# These are some example clients. Every section is a client. # Every client has to have a hostname, a class and at least # one of mac or duid to be identified depending on class definition. # # The option attribute "id" can be used for address definitions of # category "id". [client1] hostname = client1 mac = 01:01:01:01:01:01 class = valid_client [client2] hostname = client2 mac = 02:02:02:02:02:02 class = invalid_client [client3] hostname = client3 mac= 03:03:03:03:03:03 duid = 000100011234567890abcdef1234 class = valid_client [client4] hostname = client4 mac = 04:04:04:04:04:04 id = 4 class = valid_client dhcpy6d-1.2.3/doc/config.sql000066400000000000000000000004431437472361000156210ustar00rootroot00000000000000CREATE TABLE hosts ( hostname varchar(255) NOT NULL, mac varchar(1024) DEFAULT NULL, class varchar(255) DEFAULT NULL, address varchar(255) DEFAULT NULL, prefix varchar(255) DEFAULT NULL, id varchar(255) DEFAULT NULL, duid varchar(255) DEFAULT NULL, PRIMARY KEY (hostname) ); dhcpy6d-1.2.3/doc/dhcpy6d-clients.conf.rst000066400000000000000000000110341437472361000203070ustar00rootroot00000000000000==================== dhcpy6d-clients.conf ==================== ---------------------------------------------------- Clients configuration file for DHCPv6 server dhcpy6d ---------------------------------------------------- :Author: Copyright (C) 2012-2022 Henri Wahl :Date: 2022-06-14 :Version: 1.2.2 :Manual section: 5 :Copyright: This manual page is licensed under the GPL-2 license. Description =========== This file contains all client configuration data if these options are set in **dhcpy6d.conf**: **store_config = file** and **store_file_config = /path/to/dhcpy6d-clients.conf** An alternative method to store client configuration is using database storage with SQLite or MySQLor PostgreSQL databases. Further details are available at ``_. This file follows RFC 822 style parsed by Python ConfigParser module. Some options allow multiple values. These have to be separated by spaces. Client sections =============== **[host_name]** 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*. Client attributes ================= 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*. Both of the following 2 attributes are necessary - the **class** and at least one of the others. Mandatory client attribute 'class' ------------------------------------- **class = ** Every client needs a class. If a client is identified, it depends from its class, which addresses it will get. This relation is configured in *dhcpy6d.conf*. Semi-mandatory client attributes -------------------------------- Depending on **identification** in *dhcpy6d.conf* clients need to have the corresponding attributes. At least one of them is needed. **mac = ** The MAC address of the Link Local Address of the client DHCPv6 request, formatted like the most usual 01:02:03:04:05:06. **duid = ** The DUID of the client which comes with the DHCPv6 request message. No hex and \\ needed, just like for example 000100011234567890abcdef1234 . **hostname = ** The client non-FQDN hostname. It will be used for dynamic DNS updates. Extra attributes ---------------- These attributes do not serve for identification of a client but for appropriate address generation. **id = ** **id** has to be a hex number in the range 0-FFFF. The client ID from this directive will be inserted in the *address pattern* of category **id** instead of the **$id$** placeholder. **address =
[
...]** Addresses configured here will be sent to a client in addition to the ones it gets due to its class. Might be useful for some extra static address definitions. **prefix = [ ...]** Prefix configured here will be sent to client in addition to the ones it gets due to its class. Examples ======== The next lines contain some example client definitions: | [client1] | hostname = client1 | mac = 01:01:01:01:01:01 | class = valid_client | [client2] | hostname = client2 | mac = 02:02:02:02:02:02 | class = invalid_client | [client3] | hostname = client3 | duid = 000100011234567890abcdef1234 | class = valid_client | address = 2001:db8::babe:1 | [client4] | hostname = client4 | mac = 04:04:04:04:04:04 | id = 1234 | class = valid_client | [client5] | hostname = client5 | mac = 01:01:01:01:01:02 | class = valid_client | prefix = 2001:db8::/48 License ======= This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA On Debian systems, the full text of the GNU General Public License version 2 can be found in the file */usr/share/common-licenses/GPL-2*. See also ======== * dhcpy6d(8) * dhcpy6d.conf(5) * ``_ * ``_ dhcpy6d-1.2.3/doc/dhcpy6d-example.conf000066400000000000000000000221331437472361000174740ustar00rootroot00000000000000# dhcpy6d example configuration # # The first section [dhcpy6d] contains general options. # All sections whose name starts with "address_" are address # definitions. These are used in the sections named something # like "class_". These contain definitions for classes of clients. # The membership of clients of a class is defined in the client # configuration from client config file or config database. # Addresses contain various properties best seen on examples # down below. Classes contain extra properties like nameservers # for clients and filters. # There is one predefined class: "default". If not set in a # [class_default] section all clients which have no configuration # or match no filter are automatically of this class. If # [class_default] is not set the address "default" is used which # also can be defined in an [address_default] section. [dhcpy6d] # GENERAL OPTIONS # Server interface - multiple interfaces have to be separated by spaces. interface = eth0 # Server DUID - if not set there will be one generated every # time dhcpy6d starts. This might cause trouble for Windows # clients because they go crazy about the changed server DUID. # Please note that the commandline argument --duid overrides this # setting. This is the case in Debian /etc/init.d/dhcpy6d script # which uses the generated DUID value from /etc/default/dhcpy6d. #serverduid = 0001000100000000000000000000 # Server preference is 255 as default. #server_preference = 255 # non-privileged user/group user = dhcpy6d group = dhcpy6d # Nameserver for option 23 - there can be several specified # separated by spaces. nameserver = fd01:db8::53 # Domain to be used for option 39 - host FQDN domain = local # Domain search list for option 24 - domain search list. If omited the value # of option "domain" above is taken as default domain_search_list = foo.com bar.com # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = dhcpy6d.log # Log to syslog daemon log_syslog = no # Syslog facility log_syslog_facility = daemon # A remote server syslog socket or a local unix socket log_syslog_destination = remote-server:514 # Log discovered MAC/LLIP pairs of clients log_mac_llip = no # Configuration of clients can be stored in text file or in MySQL or # SQLite database. See delivered config.sql and volatile.sql for # database schemes. # Use for small environment could be to get config from text file # and store leases in SQLite database. Larger setups might have use # for config and volatile data in MySQL database. # Store config type is one of "file", "mysql", "sqlite" or "none". # if "none" no client configuration is used. store_config = file # Dito for store volatile data like leases and MAC-LLIPs-mapping - # one of "mysql", "postgresql" or "sqlite". store_volatile = sqlite # Path to file used for configuration of clients. store_file_config = clients.conf # Data used for MySQL storage # host store_db_host = localhost # database store_db_db = dhcpy6d # user store_db_user = dhcpy6d # password store_db_password = dhcpy6d # Paths to SQLite database files. # config.sqlite and volatile.sqlite are included in source folder. store_sqlite_config = config.sqlite store_sqlite_volatile = volatile.sqlite # Authentication information needed for reconfigure requests does # not work so it can safely be ignored. # If it would work it had to be some 128 bit key. #authentication_information = 00000000000000000000000000000000 # Flag to let dhcpy6d really answer to client requests - # might be of use for debugging and testing. really_do_it = no # Declare which attributes of a requesting client should be checked # to prove its identity. Default is "mac", but "duid" and "hostname" # are allowed too. It is even possible to mix them, separated by # spaces. #identification = mac duid hostname identification = mac # Declare if all checked attributes have to match or is it enough if # some do. Options are "match_all" and "match_some". The latter # might be interesting if there are some dualboot clients whose MAC # addresses match but their DUIDs don't. identification_mode = match_all # DYNAMIC DNS UPDATES # This works at the moment only for ISC Bind nameservers. # Do dynamic DNS updates. Default is "no". dns_update = no # RNDC key name for DNS Update. dns_rndc_key = rndc-key # RNDC secret - mostly some MD5-hash. Take it from # nameservers' /etc/rndc.key. dns_rndc_secret = 0000000000000000000 # Nameserver to talk to. dns_update_nameserver = ::1 # Regarding RFC 4704 5. there are 3 kinds of client behaviour # for N O S bits: # - client wants to update DNS itself -> sends 0 0 0 # - client wants server to update DNS -> sends 0 0 1 # - client wants no server DNS update -> sends 1 0 0 # Ignore client ideas about DNS (if at all, what name to use, # self-updating...) dns_ignore_client = yes # Use client supplied hostname - yes or no. It is no problem to # override client desires. dns_use_client_hostname = no # IA_NA/IA_TA OPTIONS # These lifetimes are also used as default for addresses which # have no extra defined lifetimes. # Lifetimes can be defined in address definitions. # RENEW (T1) and REBIND (T2) timers can be defined in # class definitions. # Default preferred lifetime in seconds preferred_lifetime = 43200 # default valid lifetime in seconds valid_lifetime = 64800 # T1 t1 = 21600 # T2 t2 = 32400 # information refresh time for option 32 information_refresh_time = 3600 # DEFINITION OF AVAILABLE ADDRESSES # Addresses are defined by patterns of static and variable parts. # # There are different categories: "random", "range", "id", "mac": # # $random64$ - calculate random 64 bit interface identifier address # part. maybe future a version will allow shorter random # $range$ - use range addresses - only in the last octet of address # $id$ - if configuration of clients contain some kind of ID # it can be used for one octet # $mac$ - puts MAC address into 3 octets - works only on local subnet # # Categories and variables used in pattern must match! # The two options every address definition must have are category # and pattern. # 1. Example: definition of a normal locally and globally connected # valid client # a globally unique address [address_global] # For privacy a global address might better be randomly created. category = random # This pattern results in an address like this: # 2001:0db8:0000:0000:d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # IA type is mostly non-temporary as default so it is not necessary # to declare here. ia_type = na # Lifetimes can be set in seconds for every defined address. preferred_lifetime = 32400 valid_lifetime = 43200 # A unique local address [address_local_valid] # For easier internal management put MAC address into address. category = mac # Given MAC 01:02:03:04:05:06 this pattern results in an address # like this: fd01:db8:0000:0000:babe:0102:0304:0506. pattern = fd01:db8::babe:$mac$ # Update these addresses in Bind DNS - defaults to "no" dns_update = yes # Zone to update. dns_zone = example.com # Reverse zone to update dns_rev_zone = 1.0.d.f.ip6.arpa # Define a class for normal valid clients. [class_valid_client] # These clients get 2 Addresses, one internal ULA and one global. # Different addresses should be separated by spaces. # Note that "address_" from address definition section is omitted # here! addresses = global local_valid # Some internal example nameserver. nameserver = fd01:db8::53 # 2. Example: definition of a class for invalid clients [address_local_invalid] # Invalid clients will get addresses of a range. category = range # Definition of range. range = 1000-1fff # Local address for invalid clients will get another prefix # Resulting addresses look like # fd01:0db8:0bad:0000:0000:0000:0000:1000 pattern = fd01:db8:bad::$range$ # Lifetimes of address are shorter for faster reaction to status # changes. preferred_lifetime = 2700 valid_lifetime = 3600 # Class for invalid clients [class_invalid_client] addresses = local_invalid # Extra nameserver for invalid clients. nameserver = fd01:db8:bad::53 # Short interval of address refresh attempts. t1 = 600 t2 = 900 # 3. Example: definition of filtered clients [address_filtered] # Filtered clients will get addresses of a range. category = range # Definition of range. range = 1000-1fff # Local address for filtered clients will get another prefix # Resulting addresses look like # fd01:0db8:0000:0000:babe:0000:0000:1000 pattern = fd01:db8::babe:0:0:$range$ [class_filtered_clients] addresses = filtered # Filters are regular expessions. # See http://docs.python.org/howto/regex.html # There are three types of filters allowed: # filter_hostname # filter_mac # filter_duid # With this setting all clients which transmit a hostname starting # with "windows" will get an address of range # fd01:db8::beef:0:0:1000 to fd01:db8::beef:0:01fff filter_hostname = windows.* # 4. Example: default addresses for all unknown clients # It should be enough if address_default is defined, only if # unknown clients should get # extra nameservers etc. a class_default has to be set. [address_default] category = mac # Given MAC 01:02:03:04:05:06 this pattern results in an # address like this: fd01:db8:dead:0bad:beef:0102:0304:0506. pattern = fd01:db8:dead:bad:beef:$mac$ dhcpy6d-1.2.3/doc/dhcpy6d-minimal.conf000066400000000000000000000010771437472361000174730ustar00rootroot00000000000000# dhcpy6d minimal example configuration [dhcpy6d] # Interface to listen to multicast ff02::1:2. interface = eth1 # Do not identify and configure clients. store_config = none # SQLite DB for leases and LLIP-MAC-mapping. store_volatile = sqlite store_sqlite_volatile = volatile.sqlite # Not really necessary but might help for debugging. log = on log_console = on # Special address type which applies to all not specially # configured clients. [address_default] # Choosing MAC-based addresses. category = mac # ULA-type address pattern. pattern = fd01:db8:dead:bad:beef:$mac$dhcpy6d-1.2.3/doc/dhcpy6d.conf.rst000066400000000000000000001274551437472361000166670ustar00rootroot00000000000000============ dhcpy6d.conf ============ -------------------------------------------- Configuration file for DHCPv6 server dhcpy6d -------------------------------------------- :Author: Copyright (C) 2012-2022 Henri Wahl :Date: 2022-06-14 :Version: 1.2.2 :Manual section: 5 :Copyright: This manual page is licensed under the GPL-2 license. Description =========== This file contains the general settings for DHCPv6 server daemon dhcpy6d. It follows RFC 822 style parsed by Python ConfigParser module. It contains several sections which will be discussed in detail here. An online documentation is also available at ``_. Boolean settings can be set with *1|0*, *on|off* or *yes|no* values. Some options allow multiple values. These have to be separated by spaces. There are 5 types of sections: **[dhcpy6d]** This section contains general options like interfaces, storage and logging. Only one [dhcpy6d] section is allowed. **[address_]** There can be various *[address_]* sections. In several address sections several address ranges and types can be defined. Addresses are organized in classes. For details read further down. **[prefix_]** There can be various *[prefix_]* sections. In several prefix sections several prefix ranges and types can be defined. Prefixes are organized in classes. For details read further down. **[class_]** Class definitions allow one to apply different addresses, time limits et al. to different types of clients. **[bootfile_]** There can be various *[bootfile_]* sections. In several bootfile sections several tftp bootfile urls with restrictions to CPU architecture and user class supplied by the PXE client can be defined. General configuration in section [dhcpy6d] ========================================== This section contains important general options. Values are sometimes examples and not meant to be used in production environments. **really_do_it = yes|no** Let dhcpy6d **really do it** and respond to client requests - disabling might be of use for debugging and testing. *Default: no* **interface = [ ...]** The interfaces the server listens on is defined here. Multiple interfaces have to be separated by spaces. **exclude_interface = [ ...]** The interfaces the server does not listen on. Multiple interfaces have to be separated by spaces. All interfaces not mentioned here will be used for listening. Added in version 1.2.0. **serverduid = ** The server DUID should be configured with serverduid. If there is none dhcpy6d creates a new one at every startup. Windows clients might run a little bit wild when server DUID changed. You are free to compose your own as long as it follows RFC 3315. Please note that it has to be in hexadecimal format - no octals, no "-", just like in the example below. The example here is a DUID-LLT (Link-layer Address Plus Time) even if it should be a DUID-TLL as timestamp comes first. It is composed of *DUID-type(LLT=1)* + *Hardware-type(Ethernet=1)* + *Unixtime-in-hexadecimal* + *MAC-address* what makes a *0001* + *0001* + *11fb5dc9* + *01023472a6c5* = **0001000111fb5dc901023472a6c5**. **server_preference = <0-255>** The server preference determines the priority of the server. The maximum value is 255 which means highest priority. *Default: 255* **user = ** For security reasons dhcpy6d can and should be run as non-root user. *Default: root* **group = ** For security reasons dhcpy6d can and should be run as non-root group. *Default: root* **nameserver = [ ...]** If an address type is of category *dns* at least one nameserver has to be given here. If more than one is needed they have to be separated by spaces. **domain = ** The domain to be used with FQDN hostnames for option 39. **domain_search_list = [ ...]** Domain search lists to be used with option 24. If none is given the value of domain above is used. Multiple domains have to be separated by space or comma. **ntp_server = [ ...]** NTP servers to be used. can be unicast addresses, multicast addresses or FQDNs following RFC 5908 for DHCPv6 option 56. **log = yes|no** Enable logging. *Default: no* **log_console = yes|no** Log to the console where **dhcpy6d** has been started. *Default: no* **log_file = ** Defines the file used for logging. Will be created if it does not yet exist. **log_syslog = yes|no** If logs should go to syslog it is set here. *Default: no* **log_syslog_destination = syslogserver** An UDP syslog server may be used if **log_syslog_destination** points to it. Optionally a port other than default 514 can be set when adding ":" to the destination. **log_syslog_facility = ** The default syslog facility is *daemon* but can be changed here. *Default: daemon* **log_mac_llip = yes|no** Log discovered MAC/LLIP pairs of clients. Might be pretty verbose in larger setups and with disabled MAC/LLIP pair caching. *Default: no* **store_config = file|sqlite|mysql|postgresql|none** Configuration of clients can be stored in a file or in a database. Databases MySQL, PostgreSQL and SQLite are supported at the moment, thus possible values are *file*, *mysql*, *postgresql* or *sqlite*. To disable any client configuration source it has to be *none*. *Default: none* **store_file_config = ** File which contains the clients configuration. For details see **dhcpy6d-clients.conf(5)**. *Default: /etc/dhcpy6d-clients.conf* **store_sqlite_config = /path/to/sqlite/config/file** SQLite database file which contains the clients configuration. *Default: config.sqlite* **store_volatile = sqlite|mysql|postgresql** Volatile data like leases and the mapping between Link Local addresses and MAC addresses can be stored in MySQL, PostgreSQL or SQLite database, so the possible values are *mysql*, *postgresql* and *sqlite*. **store_sqlite_volatile = /path/to/sqlite/volatile/file** If set to *sqlite* a SQLite database file must be defined. *Default: /var/lib/dhcpy6d/volatile.sqlite* **store_db_host = ** **store_db_db = ** **store_db_user = ** **store_db_password = ** If **store_config** and/or **store_volatile** use a database to store information it has to be set with these self-explanatory options. The same database is used for config and volatile data. **cache_mac_llip = yes|no** Cache discovered MAC/LLIP pairs in database. If enabled reduces response time and opens dhcpy6d to *possible* MAC/LLIP poisoning. If disabled might increase system load. *Default: no* **identification = ** Clients can be set to be identified by several attributes - MAC address, DUID or hostname. At least one of mac, duid or hostname is necessary. Hostname is the one sent in client request with DHCPv6 option 39. Identification is used to get the correct settings for the client from config file or database. Same MAC and different DUIDs might be interesting for clients with multiple OS. *Default: mac* **identification_mode = match_all|match_some** If more than one identification attribute has been set, the identification mode can be one of *match_all* or *match_some*. The first means that all attributes have to match to identify a client and the latter is more tolerant. *Default: match_all* **ignore_mac = yes|no** If serving only for delivering addresses regardless of classes (e.g. on PPP interface) MACs do not need to be investigated. **dns_update = yes|no** Dynamically update DNS. This works at the moment only with Bind DNS, but might be extended to others, maybe via call of an external command. *Default: no* **dns_update_nameserver = [ ...]** **dns_use_rndc = yes|no** DNS updates might be able without RNDC key but this is not advised. *Default: yes* **dns_rndc_key = ** **dns_rndc_secret = ** *Default: 5400* **valid_lifetime = ** *Default: 7200* **t1 = ** *Default: 2700* **t2 = ** Preferred lifetime, valid lifetime, T1 and T2 in seconds are configured with the corresponding options. *Default: 4050* **information_refresh_time = ** The lifetime of information given to clients as response to an *information-request* message. *Default: 6000* **ignore_iaid = yes|no** Ignore IAID when looking for leases in database. Might be of use in case some clients are changing their IAD for some unknown reason. *Default: no* **ignore_unknown_clients = yes|no** Ignore clients if no trace of them can be found in the neighbor cache. *Default: yes* **request_limit = yes|no** Enables request limits for clients which can be controlled by *request_limit_time* and *request_limit_count*. *Default: no* **request_limit_identification = mac|llip** Identifies clients either by MAC address or Link Local IP. *Default: llip* **request_limit_time = ** *Default: 60* **request_limit_count = ** Requests can be limited to avoid server to be flooded by buggy clients. Set number of request during a certain time in seconds. *Default: 20* **request_limit_release_time = ** Duration in seconds for brute force clients to stay on the blacklist. *Default: 7200* **manage_routes_at_start = yes|no** Check prefixes at startup and call commands for adding and deleting routes respectively. *Default: no* Address definitions in multiple [address_] sections ================================================================= The ** part of an **[address_]** section is an arbitrarily chosen identifier like *clients_global* or *invalid_clients_local*. There can be many address definitions which will be used by classes. Every address definition may include several properties: **category = mac|eui64|id|range|random|fixed|dns** Categories play an important role when defining patterns for addresses. An address belongs to a certain category: **mac** Uses MAC address from client request as part of address **eui64** Also uses MAC address from client as part of address, but converts it to a 64-bit extended unique identifier (EUI-64) **id** Uses ID given to client in configuration file or database as one octet of address, should be in range 0-ffff **range** Generate addresses of given range like 0-ffff **random** Randomly created 64 bit values used as host part in address **fixed** Use addresses from client configuration only. **dns** Ask DNS server for IPv6 address of client host **range = -** Sets range for addresses of category *range*. **from** Starting hex number of range, minimum is 0 **to** Maximum hex limit of range, highest is ffff. **pattern = 2001:db8::$mac$|$id$|$range$|$random$** **pattern= $prefix$|$mac$|$eui64$|$id$|$range$|$random$** Patterns allow one to design the addresses according to their category. See examples section below to make it more clear. **$mac$** The MAC address from the DHCPv6 request's Link Local Address found in the neighbor cache will be inserted instead of the placeholder. It will be stretched over 3 thus octets like 00:11:22:33:44:55 become 0011:2233:4455. **$eui64$** The MAC address converted to a modified 64-bit extended unique identifier (EUI-64) from the DHCPv6 request's Link Local Address found in the neighbor cache will be inserted instead of the placeholder. It will be converted according to RFC 4291 like 52:54:00:e5:b4:64 become 5054:ff:fee5:b464 **$id$** If clients get an ID in client configuration file or in client configuration database this ID will fill one octet. Thus the ID has to be in the range of 0000-ffff. **$range$** If address is of category range the range defined with extra keyword *range* will be used here in place of one octet.This is why the range can span from 0000-ffff. Clients will get an address out of the given range. **$random64$** A 64 bit random address will be generated in place of this variable. Clients get a random address just like they would if privacy extensions were used. The random part will span over 4 octets. **$prefix** This placeholder can be used instead of a literal prefix and uses the prefix given at calling dhcpy6d via the *--prefix* argument like *$prefix$::$id$*. **ia_type = na|ta** IA (Identity Association) types can be one of non-temporary address *na* or temporary address *ta*. Default and probably most used is *na*. *Default: na* **preferred_lifetime = ** **valid_lifetime = ** As default preferred and valid lifetime are set in general settings, but it is configurable individually for every address setting. **dns_update = yes|no** *Default: no* **dns_zone = ** **dns_rev_zone = ** If these addresses should be synchronized with Bind DNS, these three settings have to be set accordingly. The nameserver for updates is set in general settings. Default Address --------------- The address scheme used for the default class *class_default* is by default named *address_default*. It should be enough if *address_default* is defined, only if unknown clients should get extra nameservers etc. a *class_default* has to be set. **[address_default]** Address scheme used as default for clients which do not match any other class than *class_default*. Prefix definitions in multiple [prefix_] sections ============================================================== The ** part of an **[prefix_]** section is an arbitrarily chosen identifier like *customers*. A prefix definition may contain several properties: **category = range** Like addresses prefix have a category. Right now only *range* seems to make sense, similar to ranges in addresses being like 0-ffff. **range = -** Sets range for prefix of category *range*. **from** Starting hex number of range, minimum is 0 **to** Maximum hex limit of range, highest is ffff. **pattern = 2001:db8:$range$::** **pattern= $prefix$:$range$::** Patterns allow one to design the addresses according to their category. See examples section below to make it more clear. **$range$** If address is of category range the range defined with extra keyword *range* will be used here in place of one octet. This is why the range can span from 0000-ffff. Clients will get an address out of the given range. **length = ** Length of prefix given out to clients. **preferred_lifetime = ** **valid_lifetime = ** As default preferred and valid lifetime are set in general settings, but it is configurable individually for every prefixk setting. **route_link_lokal = yes|no** As default Link Local Address of requesting client is not used as router address for external call. Instead the client should be able to retrieve exactly 1 address from server to be used as router for the delegated prefix. Alternatively the client Link Local Address might be used by enabling this option. *Default: no* Class definitions in multiple [class_] sections =========================================================== The ** part of an **[class_]** section is an arbitrarily chosen identifier like *clients* or *invalid_clients*. Clients can be grouped in classes. Different classes can have different properties, different address sets and different numbers of addresses. Classes also might have different name servers, time intervals, filters and interfaces. A client gets the addresses, nameserver and T1/T2 values of the class which it is configured for in client configuration database or file. **addresses = [ ...]** A class can contain as many addresses as needed. Their names have to be separated by spaces. *Name* means the *name*-part of an address section like *[address_name]*. If a class does not contain any addresses clients won't get any address except they have one fixed defined in client configuration file or database. **prefixes = [ ...]** A class can contain prefixes - even most probably only one prefix will be useful. *Name* means the *name*-part of a prefiy section. **answer = normal|noaddress|none** Normally a client will get an answer, but if for whatever reason is a need to give it an *NoAddrAvail* message back or completely ignore the client it can be set here. *Default: normal* **nameserver = [ ...]** Each class can have its own nameservers. If this option is used it replaces the nameservers from general settings. **t1 = ** **t2 = ** Each class can have its own **t1** and **t2** values. The ones from general settings will be overridden. Might be of use for some invalid-but-about-to-become-valid-somehow-soon class. **filter_hostname = ** **filter_mac = ** **filter_duid = ** Filters allow one to apply a class to a client not by configuration but by a matching regular expression filter. Most useful might be the filtering by hostname, but maybe there is some use for DUID and MAC address based filtering too. The regular expressions are meant to by Python Regular Expressions. See ``_ and examples section below for details. **interface = [ ...]** It is possible to let a class only apply on specific interfaces. These have to be separated by spaces. **advertise = addresses|prefixes** A class per default allows one to advertise addresses as well as prefixes if requested. This option allows one to narrow the answers down to either *addresses* or *prefixes*. *Default: addresses* **call_up = [$prefix$] [$length$] [$router$]** When a route is requested and accepted the custom *executable* will called and the optional but senseful variables will be filled with their appropriate values. **$prefix$** Contains the prefix advertised to the client. **$length$** The prefix length. **$router$** The host which routes into the advertised prefix - of course the requesting client IPv6. **call_down = [$prefix$] [$length$] [$router$]** When a route is released the custom *executable* will called and the optional but senseful variables will be filled with their appropriate values. **$prefix$** Contains the prefix advertised to the client. **$length$** The prefix length. **$router$** The host which routes into the advertised prefix - of course the requesting client IPv6. **bootfiles = [ ...]** List of PXE bootfiles to evaluate for clients in this client. Each value must refer a bootfile section (see below). Each bootfile is evaluated by the filter defined in the bootfile section, the first machting bootfile is chosen. Example: *bootfiles = eth1_ipxe eth1_efi64 eth1_efi32 eth1_efibc* Default Class ------------- At the moment every client which does not match any other class by client configuration or filter automatically matches the class "default". This class could get an address scheme too. It should be enough if 'address_default' is defined, only if unknown clients should get extra nameservers etc. a 'class_default' has to be set. **[class_default]** Default class for all clients that do not match any other class. Like any other class it might contain all options that appyl to a class. **[class_default_]** If dhcpy6d listens at multiple interfaces, one can define a default class for every 'interface'. Bootfile definitions in multiple [bootfile_] sections ==================================================================== The ** part of an **[bootfile_]** section is an arbitrarily chosen identifier like *efi32*, *bios* or *efi64*. Each bootfile can be restricted to an architecture and/or an user class which is sent by the PXE client. **bootfile_url = ** The bootfile URL in a format like *tftp://[2001:db8:85a3::8a2e:370:7334]/pxe.efi*. The possible protocols are dependent on the PXE client, TFTP should be supported by almost every client. **client_architecture = ** Optionally restrict the bootfile to a specific CPU architecture. If the client doesn't match the requirement, the next bootfile assigned to the class definition is chosen or no bootfile is provided, if there are no further alternatives. Either the integer identifier for an architecture is possible (e.g. 0009 for EFI x86-64). The integer must consists of four numeric digits, empty digits must be written as zero (e.g. 9 => 0009). For a full list of possible integer identifier see ``_. Alternatively the well-known names of registered CPU architectures defined in RF4578 can be used: * Intel x86PC * NEC/PC98 * EFI Itanium * DEC Alpha * Arc x86 * Intel Lean Client * EFI IA32 * EFI BC * EFI Xscale * EFI x86-64 **user_class = ** Optionally restrict this bootfile to PXE clients sending this user class. The *user_class* is matched against the value of the client with simple comparison (no regular expression). Example: *user_class = iPXE* This restricts the bootfile to the iPXE boot firmware. Examples ======== The following paragraphs contain some hopefully helpful examples: Minimal configuration --------------------- Here in this minimalistic example the server daemon listens on interface eth0. It does not use any client configuration source but answers requests with default addresses. These are made of the pattern fd01:db8:dead:bad:beef:$mac$ and result in addresses like fd01:db8:deaf:bad:beef:1020:3040:5060 if the MAC address of the requesting client was 10:20:30:40:50:60. | | [dhcpy6d] | # Set to yes to really answer to clients. | really_do_it = yes | | # Interface to listen to multicast ff02::1:2. | interface = eth0 | | # Some server DUID. | serverduid = 0001000134824528134567366121 | | # Do not identify and configure clients. | store_config = none | | # SQLite DB for leases and LLIP-MAC-mapping. | store_volatile = sqlite | store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite | | # Special address type which applies to all not specially. | # configured clients. | [address_default] | # Choosing MAC-based addresses. | category = mac | # ULA-type address pattern. | pattern = fd01:db8:dead:bad:beef:$mac$ Configuration with valid and unknown clients -------------------------------------------- This example shows some more complexity. Here only valid hosts will get a random global address from 2001:db8::/64. Unknown clients get a default ULA range address from fc00::/7. | | [dhcpy6d] | # Set to yes to really answer to clients. | really_do_it = yes | | # Interface to listen to multicast ff02::1:2. | interface = eth0 | | # Server DUID - if not set there will be one generated every time dhcpy6d starts. | # This might cause trouble for Windows clients because they go crazy about the | # changed server DUID. | serverduid = 0001000134824528134567366121 | | # Non-privileged user/group. | user = dhcpy6d | group = dhcpy6d | | # Nameservers for option 23 - there can be several specified separated by spaces. | nameserver = fd00:db8::53 | | # Domain to be used for option 39 - host FQDN. | domain = example.com | | # Domain search list for option 24 - domain search list. | # If omitted the value of option "domain" above is taken as default. | domain_search_list = example.com | | # Do logging. | log = yes | # Log to console. | log_console = no | # Path to logfile. | log_file = /var/log/dhcpy6d.log | | # Use SQLite for client configuration. | store_config = sqlite | | # Use SQLite for volatile data. | store_volatile = sqlite | | # Paths to SQLite database files. | store_sqlite_config = /var/lib/dhcpy6d/config.sqlite | store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite | | # Declare which attributes of a requesting client should be checked | # to prove its identity. It is possible to mix them, separated by spaces. | identification = mac | | # Declare if all checked attributes have to match or is it enough if | # some do. Kind of senseless with just one attribute. | identification_mode = match_all | | # These lifetimes are also used as default for addresses which | # have no extra defined lifetimes. | preferred_lifetime = 43200 | valid_lifetime = 64800 | t1 = 21600 | t2 = 32400 | | # ADDRESS DEFINITION | # Addresses for proper valid clients. | [address_valid_clients] | # Better privacy for global addresses with category random. | category = random | # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. | pattern = 2001:db8::$random64$ | | # Default addresses for unknown invalid clients. | [address_default] | # Unknown clients will get an internal ULA range-based address. | category = range | # The keyword "range" sets the range used in pattern. | range = 1000-1fff | # This pattern results in addresses like fd00::1234. | pattern = fd00::$range$ | | # CLASS DEFINITION | | # Class for proper valid client. | [class_valid_clients] | # At least one of the above address schemes has to be set. | addresses = valid_clients | # Valid clients get a different nameserver. | nameserver = 2001:db8::53 | | # Default class for unknown hosts - only necessary here because of time interval settings. | [class_default] | addresses = default | # Short interval of address refresh attempts so that a client's status | # change will be reflected in IPv6 address soon. | t1 = 600 | t2 = 900 Configuration with 2 network segments, servers, valid and unknown clients ------------------------------------------------------------------------- This example uses 2 network segments, one for servers and one for clients. Servers here only get local ULA addresses. Valid clients get 2 addresses, one local ULA and one global GUA address. This feature of DHCPv6 is at the moment only well supported by Windows clients. Unknown clients will get a local ULA address. Only valid clients and servers will get information about nameservers. | | [dhcpy6d] | # Set to yes to really answer to clients. | really_do_it = yes | | # Interfaces to listen to multicast ff02::1:2. | # eth1 - client network | # eth2 - server network | interface = eth1 eth2 | | # Server DUID - if not set there will be one generated every time dhcpy6d starts. | # This might cause trouble for Windows clients because they go crazy about the | # changed server DUID. | serverduid = 0001000134824528134567366121 | | # Non-privileged user/group. | user = dhcpy6d | group = dhcpy6d | | # Domain to be used for option 39 - host FQDN. | domain = example.com | | # Domain search list for option 24 - domain search list. | # If omited the value of option "domain" above is taken as default. | domain_search_list = example.com | | # Do logging. | log = yes | # Log to console. | log_console = no | # Path to logfile. | log_file = /var/log/dhcpy6d.log | | # Use MySQL for client configuration. | store_config = mysql | | # Use MySQL for volatile data. | store_volatile = mysql | | # Data used for MySQL storage. | store_db_host = localhost | store_db_db = dhcpy6d | store_db_user = dhcpy6d | store_db_password = dhcpy6d | | # Declare which attributes of a requesting client should be checked | # to prove its identity. It is possible to mix them, separated by spaces. | identification = mac | | # Declare if all checked attributes have to match or is it enough if | # some do. Kind of senseless with just one attribute. | identification_mode = match_all | | # These lifetimes are also used as default for addresses which | # have no extra defined lifetimes. | preferred_lifetime = 43200 | valid_lifetime = 64800 | t1 = 21600 | t2 = 32400 | | # ADDRESS DEFINITION | | # Global addresses for proper valid clients (GUA). | [address_valid_clients_global] | # Better privacy for global addresses with category random. | category = random | # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. | pattern = 2001:db8::$random64$ | | # Local addresses for proper valid clients (ULA). | [address_valid_clients_local] | # Local addresses need no privacy, so they will be based of range. | category = range | range = 2000-2FFF | # Valid clients will get local ULA addresses from fd01::/64. | pattern = fd01::$range$ | | # Servers in servers network will get local addresses based on IDs from client configuration. | [address_servers] | # IDs are set in client configuration database in range of 0-FFFF. | category = id | # Servers will get local ULA addresses from fd02::/64. | pattern = fd02::$id$ | | # Default addresses for unknown invalid clients | [address_default] | # Unknown clients will get an internal ULA range-based address. | category = range | # The keyword "range" sets the range used in pattern. | range = 1000-1FFF | # This pattern results in addresses like fd00::1234. | pattern = fd00::$range$ | | # CLASS DEFINITION | | # Class for proper valid client. | [class_valid_clients] | # Clients only exist in network linked with eth1. | interface = eth1 | # Valid clients get 2 addresses, one local ULA and one global GUA | # (only works reliably with Windows clients). | addresses = valid_clients_global valid_clients_local | # Only valid clients get a nameserver from server network. | nameserver = fd02::53 | | # Class for servers in network on eth2 | [class_servers] | # Servers only exist in network linked with eth2. | interface = eth2 | # Only local addresses for servers. | addresses = servers | # Nameserver from server network. | nameserver = fd02::53 | | # Default class for unknown hosts - only necessary here because of time interval settings | [class_default] | addresses = default | # Short interval of address refresh attempts so that a client's status | # change will be reflected in IPv6 address soon. | t1 = 600 | t2 = 900 Configuration with dynamic DNS Updates -------------------------------------- In this example the hostnames of valid clients will be registered in the Bind DNS server. The zones to be updated are configured for every address definition. Here only the global GUA addresses for valid clients will be updated in DNS. The hostnames will be taken from client configuration data - the ones supplied by the clients are ignored. | | [dhcpy6d] | # Set to yes to really answer to clients. | really_do_it = yes | | # Interface to listen to multicast ff02::1:2. | interface = eth0 | | # Server DUID - if not set there will be one generated every time dhcpy6d starts. | # This might cause trouble for Windows clients because they go crazy about the | # changed server DUID. | serverduid = 0001000134824528134567366121 | | # Non-privileged user/group. | user = dhcpy6d | group = dhcpy6d | | # Nameservers for option 23 - there can be several specified separated by spaces. | nameserver = fd00:db8::53 | | # Domain to be used for option 39 - host FQDN. | domain = example.com | | # Domain search list for option 24 - domain search list. | # If omited the value of option "domain" above is taken as default. | domain_search_list = example.com | | # This works at the moment only for ISC Bind nameservers. | dns_update = yes | | # RNDC key name for DNS Update. | dns_rndc_key = rndc-key | | # RNDC secret - mostly some MD5-hash. Take it from | # nameservers' /etc/rndc.key. | dns_rndc_secret = 0123456789012345679 | | # Nameserver to talk to. | dns_update_nameserver = ::1 | | # Regarding RFC 4704 5. there are 3 kinds of client behaviour | # for N O S bits: | # - client wants to update DNS itself -> sends 0 0 0 | # - client wants server to update DNS -> sends 0 0 1 | # - client wants no server DNS update -> sends 1 0 0 | # Ignore client ideas about DNS (if at all, what name to use, self-updating...) | # Here client hostname is taken from client configuration | dns_ignore_client = yes | | # Do logging. | log = yes | # Log to console. | log_console = no | # Path to logfile. | log_file = /var/log/dhcpy6d.log | | # Use SQLite for client configuration. | store_config = sqlite | | # Use SQLite for volatile data. | store_volatile = sqlite | | # Paths to SQLite database files. | store_sqlite_config = config.sqlite | store_sqlite_volatile = volatile.sqlite | | # Declare which attributes of a requesting client should be checked | # to prove its identity. It is possible to mix them, separated by spaces. | identification = mac | | # ADDRESS DEFINITION | | # Addresses for proper valid clients. | [address_valid_clients] | # Better privacy for global addresses with category random. | category = random | # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. | pattern = 2001:db8::$random64$ | # Update these addresses in Bind DNS | dns_update = yes | # Zone to update. | dns_zone = example.com | # Reverse zone to update | dns_rev_zone = 8.b.d.0.1.0.0.2.ip6.arpa | | # Default addresses for unknown invalid clients. | [address_default] | # Unknown clients will get an internal ULA range-based address. | category = range | # The keyword "range" sets the range used in pattern. | range = 1000-1FFF | # This pattern results in addresses like fd00::1234. | pattern = fd00::$range$ | | # CLASS DEFINITION | | # Class for proper valid client. | [class_valid_clients] | # At least one of the above address schemes has to be set. | addresses = valid_clients | # Valid clients get a different nameserver. | nameserver = 2001:db8::53 Configuration with filter ------------------------- In this example the membership of a client to a class is defined by a filter for hostnames. All Windows machines have win*-names here and when requesting an address this hostname gets filtered. | | [dhcpy6d] | # Set to yes to really answer to clients. | really_do_it = yes | | # Interface to listen to multicast ff02::1:2. | interface = eth0 | | # Server DUID - if not set there will be one generated every time dhcpy6d starts. | # This might cause trouble for Windows clients because they go crazy about the | # changed server DUID. | serverduid = 0001000134824528134567366121 | | # Use no client configuration. | store_config = none | | # Use SQLite for volatile data. | store_volatile = sqlite | | # Paths to SQLite database file. | store_sqlite_volatile = volatile.sqlite | | # ADDRESS DEFINITION | | [address_local] | category = range | range = 1000-1FFF | pattern = fd00::$range$ | | [address_global] | category = random | pattern = 2001:638::$random64$ | | # CLASS DEFINITION | | [class_windows] | addresses = local | # Python regular expressions to be used here | filter_hostname = win.* | [class_default] | addresses = global Configuration with prefixes --------------------------- Here dhcpy6d also provides prefixes in the default class. To avoid heavy load by bad clients request limits are activated. | | [dhcpy6d] | interface = eth0 | server_preference = 255 | | store_config = none | store_volatile = sqlite | store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite | | log = on | log_console = yes | log_syslog = yes | log_file = /var/log/dhcpy6d.log | | identification_mode = match_all | identification = mac | | nameserver = 2001:db8::53 | ntp_server = 2001:db8::123 | | # Mitigate ugly and aggressive clients | request_limit = yes | request_limit_time = 30 | request_limit_count = 10 | request_limit_identification = llip | ignore_iaid = yes | ignore_unknown_clients = yes | | advertise = addresses prefixes | manage_routes_at_start = yes | | [address_default] | category = mac | pattern = 2001:db8::$mac$ | | [prefix_default] | category = range | range = 0000-ffff | pattern = 2001:db8:0:$range$:: | route_link_local = yes | length = 64 | | [class_default] | addresses = default | prefixes = default | call_up = sudo ip -6 route add $prefix$/$length$ via $router$ dev eth0 | call_down = sudo ip -6 route delete $prefix$/$length$ via $router$ dev eth0 Only use fixed addresses ------------------------ If no addresses should be generated, the clients need to have an address defined in their configuration file or database. It looks like this: | [example-client] | hostname = example-client | mac = 01:02:03:04:05:06 | class = fixed_address | address = 2001:db8::1234 The according class of the client simply must not have any address definition an might as well stay empty: | | [dhcpy6d] | # Set to yes to really answer to clients. | really_do_it = yes | | # Interface to listen to multicast ff02::1:2. | interface = eth0 | | # Some server DUID. | serverduid = 0001000134824528134567366121 | | # Do not identify and configure clients. | store_config = none | | # SQLite DB for leases and LLIP-MAC-mapping. | store_volatile = sqlite | store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite | | # Special address type which applies to all not specially. | # configured clients. | [address_default] | # Choosing MAC-based addresses. | category = mac | # ULA-type address pattern. | pattern = fd01:db8:dead:bad:beef:$mac$ | # To use the EUI-64 instead of the plain MAC address: | #category = eui64 | #pattern = fd01:db8:dead:bad:$eui64$ | | [class_fixed_address] | # just no address definiton here Supply a PXE bootfile for different CPU architectures and user classes ---------------------------------------------------------------------- This example how to assign PXE bootfiles depending on CPU architecture and user class: | [class_default_eth1] | bootfiles = eth1_ipxe eth1_efi64 eth1_efi32 eth1_efibc | addresses = eth1 | interface = eth1 | nameserver = fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd 2001:0470:76aa:00f5:5054:00ff:fec2:c5dd | filter_mac = .* | | [address_eth1] | # Choosing EUI-64-based addresses. | category = eui64 | # ULA-type address pattern. | pattern = fdff:cc21:56df:8bc8::$eui64$ | | [bootfile_eth1_ipxe] | user_class = iPXE | bootfile_url = tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/default.ipxe | | [bootfile_eth1_efi32] | client_architecture = 0006 | bootfile_url = tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/efi32/ipxe.efi | | [bootfile_eth1_efibc] | client_architecture = 0007 | bootfile_url = tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/efi64/ipxe.efi | | [bootfile_eth1_efi64] | client_architecture = 0009 | bootfile_url = tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/efi32/ipxe.efi | | [bootfile_eth2_ipxe] | user_class = iPXE | bootfile_url = tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/default.ipxe | | [bootfile_eth2_efi32] | client_architecture = 0006 | bootfile_url = tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/efi32/ipxe.efi | | [bootfile_eth2_efibc] | client_architecture = 0007 | bootfile_url = tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/efi64/ipxe.efi | | [bootfile_eth2_efi64] | client_architecture = 0009 | bootfile_url = tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/efi32/ipxe.efi At first there is a check for the iPXE boot firmware, which delivers an iPXE script on success. Otherwise the iPXE binary matching to the architecture is served. License ======= This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA On Debian systems, the full text of the GNU General Public License version 2 can be found in the file */usr/share/common-licenses/GPL-2*. See also ======== * dhcpy6d(8) * dhcpy6d-clients.conf(5) * ``_ * ``_ dhcpy6d-1.2.3/doc/dhcpy6d.rst000066400000000000000000000077211437472361000157340ustar00rootroot00000000000000======= dhcpy6d ======= ---------------------------------------------------------------- MAC address aware DHCPv6 server ---------------------------------------------------------------- :Author: Copyright (C) 2012-2022 Henri Wahl :Date: 2022-06-14 :Version: 1.2.2 :Manual section: 8 :Copyright: This manual page is licensed under the GPL-2 license. Synopsis ======== **dhcpy6d** [**-c** *file*] [**-u** *user*] [**-g** *group*] [**-p** *prefix*] [**-r** *yes|no*] [**-d** *duid*] [**-m** *message*] [**-G**] Description =========== **dhcpy6d** is an open source server for DHCPv6, the DHCP protocol for IPv6. Its development is driven by the need to be able to use the existing IPv4 infrastructure in coexistence with IPv6. In a dualstack scenario, the existing DHCPv4 most probably uses MAC addresses of clients to identify them. This is not intended by RFC 3315 for DHCPv6, but also not forbidden. Dhcpy6d is able to do so in local network segments and therefore offers a pragmatical method for parallel use of DHCPv4 and DHCPv6, because existing client management solutions could be used further. **dhcpy6d** comes with the following features: * identifies clients by MAC address, DUID or hostname * generates addresses randomly, by MAC address, by range, by given ID or from DNS name * filters clients by MAC, DUID or hostname * assigns multiple addresses per client * allows one to organize clients in different classes * stores leases in MySQL, PostgreSQL or SQLite database * client information can be retrieved from MySQL or PostgreSQL database or textfile * dynamically updates DNS (Bind) * supports rapid commit * listens on multiple interfaces Options ======= Most configuration is done via the configuration file. **-c, --config=** Set the configuration file to use. Default is /etc/dhcpy6d.conf. **-u, --user=** Set the unprivileged user to be used. **-g, --group=** Set the unprivileged group to be used. **-r, --really-do-it=** Really activate the DHCPv6 server. This is a precaution to prevent larger network trouble. **-d, --duid=** Set the DUID for the server. This argument is used by /etc/init.d/dhcpy6d and /lib/systemd/system/dhcpy6d.service respectively. **-p, --prefix=** Set the prefix which will be substituted for the $prefix$ variable in address definitions. Useful for setups where the ISP uses a changing prefix. **-G, --generate-duid** Generate DUID to be used in config file. This argument is used to generate a DUID for /etc/default/dhcpy6d. After generation dhcpy6d exits. **-m, --message ""** 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"*. Files ===== * /etc/dhcpy6d.conf * /etc/dhcpy6d-clients.conf * /var/lib/dhcpy6d/ * /var/log/dhcpy6d.log License ======= This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA On Debian systems, the full text of the GNU General Public License version 2 can be found in the file */usr/share/common-licenses/GPL-2*. See also ======== * dhcpy6d.conf(5) * dhcpy6d-clients.conf(5) * ``_ * ``_ dhcpy6d-1.2.3/doc/volatile.postgresql000066400000000000000000000031301437472361000175730ustar00rootroot00000000000000CREATE TABLE leases ( address varchar(32) NOT NULL, active smallint NOT NULL, preferred_lifetime int NOT NULL, valid_lifetime int NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, ia_type varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int NOT NULL DEFAULT 0, PRIMARY KEY (address) ); CREATE TABLE macs_llips ( mac varchar(17) NOT NULL, link_local_ip varchar(39) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (mac) ); CREATE TABLE prefixes ( prefix varchar(32) NOT NULL, length smallint NOT NULL, active smallint NOT NULL, preferred_lifetime int NOT NULL, valid_lifetime int NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int NOT NULL DEFAULT 0, PRIMARY KEY (prefix) ); CREATE TABLE meta ( item_key varchar(255) NOT NULL, item_value varchar(255) NOT NULL, PRIMARY KEY (item_key) ); CREATE TABLE routes ( prefix varchar(32) NOT NULL, length smallint NOT NULL, router varchar(32) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (prefix) ); INSERT INTO meta (item_key, item_value) VALUES ('version', '3'); dhcpy6d-1.2.3/doc/volatile.sql000066400000000000000000000031701437472361000161730ustar00rootroot00000000000000CREATE TABLE leases ( address varchar(32) NOT NULL, active tinyint(4) NOT NULL, preferred_lifetime int(11) NOT NULL, valid_lifetime int(11) NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, ia_type varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int(11) NOT NULL DEFAULT 0, PRIMARY KEY (address) ); CREATE TABLE macs_llips ( mac varchar(17) NOT NULL, link_local_ip varchar(39) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (mac) ); CREATE TABLE prefixes ( prefix varchar(32) NOT NULL, length tinyint(4) NOT NULL, active tinyint(4) NOT NULL, preferred_lifetime int(11) NOT NULL, valid_lifetime int(11) NOT NULL, hostname varchar(255) NOT NULL, type varchar(255) NOT NULL, category varchar(255) NOT NULL, class varchar(255) NOT NULL, mac varchar(17) NOT NULL, duid varchar(255) NOT NULL, last_update bigint NOT NULL, preferred_until bigint NOT NULL, valid_until bigint NOT NULL, iaid varchar(8) DEFAULT NULL, last_message int(11) NOT NULL DEFAULT 0, PRIMARY KEY (prefix) ); CREATE TABLE meta ( item_key varchar(255) NOT NULL, item_value varchar(255) NOT NULL, PRIMARY KEY (item_key) ); CREATE TABLE routes ( prefix varchar(32) NOT NULL, length tinyint(4) NOT NULL, router varchar(32) NOT NULL, last_update bigint NOT NULL, PRIMARY KEY (prefix) ); INSERT INTO meta (item_key, item_value) VALUES ('version', '3'); dhcpy6d-1.2.3/etc/000077500000000000000000000000001437472361000136405ustar00rootroot00000000000000dhcpy6d-1.2.3/etc/default/000077500000000000000000000000001437472361000152645ustar00rootroot00000000000000dhcpy6d-1.2.3/etc/default/dhcpy6d000066400000000000000000000000501437472361000165430ustar00rootroot00000000000000# dhcpy6d is disabled by default RUN=no dhcpy6d-1.2.3/etc/dhcpy6d.conf000066400000000000000000000014551437472361000160550ustar00rootroot00000000000000# dhcpy6d default configuration # # Please see the examples in /usr/share/doc/dhcpy6d and # https://dhcpy6.de/documentation for more information. [dhcpy6d] # Interface to listen to multicast ff02::1:2. interface = eth0 # Do not identify and configure clients. store_config = none # SQLite DB for leases and LLIP-MAC-mapping. store_volatile = sqlite store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite log = on log_file = /var/log/dhcpy6d.log # set to yes to really answer to clients # not necessary in Debian where it comes from /etc/default/dhcpy6d and /etc/init.d/dhcpy6 #really_do_it = no # Special address type which applies to all not specially # configured clients. [address_default] # Choosing MAC-based addresses. category = mac # ULA-type address pattern. pattern = fd01:db8:dead:bad:beef:$mac$ dhcpy6d-1.2.3/etc/logrotate.d/000077500000000000000000000000001437472361000160625ustar00rootroot00000000000000dhcpy6d-1.2.3/etc/logrotate.d/dhcpy6d000066400000000000000000000001511437472361000173430ustar00rootroot00000000000000/var/log/dhcpy6d.log { weekly missingok rotate 4 compress notifempty create 660 dhcpy6d dhcpy6d } dhcpy6d-1.2.3/lib/000077500000000000000000000000001437472361000136335ustar00rootroot00000000000000dhcpy6d-1.2.3/lib/systemd/000077500000000000000000000000001437472361000153235ustar00rootroot00000000000000dhcpy6d-1.2.3/lib/systemd/system/000077500000000000000000000000001437472361000166475ustar00rootroot00000000000000dhcpy6d-1.2.3/lib/systemd/system/dhcpy6d.service000066400000000000000000000005131437472361000215710ustar00rootroot00000000000000[Unit] Description=DHCPv6 Server Daemon Documentation=man:dhcpy6d(8) man:dhcpy6d.conf(5) man:dhcpy6d-clients.conf(5) Wants=network-online.target After=network-online.target After=time-sync.target [Service] ExecStart=/usr/sbin/dhcpy6d --config /etc/dhcpy6d.conf --user dhcpy6d --group dhcpy6d [Install] WantedBy=multi-user.target dhcpy6d-1.2.3/main.py000077500000000000000000000114241437472361000143700ustar00rootroot00000000000000#!/usr/bin/env python3 # # DHCPy6d DHCPv6 Daemon # # Copyright (C) 2009-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import distro import sys # access /usr/share/pyshared on Debian # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=715010 if distro.id() == 'debian': sys.path[0:0] = ['/usr/share/pyshared'] import grp import pwd import os import socket from dhcpy6d import UDPMulticastIPv6 from dhcpy6d.config import cfg from dhcpy6d.globals import (config_answer_queue, config_query_queue, IF_NAME, route_queue, volatile_answer_queue, volatile_query_queue) from dhcpy6d.log import log from dhcpy6d.handler import RequestHandler from dhcpy6d.route import manage_prefixes_routes from dhcpy6d.storage import (config_store, QueryQueue, volatile_store) from dhcpy6d.threads import (DNSQueryThread, RouteThread, TidyUpThread, TimerThread) # main part, initializing all stuff def run(): log.info('Starting dhcpy6d daemon...') log.info(f'Server DUID: {cfg.SERVERDUID}') # configure SocketServer UDPMulticastIPv6.address_family = socket.AF_INET6 udp_server = UDPMulticastIPv6(('', 547), RequestHandler) # start query queue watcher config_query_queue_watcher = QueryQueue(name='config_query_queue', store_type=config_store, query_queue=config_query_queue, answer_queue=config_answer_queue) config_query_queue_watcher.start() volatile_query_queue_watcher = QueryQueue(name='volatile_query_queue', store_type=volatile_store, query_queue=volatile_query_queue, answer_queue=volatile_answer_queue) volatile_query_queue_watcher.start() # check if database tables are up-to-date or exist and create them if not volatile_store.check_storage() # check if config database - if any - supports prefixes config_store.check_config_prefixes_support() # if global dynamic prefix was not given take it from database - only possible after database initialisation if cfg.PREFIX == '': cfg.PREFIX = volatile_store.get_dynamic_prefix() if cfg.PREFIX is None: cfg.PREFIX = '' # apply dynamic prefix to addresses and prefixes for a in cfg.ADDRESSES: cfg.ADDRESSES[a].inject_dynamic_prefix_into_prototype(cfg.PREFIX) for p in cfg.PREFIXES: cfg.PREFIXES[p].inject_dynamic_prefix_into_prototype(cfg.PREFIX) # collect all known MAC addresses from database if cfg.CACHE_MAC_LLIP: volatile_store.collect_macs_from_db() # start timer timer_thread = TimerThread() timer_thread.start() # start route queue to care for routes in background route_thread = RouteThread(route_queue) route_thread.start() # delete invalid and add valid routes - useful after reboot if cfg.MANAGE_ROUTES_AT_START: manage_prefixes_routes() # start TidyUp thread for cleaning in background tidyup_thread = TidyUpThread() tidyup_thread.start() # start DNS query queue to care for DNS in background dns_query_thread = DNSQueryThread() dns_query_thread.start() # set user and group log.info(f'Running as user {cfg.USER} (UID {pwd.getpwnam(cfg.USER).pw_uid}) and ' f'group {cfg.GROUP} (GID {grp.getgrnam(cfg.GROUP).gr_gid})') # first set group because otherwise the freshly unprivileged user could not modify its groups itself os.setgid(grp.getgrnam(cfg.GROUP).gr_gid) os.setuid(pwd.getpwnam(cfg.USER).pw_uid) # log interfaces log.info(f'Listening on interfaces: {" ".join(IF_NAME)}') # serve forever try: udp_server.serve_forever() except KeyboardInterrupt: sys.exit(0) if __name__ == '__main__': run() dhcpy6d-1.2.3/man/000077500000000000000000000000001437472361000136405ustar00rootroot00000000000000dhcpy6d-1.2.3/man/man5/000077500000000000000000000000001437472361000145005ustar00rootroot00000000000000dhcpy6d-1.2.3/man/man5/dhcpy6d-clients.conf.5000066400000000000000000000124261437472361000205170ustar00rootroot00000000000000.\" Man page generated from reStructuredText. . .TH DHCPY6D-CLIENTS.CONF 5 "2020-12-21" "1.0.3" "" .SH NAME dhcpy6d-clients.conf \- Clients configuration file for DHCPv6 server dhcpy6d . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH DESCRIPTION .sp This file contains all client configuration data if these options are set in \fBdhcpy6d.conf\fP: .sp \fBstore_config = file\fP .sp and .sp \fBstore_file_config = /path/to/dhcpy6d\-clients.conf\fP .sp An alternative method to store client configuration is using database storage with SQLite or MySQLor PostgreSQL databases. Further details are available at \fI\%https://dhcpy6d.de/documentation/config\fP\&. .sp This file follows RFC 822 style parsed by Python ConfigParser module. .sp Some options allow multiple values. These have to be separated by spaces. .SH CLIENT SECTIONS .INDENT 0.0 .TP .B \fB[host_name]\fP 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\&. .UNINDENT .SH CLIENT ATTRIBUTES .sp 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\&. .sp Both of the following 2 attributes are necessary \- the \fBclass\fP and at least one of the others. .SS Mandatory client attribute \(aqclass\(aq .INDENT 0.0 .TP .B \fBclass = \fP Every client needs a class. If a client is identified, it depends from its class, which addresses it will get. This relation is configured in \fIdhcpy6d.conf\fP\&. .UNINDENT .SS Semi\-mandatory client attributes .sp Depending on \fBidentification\fP in \fIdhcpy6d.conf\fP clients need to have the corresponding attributes. At least one of them is needed. .INDENT 0.0 .TP .B \fBmac = \fP The MAC address of the Link Local Address of the client DHCPv6 request, formatted like the most usual 01:02:03:04:05:06. .TP .B \fBduid = \fP The DUID of the client which comes with the DHCPv6 request message. No hex and \e needed, just like for example 000100011234567890abcdef1234 . .TP .B \fBhostname = \fP The client non\-FQDN hostname. It will be used for dynamic DNS updates. .UNINDENT .SS Extra attributes .sp These attributes do not serve for identification of a client but for appropriate address generation. .INDENT 0.0 .TP .B \fBid = \fP \fBid\fP 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. .TP .B \fBaddress =
[
...]\fP 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. .TP .B \fBprefix = [ ...]\fP Prefix configured here will be sent to client in addition to the ones it gets due to its class. .UNINDENT .SH EXAMPLES .sp The next lines contain some example client definitions: .nf [client1] hostname = client1 mac = 01:01:01:01:01:01 class = valid_client .fi .sp .nf [client2] hostname = client2 mac = 02:02:02:02:02:02 class = invalid_client .fi .sp .nf [client3] hostname = client3 duid = 000100011234567890abcdef1234 class = valid_client address = 2001:db8::babe:1 .fi .sp .nf [client4] hostname = client4 mac = 04:04:04:04:04:04 id = 1234 class = valid_client .fi .sp .nf [client5] hostname = client5 mac = 01:01:01:01:01:02 class = valid_client prefix = 2001:db8::/48 .fi .sp .SH LICENSE .sp This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. .sp This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. .sp You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110\-1301 USA .sp On Debian systems, the full text of the GNU General Public License version 2 can be found in the file \fI/usr/share/common\-licenses/GPL\-2\fP\&. .SH SEE ALSO .INDENT 0.0 .IP \(bu 2 dhcpy6d(8) .IP \(bu 2 dhcpy6d.conf(5) .IP \(bu 2 \fI\%https://dhcpy6d.de\fP .IP \(bu 2 \fI\%https://github.com/HenriWahl/dhcpy6d\fP .UNINDENT .SH AUTHOR Copyright (C) 2012-2022 Henri Wahl .SH COPYRIGHT This manual page is licensed under the GPL-2 license. .\" Generated by docutils manpage writer. . dhcpy6d-1.2.3/man/man5/dhcpy6d.conf.5000066400000000000000000001231371437472361000170620ustar00rootroot00000000000000.\" Man page generated from reStructuredText. . .TH DHCPY6D.CONF 5 "2020-12-21" "1.0.3" "" .SH NAME dhcpy6d.conf \- Configuration file for DHCPv6 server dhcpy6d . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH DESCRIPTION .sp This file contains the general settings for DHCPv6 server daemon dhcpy6d. It follows RFC 822 style parsed by Python ConfigParser module. It contains several sections which will be discussed in detail here. .sp An online documentation is also available at \fI\%https://dhcpy6d.de/documentation/config\fP\&. .sp Boolean settings can be set with \fI1|0\fP, \fIon|off\fP or \fIyes|no\fP values. .sp Some options allow multiple values. These have to be separated by spaces. .sp There are 5 types of sections: .INDENT 0.0 .TP .B \fB[dhcpy6d]\fP This section contains general options like interfaces, storage and logging. Only one [dhcpy6d] section is allowed. .TP .B \fB[address_]\fP There can be various \fI[address_]\fP sections. In several address sections several address ranges and types can be defined. Addresses are organized in classes. For details read further down. .TP .B \fB[prefix_]\fP There can be various \fI[prefix_]\fP sections. In several prefix sections several prefix ranges and types can be defined. Prefixes are organized in classes. For details read further down. .TP .B \fB[class_]\fP Class definitions allow one to apply different addresses, time limits et al. to different types of clients. .TP .B \fB[bootfile_]\fP There can be various \fI[bootfile_]\fP sections. In several bootfile sections several tftp bootfile urls with restrictions to CPU architecture and user class supplied by the PXE client can be defined. .UNINDENT .SH GENERAL CONFIGURATION IN SECTION [DHCPY6D] .sp This section contains important general options. Values are sometimes examples and not meant to be used in production environments. .INDENT 0.0 .TP .B \fBreally_do_it = yes|no\fP Let dhcpy6d \fBreally do it\fP and respond to client requests \- disabling might be of use for debugging and testing. \fIDefault: no\fP .TP .B \fBinterface = [ ...]\fP The interfaces the server listens on is defined with keyword interface. Multiple interfaces have to be separated by spaces. .TP .B \fBserverduid = \fP The server DUID should be configured with serverduid. If there is none dhcpy6d creates a new one at every startup. Windows clients might run a little bit wild when server DUID changed. You are free to compose your own as long as it follows RFC 3315. Please note that it has to be in hexadecimal format \- no octals, no "\-", just like in the example below. The example here is a DUID\-LLT (Link\-layer Address Plus Time) even if it should be a DUID\-TLL as timestamp comes first. It is composed of \fIDUID\-type(LLT=1)\fP + \fIHardware\-type(Ethernet=1)\fP + \fIUnixtime\-in\-hexadecimal\fP + \fIMAC\-address\fP what makes a \fI0001\fP + \fI0001\fP + \fI11fb5dc9\fP + \fI01023472a6c5\fP = \fB0001000111fb5dc901023472a6c5\fP\&. .TP .B \fBserver_preference = <0\-255>\fP The server preference determines the priority of the server. The maximum value is 255 which means highest priority. \fIDefault: 255\fP .TP .B \fBuser = \fP For security reasons dhcpy6d can and should be run as non\-root user. \fIDefault: root\fP .TP .B \fBgroup = \fP For security reasons dhcpy6d can and should be run as non\-root group. \fIDefault: root\fP .TP .B \fBnameserver = [ ...]\fP If an address type is of category \fIdns\fP at least one nameserver has to be given here. If more than one is needed they have to be separated by spaces. .TP .B \fBdomain = \fP The domain to be used with FQDN hostnames for option 39. .TP .B \fBdomain_search_list = [ ...]\fP Domain search lists to be used with option 24. If none is given the value of domain above is used. Multiple domains have to be separated by space or comma. .TP .B \fBntp_server = [ ...]\fP NTP servers to be used. can be unicast addresses, multicast addresses or FQDNs following RFC 5908 for DHCPv6 option 56. .TP .B \fBlog = yes|no\fP Enable logging. \fIDefault: no\fP .TP .B \fBlog_console = yes|no\fP Log to the console where \fBdhcpy6d\fP has been started. \fIDefault: no\fP .TP .B \fBlog_file = \fP Defines the file used for logging. Will be created if it does not yet exist. .TP .B \fBlog_syslog = yes|no\fP If logs should go to syslog it is set here. \fIDefault: no\fP .TP .B \fBlog_syslog_destination = syslogserver\fP An UDP syslog server may be used if \fBlog_syslog_destination\fP points to it. Optionally a port other than default 514 can be set when adding ":" to the destination. .TP .B \fBlog_syslog_facility = \fP The default syslog facility is \fIdaemon\fP but can be changed here. \fIDefault: daemon\fP .TP .B \fBlog_mac_llip = yes|no\fP Log discovered MAC/LLIP pairs of clients. Might be pretty verbose in larger setups and with disabled MAC/LLIP pair caching. \fIDefault: no\fP .TP .B \fBstore_config = file|sqlite|mysql|postgresql|none\fP Configuration of clients can be stored in a file or in a database. Databases MySQL, PostgreSQL and SQLite are supported at the moment, thus possible values are \fIfile\fP, \fImysql\fP, \fIpostgresql\fP or \fIsqlite\fP\&. To disable any client configuration source it has to be \fInone\fP\&. \fIDefault: none\fP .TP .B \fBstore_file_config = \fP File which contains the clients configuration. For details see \fBdhcpy6d\-clients.conf(5)\fP\&. \fIDefault: /etc/dhcpy6d\-clients.conf\fP .TP .B \fBstore_sqlite_config = /path/to/sqlite/config/file\fP SQLite database file which contains the clients configuration. \fIDefault: config.sqlite\fP .TP .B \fBstore_volatile = sqlite|mysql|postgresql\fP Volatile data like leases and the mapping between Link Local addresses and MAC addresses can be stored in MySQL, PostgreSQL or SQLite database, so the possible values are \fImysql\fP, \fIpostgresql\fP and \fIsqlite\fP\&. .TP .B \fBstore_sqlite_volatile = /path/to/sqlite/volatile/file\fP If set to \fIsqlite\fP a SQLite database file must be defined. \fIDefault: /var/lib/dhcpy6d/volatile.sqlite\fP .UNINDENT .sp \fBstore_db_host = \fP .sp \fBstore_db_db = \fP .sp \fBstore_db_user = \fP .INDENT 0.0 .TP .B \fBstore_db_password = \fP If \fBstore_config\fP and/or \fBstore_volatile\fP use a database to store information it has to be set with these self\-explanatory options. The same database is used for config and volatile data. .TP .B \fBcache_mac_llip = yes|no\fP Cache discovered MAC/LLIP pairs in database. If enabled reduces response time and opens dhcpy6d to \fIpossible\fP MAC/LLIP poisoning. If disabled might increase system load. \fIDefault: no\fP .TP .B \fBidentification = \fP Clients can be set to be identified by several attributes \- MAC address, DUID or hostname. At least one of mac, duid or hostname is necessary. Hostname is the one sent in client request with DHCPv6 option 39. Identification is used to get the correct settings for the client from config file or database. Same MAC and different DUIDs might be interesting for clients with multiple OS. \fIDefault: mac\fP .TP .B \fBidentification_mode = match_all|match_some\fP If more than one identification attribute has been set, the identification mode can be one of \fImatch_all\fP or \fImatch_some\fP\&. The first means that all attributes have to match to identify a client and the latter is more tolerant. \fIDefault: match_all\fP .TP .B \fBignore_mac = yes|no\fP If serving only for delivering addresses regardless of classes (e.g. on PPP interface) MACs do not need to be investigated. .TP .B \fBdns_update = yes|no\fP Dynamically update DNS. This works at the moment only with Bind DNS, but might be extended to others, maybe via call of an external command. \fIDefault: no\fP .UNINDENT .sp \fBdns_update_nameserver = [ ...]\fP .INDENT 0.0 .TP .B \fBdns_use_rndc = yes|no\fP DNS updates might be able without RNDC key but this is not advised. \fIDefault: yes\fP .UNINDENT .sp \fBdns_rndc_key = \fP .INDENT 0.0 .TP .B \fBdns_rndc_secret = \fP \fIDefault: 5400\fP .TP .B \fBvalid_lifetime = \fP \fIDefault: 7200\fP .TP .B \fBt1 = \fP \fIDefault: 2700\fP .TP .B \fBt2 = \fP Preferred lifetime, valid lifetime, T1 and T2 in seconds are configured with the corresponding options. \fIDefault: 4050\fP .TP .B \fBinformation_refresh_time = \fP The lifetime of information given to clients as response to an \fIinformation\-request\fP message. \fIDefault: 6000\fP .TP .B \fBignore_iaid = yes|no\fP Ignore IAID when looking for leases in database. Might be of use in case some clients are changing their IAD for some unknown reason. \fIDefault: no\fP .TP .B \fBignore_unknown_clients = yes|no\fP Ignore clients if no trace of them can be found in the neighbor cache. \fIDefault: yes\fP .TP .B \fBrequest_limit = yes|no\fP Enables request limits for clients which can be controlled by \fIrequest_limit_time\fP and \fIrequest_limit_count\fP\&. \fIDefault: no\fP .TP .B \fBrequest_limit_identification = mac|llip\fP Identifies clients either by MAC address or Link Local IP. \fIDefault: llip\fP .TP .B \fBrequest_limit_time = \fP \fIDefault: 60\fP .TP .B \fBrequest_limit_count = \fP Requests can be limited to avoid server to be flooded by buggy clients. Set number of request during a certain time in seconds. \fIDefault: 20\fP .TP .B \fBrequest_limit_release_time = \fP Duration in seconds for brute force clients to stay on the blacklist. \fIDefault: 7200\fP .TP .B \fBmanage_routes_at_start = yes|no\fP Check prefixes at startup and call commands for adding and deleting routes respectively. \fIDefault: no\fP .UNINDENT .SH ADDRESS DEFINITIONS IN MULTIPLE [ADDRESS_] SECTIONS .sp The \fI\fP part of an \fB[address_]\fP section is an arbitrarily chosen identifier like \fIclients_global\fP or \fIinvalid_clients_local\fP\&. There can be many address definitions which will be used by classes. Every address definition may include several properties: .INDENT 0.0 .TP .B \fBcategory = mac|eui64|id|range|random|fixed|dns\fP Categories play an important role when defining patterns for addresses. An address belongs to a certain category: .INDENT 7.0 .TP .B \fBmac\fP Uses MAC address from client request as part of address .TP .B \fBeui64\fP Also uses MAC address from client as part of address, but converts it to a 64\-bit extended unique identifier (EUI\-64) .TP .B \fBid\fP Uses ID given to client in configuration file or database as one octet of address, should be in range 0\-ffff .TP .B \fBrange\fP Generate addresses of given range like 0\-ffff .TP .B \fBrandom\fP Randomly created 64 bit values used as host part in address .TP .B \fBfixed\fP Use addresses from client configuration only. .TP .B \fBdns\fP Ask DNS server for IPv6 address of client host .UNINDENT .TP .B \fBrange = \-\fP Sets range for addresses of category \fIrange\fP\&. .INDENT 7.0 .TP .B \fBfrom\fP Starting hex number of range, minimum is 0 .TP .B \fBto\fP Maximum hex limit of range, highest is ffff. .UNINDENT .UNINDENT .sp \fBpattern = 2001:db8::$mac$|$id$|$range$|$random$\fP .INDENT 0.0 .TP .B \fBpattern= $prefix$|$mac$|$eui64$|$id$|$range$|$random$\fP Patterns allow one to design the addresses according to their category. See examples section below to make it more clear. .INDENT 7.0 .TP .B \fB$mac$\fP The MAC address from the DHCPv6 request\(aqs Link Local Address found in the neighbor cache will be inserted instead of the placeholder. It will be stretched over 3 thus octets like 00:11:22:33:44:55 become 0011:2233:4455. .TP .B \fB$eui64$\fP The MAC address converted to a modified 64\-bit extended unique identifier (EUI\-64) from the DHCPv6 request\(aqs Link Local Address found in the neighbor cache will be inserted instead of the placeholder. It will be converted according to RFC 4291 like 52:54:00:e5:b4:64 become 5054:ff:fee5:b464 .TP .B \fB$id$\fP If clients get an ID in client configuration file or in client configuration database this ID will fill one octet. Thus the ID has to be in the range of 0000\-ffff. .TP .B \fB$range$\fP If address is of category range the range defined with extra keyword \fIrange\fP will be used here in place of one octet.This is why the range can span from 0000\-ffff. Clients will get an address out of the given range. .TP .B \fB$random64$\fP A 64 bit random address will be generated in place of this variable. Clients get a random address just like they would if privacy extensions were used. The random part will span over 4 octets. .TP .B \fB$prefix\fP This placeholder can be used instead of a literal prefix and uses the prefix given at calling dhcpy6d via the \fI\-\-prefix\fP argument like \fI$prefix$::$id$\fP\&. .UNINDENT .TP .B \fBia_type = na|ta\fP IA (Identity Association) types can be one of non\-temporary address \fIna\fP or temporary address \fIta\fP\&. Default and probably most used is \fIna\fP\&. \fIDefault: na\fP .UNINDENT .sp \fBpreferred_lifetime = \fP .INDENT 0.0 .TP .B \fBvalid_lifetime = \fP As default preferred and valid lifetime are set in general settings, but it is configurable individually for every address setting. .TP .B \fBdns_update = yes|no\fP \fIDefault: no\fP .UNINDENT .sp \fBdns_zone = \fP .INDENT 0.0 .TP .B \fBdns_rev_zone = \fP If these addresses should be synchronized with Bind DNS, these three settings have to be set accordingly. The nameserver for updates is set in general settings. .UNINDENT .SS Default Address .sp The address scheme used for the default class \fIclass_default\fP is by default named \fIaddress_default\fP\&. It should be enough if \fIaddress_default\fP is defined, only if unknown clients should get extra nameservers etc. a \fIclass_default\fP has to be set. .INDENT 0.0 .TP .B \fB[address_default]\fP Address scheme used as default for clients which do not match any other class than \fIclass_default\fP\&. .UNINDENT .SH PREFIX DEFINITIONS IN MULTIPLE [PREFIX_] SECTIONS .sp The \fI\fP part of an \fB[prefix_]\fP section is an arbitrarily chosen identifier like \fIcustomers\fP\&. A prefix definition may contain several properties: .INDENT 0.0 .TP .B \fBcategory = range\fP Like addresses prefix have a category. Right now only \fIrange\fP seems to make sense, similar to ranges in addresses being like 0\-ffff. .TP .B \fBrange = \-\fP Sets range for prefix of category \fIrange\fP\&. .INDENT 7.0 .TP .B \fBfrom\fP Starting hex number of range, minimum is 0 .TP .B \fBto\fP Maximum hex limit of range, highest is ffff. .UNINDENT .UNINDENT .sp \fBpattern = 2001:db8:$range$::\fP .INDENT 0.0 .TP .B \fBpattern= $prefix$:$range$::\fP Patterns allow one to design the addresses according to their category. See examples section below to make it more clear. .INDENT 7.0 .TP .B \fB$range$\fP If address is of category range the range defined with extra keyword \fIrange\fP will be used here in place of one octet. This is why the range can span from 0000\-ffff. Clients will get an address out of the given range. .UNINDENT .TP .B \fBlength = \fP Length of prefix given out to clients. .UNINDENT .sp \fBpreferred_lifetime = \fP .INDENT 0.0 .TP .B \fBvalid_lifetime = \fP As default preferred and valid lifetime are set in general settings, but it is configurable individually for every prefixk setting. .TP .B \fBroute_link_lokal = yes|no\fP As default Link Local Address of requesting client is not used as router address for external call. Instead the client should be able to retrieve exactly 1 address from server to be used as router for the delegated prefix. Alternatively the client Link Local Address might be used by enabling this option. \fIDefault: no\fP .UNINDENT .SH CLASS DEFINITIONS IN MULTIPLE [CLASS_] SECTIONS .sp The \fI\fP part of an \fB[class_]\fP section is an arbitrarily chosen identifier like \fIclients\fP or \fIinvalid_clients\fP\&. Clients can be grouped in classes. Different classes can have different properties, different address sets and different numbers of addresses. Classes also might have different name servers, time intervals, filters and interfaces. .sp A client gets the addresses, nameserver and T1/T2 values of the class which it is configured for in client configuration database or file. .INDENT 0.0 .TP .B \fBaddresses = [ ...]\fP A class can contain as many addresses as needed. Their names have to be separated by spaces. \fIName\fP means the \fIname\fP\-part of an address section like \fI[address_name]\fP\&. If a class does not contain any addresses clients won\(aqt get any address except they have one fixed defined in client configuration file or database. .TP .B \fBprefixes = [ ...]\fP A class can contain prefixes \- even most probably only one prefix will be useful. \fIName\fP means the \fIname\fP\-part of a prefiy section. .TP .B \fBanswer = normal|noaddress|none\fP Normally a client will get an answer, but if for whatever reason is a need to give it an \fINoAddrAvail\fP message back or completely ignore the client it can be set here. \fIDefault: normal\fP .TP .B \fBnameserver = [ ...]\fP Each class can have its own nameservers. If this option is used it replaces the nameservers from general settings. .UNINDENT .sp \fBt1 = \fP .INDENT 0.0 .TP .B \fBt2 = \fP Each class can have its own \fBt1\fP and \fBt2\fP values. The ones from general settings will be overridden. Might be of use for some invalid\-but\-about\-to\-become\-valid\-somehow\-soon class. .UNINDENT .sp \fBfilter_hostname = \fP .sp \fBfilter_mac = \fP .INDENT 0.0 .TP .B \fBfilter_duid = \fP Filters allow one to apply a class to a client not by configuration but by a matching regular expression filter. Most useful might be the filtering by hostname, but maybe there is some use for DUID and MAC address based filtering too. The regular expressions are meant to by Python Regular Expressions. See \fI\%https://docs.python.org/2/howto/regex.html\fP and examples section below for details. .TP .B \fBinterface = [ ...]\fP It is possible to let a class only apply on specific interfaces. These have to be separated by spaces. .TP .B \fBadvertise = addresses|prefixes\fP A class per default allows one to advertise addresses as well as prefixes if requested. This option allows one to narrow the answers down to either \fIaddresses\fP or \fIprefixes\fP\&. \fIDefault: addresses\fP .TP .B \fBcall_up = [$prefix$] [$length$] [$router$]\fP When a route is requested and accepted the custom \fIexecutable\fP will called and the optional but senseful variables will be filled with their appropriate values. .INDENT 7.0 .TP .B \fB$prefix$\fP Contains the prefix advertised to the client. .TP .B \fB$length$\fP The prefix length. .TP .B \fB$router$\fP The host which routes into the advertised prefix \- of course the requesting client IPv6. .UNINDENT .TP .B \fBcall_down = [$prefix$] [$length$] [$router$]\fP When a route is released the custom \fIexecutable\fP will called and the optional but senseful variables will be filled with their appropriate values. .INDENT 7.0 .TP .B \fB$prefix$\fP Contains the prefix advertised to the client. .TP .B \fB$length$\fP The prefix length. .TP .B \fB$router$\fP The host which routes into the advertised prefix \- of course the requesting client IPv6. .UNINDENT .TP .B \fBbootfiles = [ ...]\fP List of PXE bootfiles to evaluate for clients in this client. Each value must refer a bootfile section (see below). Each bootfile is evaluated by the filter defined in the bootfile section, the first machting bootfile is chosen. .sp Example: .INDENT 7.0 .INDENT 3.5 \fIbootfiles = eth1_ipxe eth1_efi64 eth1_efi32 eth1_efibc\fP .UNINDENT .UNINDENT .UNINDENT .SS Default Class .sp At the moment every client which does not match any other class by client configuration or filter automatically matches the class "default". This class could get an address scheme too. It should be enough if \(aqaddress_default\(aq is defined, only if unknown clients should get extra nameservers etc. a \(aqclass_default\(aq has to be set. .INDENT 0.0 .TP .B \fB[class_default]\fP Default class for all clients that do not match any other class. Like any other class it might contain all options that appyl to a class. .TP .B \fB[class_default_]\fP If dhcpy6d listens at multiple interfaces, one can define a default class for every \(aqinterface\(aq. .UNINDENT .SH BOOTFILE DEFINITIONS IN MULTIPLE [BOOTFILE_] SECTIONS .sp The \fI\fP part of an \fB[bootfile_]\fP section is an arbitrarily chosen identifier like \fIefi32\fP, \fIbios\fP or \fIefi64\fP\&. Each bootfile can be restricted to an architecture and/or an user class which is sent by the PXE client. .INDENT 0.0 .TP .B \fBbootfile_url = \fP The bootfile URL in a format like \fItftp://[2001:db8:85a3::8a2e:370:7334]/pxe.efi\fP\&. The possible protocols are dependent on the PXE client, TFTP should be supported by almost every client. .TP .B \fBclient_architecture = \fP Optionally restrict the bootfile to a specific CPU architecture. If the client doesn\(aqt match the requirement, the next bootfile assigned to the class definition is chosen or no bootfile is provided, if there are no further alternatives. .sp Either the integer identifier for an architecture is possible (e.g. 0009 for EFI x86\-64). The integer must consists of four numeric digits, empty digits must be written as zero (e.g. 9 => 0009). For a full list of possible integer identifier see \fI\%https://tools.ietf.org/html/rfc4578#section\-2.1\fP\&. Alternatively the well\-known names of registered CPU architectures defined in RF4578 can be used: .INDENT 7.0 .IP \(bu 2 Intel x86PC .IP \(bu 2 NEC/PC98 .IP \(bu 2 EFI Itanium .IP \(bu 2 DEC Alpha .IP \(bu 2 Arc x86 .IP \(bu 2 Intel Lean Client .IP \(bu 2 EFI IA32 .IP \(bu 2 EFI BC .IP \(bu 2 EFI Xscale .IP \(bu 2 EFI x86\-64 .UNINDENT .TP .B \fBuser_class = \fP Optionally restrict this bootfile to PXE clients sending this user class. The \fIuser_class\fP is matched against the value of the client with simple comparison (no regular expression). .sp Example: .INDENT 7.0 .INDENT 3.5 \fIuser_class = iPXE\fP .UNINDENT .UNINDENT .sp This restricts the bootfile to the iPXE boot firmware. .UNINDENT .SH EXAMPLES .sp The following paragraphs contain some hopefully helpful examples: .SS Minimal configuration .INDENT 0.0 .INDENT 3.5 Here in this minimalistic example the server daemon listens on interface eth0. It does not use any client configuration source but answers requests with default addresses. These are made of the pattern fd01:db8:dead:bad:beef:$mac$ and result in addresses like fd01:db8:deaf:bad:beef:1020:3040:5060 if the MAC address of the requesting client was 10:20:30:40:50:60. .nf .in +2 [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Some server DUID. serverduid = 0001000134824528134567366121 # Do not identify and configure clients. store_config = none # SQLite DB for leases and LLIP\-MAC\-mapping. store_volatile = sqlite store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite # Special address type which applies to all not specially. # configured clients. [address_default] # Choosing MAC\-based addresses. category = mac # ULA\-type address pattern. pattern = fd01:db8:dead:bad:beef:$mac$ .in -2 .fi .sp .UNINDENT .UNINDENT .SS Configuration with valid and unknown clients .INDENT 0.0 .INDENT 3.5 This example shows some more complexity. Here only valid hosts will get a random global address from 2001:db8::/64. Unknown clients get a default ULA range address from fc00::/7. .nf .in +2 [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Server DUID \- if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Non\-privileged user/group. user = dhcpy6d group = dhcpy6d # Nameservers for option 23 \- there can be several specified separated by spaces. nameserver = fd00:db8::53 # Domain to be used for option 39 \- host FQDN. domain = example.com # Domain search list for option 24 \- domain search list. # If omitted the value of option "domain" above is taken as default. domain_search_list = example.com # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = /var/log/dhcpy6d.log # Use SQLite for client configuration. store_config = sqlite # Use SQLite for volatile data. store_volatile = sqlite # Paths to SQLite database files. store_sqlite_config = /var/lib/dhcpy6d/config.sqlite store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite # Declare which attributes of a requesting client should be checked # to prove its identity. It is possible to mix them, separated by spaces. identification = mac # Declare if all checked attributes have to match or is it enough if # some do. Kind of senseless with just one attribute. identification_mode = match_all # These lifetimes are also used as default for addresses which # have no extra defined lifetimes. preferred_lifetime = 43200 valid_lifetime = 64800 t1 = 21600 t2 = 32400 # ADDRESS DEFINITION # Addresses for proper valid clients. [address_valid_clients] # Better privacy for global addresses with category random. category = random # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # Default addresses for unknown invalid clients. [address_default] # Unknown clients will get an internal ULA range\-based address. category = range # The keyword "range" sets the range used in pattern. range = 1000\-1fff # This pattern results in addresses like fd00::1234. pattern = fd00::$range$ # CLASS DEFINITION # Class for proper valid client. [class_valid_clients] # At least one of the above address schemes has to be set. addresses = valid_clients # Valid clients get a different nameserver. nameserver = 2001:db8::53 # Default class for unknown hosts \- only necessary here because of time interval settings. [class_default] addresses = default # Short interval of address refresh attempts so that a client\(aqs status # change will be reflected in IPv6 address soon. t1 = 600 t2 = 900 .in -2 .fi .sp .UNINDENT .UNINDENT .SS Configuration with 2 network segments, servers, valid and unknown clients .INDENT 0.0 .INDENT 3.5 This example uses 2 network segments, one for servers and one for clients. Servers here only get local ULA addresses. Valid clients get 2 addresses, one local ULA and one global GUA address. This feature of DHCPv6 is at the moment only well supported by Windows clients. Unknown clients will get a local ULA address. Only valid clients and servers will get information about nameservers. .nf .in +2 [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interfaces to listen to multicast ff02::1:2. # eth1 \- client network # eth2 \- server network interface = eth1 eth2 # Server DUID \- if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Non\-privileged user/group. user = dhcpy6d group = dhcpy6d # Domain to be used for option 39 \- host FQDN. domain = example.com # Domain search list for option 24 \- domain search list. # If omited the value of option "domain" above is taken as default. domain_search_list = example.com # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = /var/log/dhcpy6d.log # Use MySQL for client configuration. store_config = mysql # Use MySQL for volatile data. store_volatile = mysql # Data used for MySQL storage. store_db_host = localhost store_db_db = dhcpy6d store_db_user = dhcpy6d store_db_password = dhcpy6d # Declare which attributes of a requesting client should be checked # to prove its identity. It is possible to mix them, separated by spaces. identification = mac # Declare if all checked attributes have to match or is it enough if # some do. Kind of senseless with just one attribute. identification_mode = match_all # These lifetimes are also used as default for addresses which # have no extra defined lifetimes. preferred_lifetime = 43200 valid_lifetime = 64800 t1 = 21600 t2 = 32400 # ADDRESS DEFINITION # Global addresses for proper valid clients (GUA). [address_valid_clients_global] # Better privacy for global addresses with category random. category = random # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # Local addresses for proper valid clients (ULA). [address_valid_clients_local] # Local addresses need no privacy, so they will be based of range. category = range range = 2000\-2FFF # Valid clients will get local ULA addresses from fd01::/64. pattern = fd01::$range$ # Servers in servers network will get local addresses based on IDs from client configuration. [address_servers] # IDs are set in client configuration database in range of 0\-FFFF. category = id # Servers will get local ULA addresses from fd02::/64. pattern = fd02::$id$ # Default addresses for unknown invalid clients [address_default] # Unknown clients will get an internal ULA range\-based address. category = range # The keyword "range" sets the range used in pattern. range = 1000\-1FFF # This pattern results in addresses like fd00::1234. pattern = fd00::$range$ # CLASS DEFINITION # Class for proper valid client. [class_valid_clients] # Clients only exist in network linked with eth1. interface = eth1 # Valid clients get 2 addresses, one local ULA and one global GUA # (only works reliably with Windows clients). addresses = valid_clients_global valid_clients_local # Only valid clients get a nameserver from server network. nameserver = fd02::53 # Class for servers in network on eth2 [class_servers] # Servers only exist in network linked with eth2. interface = eth2 # Only local addresses for servers. addresses = servers # Nameserver from server network. nameserver = fd02::53 # Default class for unknown hosts \- only necessary here because of time interval settings [class_default] addresses = default # Short interval of address refresh attempts so that a client\(aqs status # change will be reflected in IPv6 address soon. t1 = 600 t2 = 900 .in -2 .fi .sp .UNINDENT .UNINDENT .SS Configuration with dynamic DNS Updates .INDENT 0.0 .INDENT 3.5 In this example the hostnames of valid clients will be registered in the Bind DNS server. The zones to be updated are configured for every address definition. Here only the global GUA addresses for valid clients will be updated in DNS. The hostnames will be taken from client configuration data \- the ones supplied by the clients are ignored. .nf .in +2 [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Server DUID \- if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Non\-privileged user/group. user = dhcpy6d group = dhcpy6d # Nameservers for option 23 \- there can be several specified separated by spaces. nameserver = fd00:db8::53 # Domain to be used for option 39 \- host FQDN. domain = example.com # Domain search list for option 24 \- domain search list. # If omited the value of option "domain" above is taken as default. domain_search_list = example.com # This works at the moment only for ISC Bind nameservers. dns_update = yes # RNDC key name for DNS Update. dns_rndc_key = rndc\-key # RNDC secret \- mostly some MD5\-hash. Take it from # nameservers\(aq /etc/rndc.key. dns_rndc_secret = 0123456789012345679 # Nameserver to talk to. dns_update_nameserver = ::1 # Regarding RFC 4704 5. there are 3 kinds of client behaviour # for N O S bits: # \- client wants to update DNS itself \-> sends 0 0 0 # \- client wants server to update DNS \-> sends 0 0 1 # \- client wants no server DNS update \-> sends 1 0 0 # Ignore client ideas about DNS (if at all, what name to use, self\-updating...) # Here client hostname is taken from client configuration dns_ignore_client = yes # Do logging. log = yes # Log to console. log_console = no # Path to logfile. log_file = /var/log/dhcpy6d.log # Use SQLite for client configuration. store_config = sqlite # Use SQLite for volatile data. store_volatile = sqlite # Paths to SQLite database files. store_sqlite_config = config.sqlite store_sqlite_volatile = volatile.sqlite # Declare which attributes of a requesting client should be checked # to prove its identity. It is possible to mix them, separated by spaces. identification = mac # ADDRESS DEFINITION # Addresses for proper valid clients. [address_valid_clients] # Better privacy for global addresses with category random. category = random # The following pattern will result in addresses like 2001:0db8::d3f6:834a:03d5:139c. pattern = 2001:db8::$random64$ # Update these addresses in Bind DNS dns_update = yes # Zone to update. dns_zone = example.com # Reverse zone to update dns_rev_zone = 8.b.d.0.1.0.0.2.ip6.arpa # Default addresses for unknown invalid clients. [address_default] # Unknown clients will get an internal ULA range\-based address. category = range # The keyword "range" sets the range used in pattern. range = 1000\-1FFF # This pattern results in addresses like fd00::1234. pattern = fd00::$range$ # CLASS DEFINITION # Class for proper valid client. [class_valid_clients] # At least one of the above address schemes has to be set. addresses = valid_clients # Valid clients get a different nameserver. nameserver = 2001:db8::53 .in -2 .fi .sp .UNINDENT .UNINDENT .SS Configuration with filter .INDENT 0.0 .INDENT 3.5 In this example the membership of a client to a class is defined by a filter for hostnames. All Windows machines have win*\-names here and when requesting an address this hostname gets filtered. .nf .in +2 [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Server DUID \- if not set there will be one generated every time dhcpy6d starts. # This might cause trouble for Windows clients because they go crazy about the # changed server DUID. serverduid = 0001000134824528134567366121 # Use no client configuration. store_config = none # Use SQLite for volatile data. store_volatile = sqlite # Paths to SQLite database file. store_sqlite_volatile = volatile.sqlite # ADDRESS DEFINITION [address_local] category = range range = 1000\-1FFF pattern = fd00::$range$ [address_global] category = random pattern = 2001:638::$random64$ # CLASS DEFINITION [class_windows] addresses = local # Python regular expressions to be used here filter_hostname = win.* [class_default] addresses = global .in -2 .fi .sp .UNINDENT .UNINDENT .SS Configuration with prefixes .sp Here dhcpy6d also provides prefixes in the default class. To avoid heavy load by bad clients request limits are activated. .INDENT 0.0 .INDENT 3.5 .nf .in +2 [dhcpy6d] interface = eth0 server_preference = 255 store_config = none store_volatile = sqlite store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite log = on log_console = yes log_syslog = yes log_file = /var/log/dhcpy6d.log identification_mode = match_all identification = mac nameserver = 2001:db8::53 ntp_server = 2001:db8::123 # Mitigate ugly and aggressive clients request_limit = yes request_limit_time = 30 request_limit_count = 10 request_limit_identification = llip ignore_iaid = yes ignore_unknown_clients = yes advertise = addresses prefixes manage_routes_at_start = yes [address_default] category = mac pattern = 2001:db8::$mac$ [prefix_default] category = range range = 0000\-ffff pattern = 2001:db8:0:$range$:: route_link_local = yes length = 64 [class_default] addresses = default prefixes = default call_up = sudo ip \-6 route add $prefix$/$length$ via $router$ dev eth0 call_down = sudo ip \-6 route delete $prefix$/$length$ via $router$ dev eth0 .in -2 .fi .sp .UNINDENT .UNINDENT .SS Only use fixed addresses .sp If no addresses should be generated, the clients need to have an address defined in their configuration file or database. It looks like this: .INDENT 0.0 .INDENT 3.5 .nf [example\-client] hostname = example\-client mac = 01:02:03:04:05:06 class = fixed_address address = 2001:db8::1234 .fi .sp .UNINDENT .UNINDENT .sp The according class of the client simply must not have any address definition an might as well stay empty: .INDENT 0.0 .INDENT 3.5 .nf .in +2 [dhcpy6d] # Set to yes to really answer to clients. really_do_it = yes # Interface to listen to multicast ff02::1:2. interface = eth0 # Some server DUID. serverduid = 0001000134824528134567366121 # Do not identify and configure clients. store_config = none # SQLite DB for leases and LLIP\-MAC\-mapping. store_volatile = sqlite store_sqlite_volatile = /var/lib/dhcpy6d/volatile.sqlite # Special address type which applies to all not specially. # configured clients. [address_default] # Choosing MAC\-based addresses. category = mac # ULA\-type address pattern. pattern = fd01:db8:dead:bad:beef:$mac$ # To use the EUI\-64 instead of the plain MAC address: #category = eui64 #pattern = fd01:db8:dead:bad:$eui64$ [class_fixed_address] # just no address definiton here .in -2 .fi .sp .UNINDENT .UNINDENT .SS Supply a PXE bootfile for different CPU architectures and user classes .sp This example how to assign PXE bootfiles depending on CPU architecture and user class: .INDENT 0.0 .INDENT 3.5 .nf [class_default_eth1] bootfiles = eth1_ipxe eth1_efi64 eth1_efi32 eth1_efibc addresses = eth1 interface = eth1 nameserver = fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd 2001:0470:76aa:00f5:5054:00ff:fec2:c5dd filter_mac = .* [address_eth1] # Choosing EUI\-64\-based addresses. category = eui64 # ULA\-type address pattern. pattern = fdff:cc21:56df:8bc8::$eui64$ [bootfile_eth1_ipxe] user_class = iPXE bootfile_url = \fI\%tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/default.ipxe\fP [bootfile_eth1_efi32] client_architecture = 0006 bootfile_url = \fI\%tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/efi32/ipxe.efi\fP [bootfile_eth1_efibc] client_architecture = 0007 bootfile_url = \fI\%tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/efi64/ipxe.efi\fP [bootfile_eth1_efi64] client_architecture = 0009 bootfile_url = \fI\%tftp://[fdff:cc21:56df:8bc8:5054:00ff:fec2:c5dd]/efi32/ipxe.efi\fP [bootfile_eth2_ipxe] user_class = iPXE bootfile_url = \fI\%tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/default.ipxe\fP [bootfile_eth2_efi32] client_architecture = 0006 bootfile_url = \fI\%tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/efi32/ipxe.efi\fP [bootfile_eth2_efibc] client_architecture = 0007 bootfile_url = \fI\%tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/efi64/ipxe.efi\fP [bootfile_eth2_efi64] client_architecture = 0009 bootfile_url = \fI\%tftp://[fdff:cc21:56df:fe1d:5054:00ff:fe3f:5da0]/efi32/ipxe.efi\fP .fi .sp .UNINDENT .UNINDENT .sp At first there is a check for the iPXE boot firmware, which delivers an iPXE script on success. Otherwise the iPXE binary matching to the architecture is served. .SH LICENSE .sp This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. .sp This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. .sp You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110\-1301 USA .sp On Debian systems, the full text of the GNU General Public License version 2 can be found in the file \fI/usr/share/common\-licenses/GPL\-2\fP\&. .SH SEE ALSO .INDENT 0.0 .IP \(bu 2 dhcpy6d(8) .IP \(bu 2 dhcpy6d\-clients.conf(5) .IP \(bu 2 \fI\%https://dhcpy6d.de\fP .IP \(bu 2 \fI\%https://github.com/HenriWahl/dhcpy6d\fP .UNINDENT .SH AUTHOR Copyright (C) 2012-2022 Henri Wahl .SH COPYRIGHT This manual page is licensed under the GPL-2 license. .\" Generated by docutils manpage writer. . dhcpy6d-1.2.3/man/man8/000077500000000000000000000000001437472361000145035ustar00rootroot00000000000000dhcpy6d-1.2.3/man/man8/dhcpy6d.8000066400000000000000000000116271437472361000161440ustar00rootroot00000000000000.\" Man page generated from reStructuredText. . .TH DHCPY6D 8 "2020-12-21" "1.0.3" "" .SH NAME dhcpy6d \- MAC address aware DHCPv6 server . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp \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] .SH DESCRIPTION .sp \fBdhcpy6d\fP is an open source server for DHCPv6, the DHCP protocol for IPv6. .sp Its development is driven by the need to be able to use the existing IPv4 infrastructure in coexistence with IPv6. In a dualstack scenario, the existing DHCPv4 most probably uses MAC addresses of clients to identify them. This is not intended by RFC 3315 for DHCPv6, but also not forbidden. Dhcpy6d is able to do so in local network segments and therefore offers a pragmatical method for parallel use of DHCPv4 and DHCPv6, because existing client management solutions could be used further. .sp \fBdhcpy6d\fP comes with the following features: .INDENT 0.0 .IP \(bu 2 identifies clients by MAC address, DUID or hostname .IP \(bu 2 generates addresses randomly, by MAC address, by range, by given ID or from DNS name .IP \(bu 2 filters clients by MAC, DUID or hostname .IP \(bu 2 assigns multiple addresses per client .IP \(bu 2 allows one to organize clients in different classes .IP \(bu 2 stores leases in MySQL, PostgreSQL or SQLite database .IP \(bu 2 client information can be retrieved from MySQL or PostgreSQL database or textfile .IP \(bu 2 dynamically updates DNS (Bind) .IP \(bu 2 supports rapid commit .IP \(bu 2 listens on multiple interfaces .UNINDENT .SH OPTIONS .sp Most configuration is done via the configuration file. .INDENT 0.0 .TP .B \fB\-c, \-\-config=\fP Set the configuration file to use. Default is /etc/dhcpy6d.conf. .TP .B \fB\-u, \-\-user=\fP Set the unprivileged user to be used. .TP .B \fB\-g, \-\-group=\fP Set the unprivileged group to be used. .TP .B \fB\-r, \-\-really\-do\-it=\fP Really activate the DHCPv6 server. This is a precaution to prevent larger network trouble. .TP .B \fB\-d, \-\-duid=\fP Set the DUID for the server. This argument is used by /etc/init.d/dhcpy6d and /lib/systemd/system/dhcpy6d.service respectively. .TP .B \fB\-p, \-\-prefix=\fP Set the prefix which will be substituted for the $prefix$ variable in address definitions. Useful for setups where the ISP uses a changing prefix. .TP .B \fB\-G, \-\-generate\-duid\fP Generate DUID to be used in config file. This argument is used to generate a DUID for /etc/default/dhcpy6d. After generation dhcpy6d exits. .TP .B \fB\-m, \-\-message ""\fP Send message to running dhcpy6d server. At the moment the only valid message is \fI"prefix "\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 "prefix 2001:db8"\fP\&. .UNINDENT .SH FILES .INDENT 0.0 .IP \(bu 2 /etc/dhcpy6d.conf .IP \(bu 2 /etc/dhcpy6d\-clients.conf .IP \(bu 2 /var/lib/dhcpy6d/ .IP \(bu 2 /var/log/dhcpy6d.log .UNINDENT .SH LICENSE .sp This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. .sp This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. .sp You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110\-1301 USA .sp On Debian systems, the full text of the GNU General Public License version 2 can be found in the file \fI/usr/share/common\-licenses/GPL\-2\fP\&. .SH SEE ALSO .INDENT 0.0 .IP \(bu 2 dhcpy6d.conf(5) .IP \(bu 2 dhcpy6d\-clients.conf(5) .IP \(bu 2 \fI\%https://dhcpy6d.de\fP .IP \(bu 2 \fI\%https://github.com/HenriWahl/dhcpy6d\fP .UNINDENT .SH AUTHOR Copyright (C) 2012-2022 Henri Wahl .SH COPYRIGHT This manual page is licensed under the GPL-2 license. .\" Generated by docutils manpage writer. . dhcpy6d-1.2.3/redhat/000077500000000000000000000000001437472361000143345ustar00rootroot00000000000000dhcpy6d-1.2.3/redhat/dhcpy6d.spec000066400000000000000000000127541437472361000165620ustar00rootroot00000000000000%{?!dhcpy6d_uid: %define dhcpy6d_uid dhcpy6d} %{?!dhcpy6d_gid: %define dhcpy6d_gid %dhcpy6d_uid} %{!?python3_sitelib: %global python3_sitelib %(%{__python3} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} Name: dhcpy6d Version: 1.0.9 Release: 1%{?dist} Summary: DHCPv6 server daemon %if 0%{?suse_version} Group: Productivity/Networking/Boot/Servers %else Group: System Environment/Daemons %endif License: GPLv2 URL: https://dhcpy6d.de Source0: https://github.com/HenriWahl/%{name}/archive/refs/tags/v%{version}.tar.gz # in order to build from tarball # tar -zxvf dhcpy6d-%%{version}.tar.gz -C ~/ dhcpy6d-%%{version}/redhat/init.d/dhcpy6d --strip-components=4&& rpmbuild -ta dhcpy6d-%%{version}.tar.gz&& rm -f ~/dhcpy6d Source1: %{name} BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) BuildArch: noarch BuildRequires: python3 BuildRequires: python3-setuptools BuildRequires: python3-devel Requires: python3 BuildRequires: systemd Requires: systemd %if 0%{?suse_version} Requires: python3-mysql Requires: python3-dnspython %else Requires: python3-distro Requires: python3-dns Requires: python3-PyMySQL %endif Requires: coreutils Requires: filesystem Requires(pre): /usr/sbin/useradd, /usr/sbin/groupadd Requires(post): coreutils, filesystem, systemd Requires(preun): coreutils, /usr/sbin/userdel, /usr/sbin/groupdel Requires: logrotate %description Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack 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. %prep %setup -q %build %py3_build #CFLAGS="%{optflags}" %{__python3} setup.py build %install %{__python3} setup.py install --skip-build --prefix=%{_prefix} --install-scripts=%{_sbindir} --root=%{buildroot} install -p -D -m 644 %{S:1} %{buildroot}%{_unitdir}/%{name}.service install -p -D -m 644 etc/logrotate.d/%{name} %{buildroot}%{_sysconfdir}/logrotate.d/%{name} /bin/chmod 0550 %{buildroot}%{_sbindir}/%{name} %pre # enable that only for non-root user! %if "%{dhcpy6d_uid}" != "root" /usr/sbin/groupadd -f -r %{dhcpy6d_gid} > /dev/null 2>&1 || : /usr/sbin/useradd -r -s /sbin/nologin -d /var/lib/%{name} -M \ -g %{dhcpy6d_gid} %{dhcpy6d_uid} > /dev/null 2>&1 || : %endif # backup existing volatile.sqlite file=/var/lib/%{name}/volatile.sqlite if [ -f ${file} ] then /bin/cp -a ${file} ${file}.backup-%{version}-%{release} fi %post file=/var/log/%{name}.log if [ ! -f ${file} ] then /bin/touch ${file} fi file=/var/lib/%{name}/volatile.sqlite # restore backup volatile.sqlite if [ -f ${file}.backup-%{version}-%{release} ] then /bin/mv ${file}.backup-%{version}-%{release} ${file} fi # set permissions on folder and create empty volatile.sqlite if it does not yet exist if [ ! -f ${file} ] then /bin/touch ${file} /bin/chmod 0775 %{_localstatedir}/lib/%{name} fi /bin/chown %{dhcpy6d_uid}:%{dhcpy6d_gid} ${file} /bin/chmod 0640 ${file} %preun if [ "$1" = "0" ]; then /bin/systemctl %{name}.service stop > /dev/null 2>&1 || : /bin/rm -f /var/lib/%{name}/pid > /dev/null 2>&1 || : %{?stop_on_removal: %{stop_on_removal %{name}} } %{!?stop_on_removal: # undefined /bin/systemctl disable %{name}.service } # enable that only for non-root user! %if "%{dhcpy6d_uid}" != "root" /usr/sbin/userdel %{dhcpy6d_uid} if [ ! `grep %{dhcpy6d_gid} /etc/group` = "" ]; then /usr/sbin/groupdel %{dhcpy6d_uid} fi %endif fi %postun if [ $1 -ge 1 ]; then %{?restart_on_update: %{restart_on_update %{name}} } %{!?restart_on_update: # undefined /bin/systemctl start %{name}.service > /dev/null 2>&1 || : } fi %files %doc %{_defaultdocdir}/* %{_mandir}/man?/* %{_sbindir}/%{name} %{python3_sitelib}/*dhcpy6* %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} %config(noreplace) %{_sysconfdir}/%{name}.conf %exclude %{_localstatedir}/log/%{name}.log %{_unitdir}/%{name}.service %dir %attr(0775,%{dhcpy6d_uid},%{dhcpy6d_gid}) %{_localstatedir}/lib/%{name} %config(noreplace) %attr(0644,%{dhcpy6d_uid},%{dhcpy6d_gid}) %{_localstatedir}/lib/%{name}/volatile.sqlite %changelog * Fri Jul 24 2020 Henri Wahl - 1.0.1-1 - New upstream release * Fri Apr 03 2020 Henri Wahl - 1.0-1 - New upstream release * Mon Apr 30 2018 Henri Wahl - 0.7-1 - New upstream release * Fri Sep 15 2017 Henri Wahl - 0.6-1 - New upstream release * Mon May 29 2017 Henri Wahl - 0.5-1 - New upstream release * Sat Dec 26 2015 Henri Wahl - 0.4.3-1 - New upstream release * Tue Aug 18 2015 Henri Wahl - 0.4.2-1 - New upstream release * Tue Mar 17 2015 Henri Wahl - 0.4.1-1 - New upstream release * Tue Oct 21 2014 Henri Wahl - 0.4-1 - New upstream release * Sun Jun 09 2013 Marcin Dulak - 0.2-1 - RHEL and openSUSE versions based on Christopher Meng's spec * Tue Jun 04 2013 Christopher Meng - 0.2-1 - New upstream release. * Thu May 09 2013 Christopher Meng - 0.1.3-1 - Initial Package. dhcpy6d-1.2.3/setup.py000066400000000000000000000062541437472361000146060ustar00rootroot00000000000000#!/usr/bin/env python3 # dhcpy6d - DHCPv6 server # Copyright (C) 2012-2022 Henri Wahl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the # # Free Software Foundation # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301 # USA import os import os.path from setuptools import setup, find_packages import shutil # workaround to get dhcpy6d-startscript created try: if not os.path.exists('sbin'): os.mkdir('sbin') shutil.copyfile('main.py', 'sbin/dhcpy6d') os.chmod('sbin/dhcpy6d', 0o554) except: print('could not copy main.py to sbin/dhcpy6d') classifiers = [ 'Intended Audience :: System Administrators', 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX', 'Natural Language :: English', 'Programming Language :: Python', 'Topic :: System :: Networking' ] data_files = [('/var/lib/dhcpy6d', ['var/lib/volatile.sqlite']), ('/var/log', ['var/log/dhcpy6d.log']), ('/usr/share/doc/dhcpy6d', ['doc/clients-example.conf', 'doc/config.sql', 'doc/dhcpy6d-example.conf', 'doc/dhcpy6d-minimal.conf', 'doc/LICENSE', 'doc/volatile.sql', 'doc/volatile.postgresql']), ('/usr/share/man/man5', ['man/man5/dhcpy6d.conf.5', 'man/man5/dhcpy6d-clients.conf.5']), ('/usr/share/man/man8', ['man/man8/dhcpy6d.8']), ('/etc', ['etc/dhcpy6d.conf']), ('/usr/sbin', ['sbin/dhcpy6d']), ] setup(name='dhcpy6d', version='1.2.2', license='GNU GPL v2', description='DHCPv6 server daemon', long_description='Dhcpy6d delivers IPv6 addresses for DHCPv6 clients, which can be identified by DUID, hostname or MAC address as in the good old IPv4 days. It allows easy dualstack 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.', author='Henri Wahl', author_email='henri@dhcpy6d.de', url='https://dhcpy6d.de/', download_url='https://dhcpy6d.de/download', requires=['distro', 'dnspython'], packages=find_packages(), classifiers=classifiers, data_files=data_files ) dhcpy6d-1.2.3/var/000077500000000000000000000000001437472361000136555ustar00rootroot00000000000000dhcpy6d-1.2.3/var/lib/000077500000000000000000000000001437472361000144235ustar00rootroot00000000000000dhcpy6d-1.2.3/var/lib/volatile.sqlite000066400000000000000000000000001437472361000174530ustar00rootroot00000000000000dhcpy6d-1.2.3/var/log/000077500000000000000000000000001437472361000144365ustar00rootroot00000000000000dhcpy6d-1.2.3/var/log/dhcpy6d.log000066400000000000000000000000001437472361000164700ustar00rootroot00000000000000