├── .ci ├── checklicenses_config.json ├── setup-integ.sh └── tests.sh ├── .github ├── codecov.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .stickler.yml ├── CNAME ├── Dockerfile ├── LICENSE ├── README.md ├── _config.yml ├── cmds ├── client │ ├── README.md │ └── main.go ├── coredhcp-generator │ ├── README.md │ ├── core-plugins.txt │ ├── coredhcp.go.template │ └── main.go └── coredhcp │ ├── config.yml.example │ ├── file_leases.txt.example │ └── main.go ├── config ├── config.go ├── config_test.go └── errors.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── handler └── handler.go ├── integ └── server6 │ ├── leases-dhcpv6-test.txt │ └── server6.go ├── logger └── logger.go ├── plugins ├── allocators │ ├── allocator.go │ ├── bitmap │ │ ├── bitmap.go │ │ ├── bitmap_ipv4.go │ │ ├── bitmap_ipv4_test.go │ │ └── bitmap_test.go │ ├── ipcalc.go │ └── ipcalc_test.go ├── autoconfigure │ ├── plugin.go │ └── plugin_test.go ├── dns │ ├── plugin.go │ └── plugin_test.go ├── example │ └── plugin.go ├── file │ ├── plugin.go │ └── plugin_test.go ├── ipv6only │ ├── plugin.go │ └── plugin_test.go ├── leasetime │ └── plugin.go ├── mtu │ ├── plugin.go │ └── plugin_test.go ├── nbp │ └── nbp.go ├── netmask │ ├── plugin.go │ └── plugin_test.go ├── plugin.go ├── prefix │ ├── plugin.go │ └── plugin_test.go ├── range │ ├── plugin.go │ ├── storage.go │ └── storage_test.go ├── router │ └── plugin.go ├── searchdomains │ ├── plugin.go │ └── plugin_test.go ├── serverid │ ├── plugin.go │ └── plugin_test.go ├── sleep │ └── plugin.go └── staticroute │ ├── plugin.go │ └── plugin_test.go └── server ├── handle.go ├── sendEthernet.go └── serve.go /.ci/checklicenses_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "gopkg": "github.com/coredhcp/coredhcp", 3 | "licenses": [ 4 | [ 5 | "^// Copyright 2018-present the CoreDHCP Authors\\. All rights reserved", 6 | "// This source code is licensed under the MIT license found in the", 7 | "// LICENSE file in the root directory of this source tree\\." 8 | ] 9 | ], 10 | "accept": [ 11 | ".*\\.go" 12 | ], 13 | "reject": [] 14 | } 15 | -------------------------------------------------------------------------------- /.ci/setup-integ.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | if [[ $UID -ne 0 ]] # TODO: check for permissions instead (we can have CAP_NET_ADMIN without root) 5 | then 6 | sudo "$0" "$@" 7 | exit $? 8 | fi 9 | 10 | # Topology: with one or 3 netns 11 | # 12 | # * 3-netns, for relay operations 13 | # -------------------------------- 14 | # | server (cdhcp_srv <---------) | Upper netns 15 | # -----------------------------|-- 16 | # | (veth pair) 17 | # -----------------------------|--- 18 | # | relay upper (cdhcp_relay_u <-) | 19 | # | | Relay netns 20 | # | relay lower (cdhcp_relay_d <-) | 21 | # -----------------------------|-- 22 | # | (veth pair) 23 | # ------------------------------|-- 24 | # | client (cdhcp_cli <---------) | Lower netns 25 | # --------------------------------- 26 | # 27 | # For 2-netns operation, remove the entire middle layer: 28 | # 29 | # -------------------------------- 30 | # | server (cdhcp_srv <---------) | Upper netns 31 | # -----------------------------|-- 32 | # | (veth pair) 33 | # ------------------------------|-- 34 | # | client (cdhcp_cli <---------) | Lower netns 35 | # --------------------------------- 36 | # 37 | 38 | 39 | # Interface names are limited to 15 chars (IFNAMSIZ=16) 40 | if_server=cdhcp_srv 41 | if_relay_up=cdhcp_relay_u 42 | if_relay_down=cdhcp_relay_d 43 | if_client=cdhcp_cli 44 | 45 | netns_server=coredhcp-upper 46 | netns_relay=coredhcp-middle 47 | netns_client=coredhcp-lower 48 | 49 | netns_direct_server=coredhcp-direct-upper 50 | netns_direct_client=coredhcp-direct-lower 51 | 52 | ula_prefix=${ULA_PREFIX:-fd4f:6b37:542c:b643} 53 | 54 | all_ns=("$netns_server" "$netns_relay" "$netns_client" "$netns_direct_server" "$netns_direct_client") 55 | 56 | # Clean existing namespaces 57 | for netns in "${all_ns[@]}"; do 58 | ip netns delete "$netns" || true 59 | done 60 | [[ $1 == teardown ]] && exit 61 | 62 | # create namespaces 63 | for netns in "${all_ns[@]}"; do 64 | ip netns add "$netns" 65 | done 66 | 67 | # Create the links in one of the relevant netns, to ensure we don't pollute the main netns 68 | ip -n "$netns_client" link add "$if_client" type veth peer name "$if_relay_down" 69 | ip -n "$netns_client" link set "$if_relay_down" netns "$netns_relay" 70 | ip -n "$netns_server" link add "$if_server" type veth peer name "$if_relay_up" 71 | ip -n "$netns_server" link set "$if_relay_up" netns "$netns_relay" 72 | 73 | # configure networking on the veth interfaces 74 | ip -n "$netns_server" addr add "${ula_prefix}:a::1/80" dev "$if_server" 75 | ip -n "$netns_server" addr add "10.0.1.1/24" dev "$if_server" 76 | ip -n "$netns_server" link set "$if_server" up 77 | 78 | ip -n "$netns_client" addr add "${ula_prefix}:b::1/80" dev "$if_client" 79 | ip -n "$netns_client" addr add "10.0.2.1/24" dev "$if_client" 80 | ip -n "$netns_client" link set "$if_client" up 81 | 82 | ip -n "$netns_relay" addr add "${ula_prefix}:b::2/80" dev "$if_relay_down" 83 | ip -n "$netns_relay" addr add "${ula_prefix}:a::2/80" dev "$if_relay_up" 84 | ip -n "$netns_relay" addr add "10.0.2.2/24" dev "$if_relay_down" 85 | ip -n "$netns_relay" addr add "10.0.1.2/24" dev "$if_relay_up" 86 | ip -n "$netns_relay" link set "$if_relay_down" up 87 | ip -n "$netns_relay" link set "$if_relay_up" up 88 | 89 | # Now setup the direct-attach ns (with the same addresses as in the relay scenario) 90 | ip -n "$netns_direct_client" link add "$if_client" type veth peer name "$if_server" 91 | ip -n "$netns_direct_client" link set "$if_server" netns "$netns_direct_server" 92 | 93 | # Use the same addresses as the direct-attached version; with a larger subnet so they can link 94 | ip -n "$netns_direct_server" addr add "${ula_prefix}:a::1/64" dev "$if_server" 95 | ip -n "$netns_direct_server" addr add "10.0.1.1/16" dev "$if_server" 96 | ip -n "$netns_direct_server" link set "$if_server" up 97 | 98 | ip -n "$netns_direct_client" addr add "${ula_prefix}:b::1/64" dev "$if_client" 99 | ip -n "$netns_direct_client" addr add "10.0.2.1/16" dev "$if_client" 100 | ip -n "$netns_direct_client" link set "$if_client" up 101 | 102 | # show what we did 103 | set +x 104 | for netns in "${all_ns[@]}"; do 105 | echo "# Addresses in $netns:" 106 | ip -n "$netns" address list 107 | done 108 | -------------------------------------------------------------------------------- /.ci/tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # because things are never simple. 4 | # See https://github.com/codecov/example-go#caveat-multiple-files 5 | 6 | set -e 7 | echo "" > coverage.txt 8 | 9 | for d in $(go list ./... | grep -v vendor); do 10 | go test -race -coverprofile=profile.out -covermode=atomic $d 11 | if [ -f profile.out ]; then 12 | cat profile.out >> coverage.txt 13 | rm profile.out 14 | fi 15 | done 16 | 17 | for d in $(go list -tags=integration ./... | grep -v vendor); do 18 | # integration tests 19 | go test -c -tags=integration -race -coverprofile=profile.out -covermode=atomic $d 20 | testbin="./$(basename $d).test" 21 | # only run it if it was built - i.e. if there are integ tests 22 | test -x "${testbin}" && sudo "./${testbin}" 23 | if [ -f profile.out ]; then 24 | cat profile.out >> coverage.txt 25 | rm -f profile.out 26 | fi 27 | done 28 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # Map file paths during tests to the canonical path in the repo 2 | fixes: 3 | # Removes the prefix we use during github actions, use glob to support forks 4 | - "src/github.com/*/coredhcp/::" 5 | - "github.com/*/coredhcp/::" 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | coredhcp: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | go: ['1.22', '1.23'] 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | # clone in the gopath 15 | path: src/github.com/${{ github.repository }} 16 | - uses: actions/setup-go@v5 17 | with: 18 | stable: false 19 | go-version: ${{ matrix.go }} 20 | - name: setup environment 21 | run: | 22 | # `env` doesn't allow for variable expansion, so we use the GITHUB_ENV 23 | # trick. 24 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 25 | echo "GO111MODULE=on" >> $GITHUB_ENV 26 | - name: build coredhcp 27 | run: | 28 | set -exu 29 | cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }}/cmds/coredhcp 30 | go build 31 | - uses: actions/upload-artifact@v4 32 | with: 33 | name: coredhcp-${{ matrix.go }} 34 | path: src/github.com/${{ github.repository }}/cmds/coredhcp/coredhcp 35 | if-no-files-found: error 36 | coredhcp-generator: 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | go: ['1.22', '1.23'] 41 | steps: 42 | - uses: actions/checkout@v4 43 | with: 44 | # clone in the gopath 45 | path: src/github.com/${{ github.repository }} 46 | - uses: actions/setup-go@v5 47 | with: 48 | stable: false 49 | go-version: ${{ matrix.go }} 50 | - name: setup environment 51 | run: | 52 | # `env` doesn't allow for variable expansion, so we use the GITHUB_ENV 53 | # trick. 54 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 55 | echo "GOBIN=$GITHUB_WORKSPACE/bin" >> $GITHUB_ENV 56 | - name: build coredhcp-generator 57 | run: | 58 | set -exu 59 | cd "${GITHUB_WORKSPACE}"/src/github.com/${{ github.repository }}/cmds/coredhcp-generator 60 | go build 61 | builddir=$(./coredhcp-generator -f core-plugins.txt) 62 | cd "${builddir}" 63 | ls -l 64 | go mod init "coredhcp" 65 | go mod edit -replace "github.com/coredhcp/coredhcp=${GITHUB_WORKSPACE}/src/github.com/${{ github.repository }}" 66 | go mod tidy 67 | go build 68 | gofmt -w "${builddir}/coredhcp.go" 69 | diff -u "${builddir}/coredhcp.go" "${GITHUB_WORKSPACE}"/src/github.com/${{ github.repository }}/cmds/coredhcp/main.go 70 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | pull_request: 10 | 11 | jobs: 12 | golangci: 13 | name: golangci-lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: 'stable' 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v4 22 | with: 23 | version: v1.62.2 24 | args: --timeout=5m 25 | 26 | # Optional: show only new issues if it's a pull request. The default value is `false`. 27 | only-new-issues: true 28 | 29 | checklicenses: 30 | name: checklicenses 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-go@v5 35 | with: 36 | go-version: 'stable' 37 | - name: check license headers 38 | run: | 39 | set -exu 40 | go get github.com/u-root/u-root/tools/checklicenses 41 | go install github.com/u-root/u-root/tools/checklicenses 42 | $(go env GOPATH)/bin/checklicenses -c .ci/checklicenses_config.json 43 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | go: ['1.22', '1.23'] 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | # clone in the gopath 15 | path: src/github.com/${{ github.repository }} 16 | fetch-depth: 0 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{ matrix.go }} 20 | - run: | 21 | # `env` doesn't allow for variable expansion, so we use the GITHUB_ENV 22 | # trick. 23 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 24 | echo "GO111MODULE=on" >> $GITHUB_ENV 25 | - name: run unit tests 26 | run: | 27 | cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }} 28 | go get -v -t ./... 29 | go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 30 | - name: report coverage to codecov 31 | uses: codecov/codecov-action@v4 32 | with: 33 | files: coverage.txt 34 | disable_search: true 35 | flags: unittests 36 | fail_ci_if_error: true 37 | verbose: true 38 | root_dir: ${{ github.workspace }}/src/github.com/${{ github.repository }} 39 | working-directory: ${{ github.workspace }}/src/github.com/${{ github.repository }} 40 | env: 41 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 42 | integration-tests: 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | go: ['1.22', '1.23'] 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | # clone in the gopath 51 | path: src/github.com/${{ github.repository }} 52 | fetch-depth: 0 53 | - uses: actions/setup-go@v5 54 | with: 55 | go-version: ${{ matrix.go }} 56 | - run: | 57 | # `env` doesn't allow for variable expansion, so we use the GITHUB_ENV 58 | # trick. 59 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 60 | echo "GO111MODULE=on" >> $GITHUB_ENV 61 | - name: setup integ tests 62 | run: | 63 | cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }} 64 | ./.ci/setup-integ.sh 65 | - name: run integ tests 66 | env: 67 | GOCOVERDIR: ${{github.workspace}}/.cover 68 | run: | 69 | mkdir $GOCOVERDIR 70 | cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }} 71 | go get -v -t -tags=integration ./integ/... 72 | for d in integ/*; do 73 | pushd "$d" 74 | go build -tags=integration -race -cover -coverpkg=github.com/coredhcp/coredhcp/... . 75 | testbin=$(basename $d) 76 | test -x "${testbin}" || echo "::error file=${d}::missing binary for integration test ${d}" 77 | # only run it if it was built - i.e. if there are integ tests 78 | test -x "${testbin}" && sudo --preserve-env=GOCOVERDIR "./${testbin}" 79 | if [ $? -ne 0 ]; then 80 | echo "::error file=${d}::Execution of integration tests for ${d} failed" 81 | fi 82 | popd 83 | done 84 | go tool covdata textfmt -i=$GOCOVERDIR -o=coverage.txt 85 | - name: report coverage to codecov 86 | uses: codecov/codecov-action@v4 87 | with: 88 | files: coverage.txt 89 | disable_search: true 90 | flags: integtests 91 | fail_ci_if_error: true 92 | verbose: true 93 | root_dir: ${{ github.workspace }}/src/github.com/${{ github.repository }} 94 | working-directory: ${{ github.workspace }}/src/github.com/${{ github.repository }} 95 | env: 96 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | cmds/coredhcp/leases.txt 3 | cmds/coredhcp/leases4.txt 4 | cmds/coredhcp/config.yml 5 | cmds/coredhcp/coredhcp 6 | cmds/client/client 7 | cmds/coredhcp-generator/coredhcp-generator 8 | .vscode 9 | coverage.txt 10 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | golint: 3 | fixer: true 4 | fixers: 5 | enable: true 6 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | coredhcp.io -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | LABEL BUILD="docker build -t coredhcp/coredhcp -f Dockerfile ." 4 | LABEL RUN="docker run --rm -it coredhcp/coredhcp" 5 | 6 | # Install dependencies 7 | RUN apt-get update && \ 8 | apt-get install -y --no-install-recommends \ 9 | sudo \ 10 | iproute2 \ 11 | # to fetch the Go toolchain 12 | ca-certificates \ 13 | wget \ 14 | # for go get 15 | git \ 16 | # for CGo support 17 | build-essential \ 18 | && \ 19 | rm -rf /var/lib/apt/lists/* 20 | 21 | # install Go 22 | WORKDIR /tmp 23 | RUN set -exu; \ 24 | wget https://golang.org/dl/go1.23.4.linux-amd64.tar.gz ;\ 25 | tar -C / -xvzf go1.23.4.linux-amd64.tar.gz 26 | ENV PATH="$PATH:/go/bin:/build/bin" 27 | ENV GOPATH=/go:/build 28 | 29 | ENV PROJDIR=/build/src/github.com/coredhcp/coredhcp 30 | RUN mkdir -p $PROJDIR 31 | COPY . $PROJDIR 32 | 33 | # build coredhcp 34 | RUN set -exu ;\ 35 | cd $PROJDIR/cmds/coredhcp ;\ 36 | go get -v ./... ;\ 37 | CGO_ENABLED=1 go build ;\ 38 | cp coredhcp /bin 39 | 40 | EXPOSE 67/udp 41 | EXPOSE 547/udp 42 | 43 | CMD coredhcp --conf /etc/coredhcp/config.yaml 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 coredhcp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coredhcp 2 | 3 | [![Build Status](https://travis-ci.org/coredhcp/coredhcp.svg?branch=master)](https://travis-ci.org/coredhcp/coredhcp) 4 | [![codecov](https://codecov.io/gh/coredhcp/coredhcp/branch/master/graph/badge.svg)](https://codecov.io/gh/coredhcp/coredhcp) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/coredhcp/coredhcp)](https://goreportcard.com/report/github.com/coredhcp/coredhcp) 6 | 7 | Fast, multithreaded, modular and extensible DHCP server written in Go 8 | 9 | This is still a work-in-progress 10 | 11 | ## Example configuration 12 | 13 | In CoreDHCP almost everything is implemented as a plugin. The order of plugins in the configuration matters: every request is evaluated calling each plugin in order, until one breaks the evaluation and responds to, or drops, the request. 14 | 15 | The following configuration runs a DHCPv6-only server, listening on all the interfaces, using a custom server ID and DNS, and reading the leases from a text file. 16 | 17 | ``` 18 | server6: 19 | # this server will listen on all the available interfaces, on the default 20 | # DHCPv6 server port, and will join the default multicast groups. For more 21 | # control, see the `listen` directive in cmds/coredhcp/config.yml.example . 22 | plugins: 23 | - server_id: LL 00:de:ad:be:ef:00 24 | - file: "leases.txt" 25 | - dns: 8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844 26 | ``` 27 | 28 | For more complex examples, like how to listen on specific interfaces and 29 | configure other plugins, see [config.yml.example](cmds/coredhcp/config.yml.example). 30 | 31 | ## Build and run 32 | 33 | An example server is located under [cmds/coredhcp/](cmds/coredhcp/), so enter that 34 | directory first. To build a server with a custom set of plugins, see the "Server 35 | with custom plugins" section below. 36 | 37 | Once you have a working configuration in `config.yml` (see [config.yml.example](cmds/coredhcp/config.yml.example)), you can build and run the server: 38 | ``` 39 | $ cd cmds/coredhcp 40 | $ go build 41 | $ sudo ./coredhcp 42 | INFO[2019-01-05T22:28:07Z] Registering plugin "file" 43 | INFO[2019-01-05T22:28:07Z] Registering plugin "server_id" 44 | INFO[2019-01-05T22:28:07Z] Loading configuration 45 | INFO[2019-01-05T22:28:07Z] Found plugin: `server_id` with 2 args, `[LL 00:de:ad:be:ef:00]` 46 | INFO[2019-01-05T22:28:07Z] Found plugin: `file` with 1 args, `[leases.txt]` 47 | INFO[2019-01-05T22:28:07Z] Loading plugins... 48 | INFO[2019-01-05T22:28:07Z] Loading plugin `server_id` 49 | INFO[2019-01-05T22:28:07Z] plugins/server_id: loading `server_id` plugin 50 | INFO[2019-01-05T22:28:07Z] plugins/server_id: using ll 00:de:ad:be:ef:00 51 | INFO[2019-01-05T22:28:07Z] Loading plugin `file` 52 | INFO[2019-01-05T22:28:07Z] plugins/file: reading leases from leases.txt 53 | INFO[2019-01-05T22:28:07Z] plugins/file: loaded 1 leases from leases.txt 54 | INFO[2019-01-05T22:28:07Z] Starting DHCPv6 listener on [::]:547 55 | INFO[2019-01-05T22:28:07Z] Waiting 56 | 2019/01/05 22:28:07 Server listening on [::]:547 57 | 2019/01/05 22:28:07 Ready to handle requests 58 | ... 59 | ``` 60 | 61 | Then try it with the local test client, that is located under 62 | [cmds/client/](cmds/client): 63 | ``` 64 | $ cd cmds/client 65 | $ go build 66 | $ sudo ./client 67 | INFO[2019-01-05T22:29:21Z] &{ReadTimeout:3s WriteTimeout:3s LocalAddr:[::1]:546 RemoteAddr:[::1]:547} 68 | INFO[2019-01-05T22:29:21Z] DHCPv6Message 69 | messageType=SOLICIT 70 | transactionid=0x6d30ff 71 | options=[ 72 | OptClientId{cid=DUID{type=DUID-LLT hwtype=Ethernet hwaddr=00:11:22:33:44:55}} 73 | OptRequestedOption{options=[DNS Recursive Name Server, Domain Search List]} 74 | OptElapsedTime{elapsedtime=0} 75 | OptIANA{IAID=[250 206 176 12], t1=3600, t2=5400, options=[]} 76 | ] 77 | ... 78 | ``` 79 | 80 | ## Docker 81 | 82 | There is a [Dockerfile](./Dockerfile) and a [docker-compose.yml](./docker-compose.yml). 83 | 84 | Docker compose expects a configuration file under `/etc/coredhcp/config.yaml`, and it is 85 | mapped to `./etc/coredhcp/config.yaml` when using `docker compose`. You can adjust the exported 86 | volume in `docker-compose.yml` to point to a different configuration file on the host file system. 87 | 88 | There is an example configuration file [config.yml.example](./cmds/coredhcp/config.yml.example) 89 | that you can use as a starting point. 90 | 91 | # Plugins 92 | 93 | CoreDHCP is heavily based on plugins: even the core functionalities are 94 | implemented as plugins. Therefore, knowing how to write one is the key to add 95 | new features to CoreDHCP. 96 | 97 | Core plugins can be found under the [plugins](/plugins/) directory. Additional 98 | plugins can also be found in the 99 | [coredhcp/plugins](https://github.com/coredhcp/plugins) repository. 100 | 101 | ## Server with custom plugins 102 | 103 | To build a server with a custom set of plugins you can use the 104 | [coredhcp-generator](/cmds/coredhcp-generator/) tool. Head there for 105 | documentation on how to use it. 106 | 107 | # How to write a plugin 108 | 109 | The best way to learn is to read the comments and source code of the 110 | [example plugin](plugins/example/), which guides you through the implementation 111 | of a simple plugin that prints a packet every time it is received by the server. 112 | 113 | 114 | # Authors 115 | 116 | * [Andrea Barberio](https://github.com/insomniacslk) 117 | * [Anatole Denis](https://github.com/natolumin) 118 | * [Pablo Mazzini](https://github.com/pmazzini) 119 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /cmds/client/README.md: -------------------------------------------------------------------------------- 1 | # DHCPv6 debug client 2 | 3 | This is a simple dhcpv6 client for use as a debugging tool with coredhcp 4 | 5 | ***This is not a general-purpose DHCP client. This is only a testing/debugging tool for developing CoreDHCP*** 6 | -------------------------------------------------------------------------------- /cmds/client/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package main 6 | 7 | /* 8 | * Sample DHCPv6 client to test on the local interface 9 | */ 10 | 11 | import ( 12 | "flag" 13 | "net" 14 | 15 | "github.com/coredhcp/coredhcp/logger" 16 | "github.com/insomniacslk/dhcp/dhcpv6" 17 | "github.com/insomniacslk/dhcp/dhcpv6/client6" 18 | "github.com/insomniacslk/dhcp/iana" 19 | ) 20 | 21 | var log = logger.GetLogger("main") 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | var macString string 27 | if len(flag.Args()) > 0 { 28 | macString = flag.Arg(0) 29 | } else { 30 | macString = "00:11:22:33:44:55" 31 | } 32 | 33 | c := client6.NewClient() 34 | c.LocalAddr = &net.UDPAddr{ 35 | IP: net.ParseIP("::1"), 36 | Port: 546, 37 | } 38 | c.RemoteAddr = &net.UDPAddr{ 39 | IP: net.ParseIP("::1"), 40 | Port: 547, 41 | } 42 | log.Printf("%+v", c) 43 | 44 | mac, err := net.ParseMAC(macString) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | duid := dhcpv6.DUIDLLT{ 49 | HWType: iana.HWTypeEthernet, 50 | Time: dhcpv6.GetTime(), 51 | LinkLayerAddr: mac, 52 | } 53 | 54 | conv, err := c.Exchange("lo", dhcpv6.WithClientID(&duid)) 55 | for _, p := range conv { 56 | log.Print(p.Summary()) 57 | } 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cmds/coredhcp-generator/README.md: -------------------------------------------------------------------------------- 1 | ## CoreDHCP Generator 2 | 3 | `coredhcp-generator` is a tool used to build CoreDHCP with the plugins you want. 4 | 5 | Why is it even needed? Go is a compiled language with no dynamic loading 6 | support. In order to load a plugin, it has to be compiled in. We are happy to 7 | provide a standard [main.go](/cmds/coredhcp/main.go), and at the same time we 8 | don't want to include plugins that not everyone would use, otherwise the binary 9 | size would grow without control. 10 | 11 | You can use `coredhcp-generator` to generate a `main.go` that includes all the 12 | plugins you wish. Just use it as follows: 13 | 14 | ``` 15 | $ ./coredhcp-generator --from core-plugins.txt 16 | 2019/11/21 23:32:04 Generating output file '/tmp/coredhcp547019106/coredhcp.go' with 7 plugin(s): 17 | 2019/11/21 23:32:04 1) github.com/coredhcp/coredhcp/plugins/file 18 | 2019/11/21 23:32:04 2) github.com/coredhcp/coredhcp/plugins/lease_time 19 | 2019/11/21 23:32:04 3) github.com/coredhcp/coredhcp/plugins/netmask 20 | 2019/11/21 23:32:04 4) github.com/coredhcp/coredhcp/plugins/range 21 | 2019/11/21 23:32:04 5) github.com/coredhcp/coredhcp/plugins/router 22 | 2019/11/21 23:32:04 6) github.com/coredhcp/coredhcp/plugins/server_id 23 | 2019/11/21 23:32:04 7) github.com/coredhcp/coredhcp/plugins/dns 24 | 2019/11/21 23:32:04 Generated file '/tmp/coredhcp547019106/coredhcp.go'. You can build it by running 'go build' in the output directory. 25 | ``` 26 | 27 | You can also specify the plugin list on the command line, or mix it with 28 | `--from`: 29 | ``` 30 | $ ./coredhcp-generator --from core-plugins.txt \ 31 | github.com/coredhcp/plugins/redis 32 | ``` 33 | 34 | Notice that it created a file called `coredhcp.go` in a temporary directory. You 35 | can now `go build` that file and have your own custom CoreDHCP. 36 | 37 | ## Bugs 38 | 39 | CoreDHCP uses Go versioned modules. The generated file does not do that yet. We 40 | will add this feature soon. 41 | -------------------------------------------------------------------------------- /cmds/coredhcp-generator/core-plugins.txt: -------------------------------------------------------------------------------- 1 | github.com/coredhcp/coredhcp/plugins/autoconfigure 2 | github.com/coredhcp/coredhcp/plugins/dns 3 | github.com/coredhcp/coredhcp/plugins/file 4 | github.com/coredhcp/coredhcp/plugins/ipv6only 5 | github.com/coredhcp/coredhcp/plugins/leasetime 6 | github.com/coredhcp/coredhcp/plugins/mtu 7 | github.com/coredhcp/coredhcp/plugins/netmask 8 | github.com/coredhcp/coredhcp/plugins/nbp 9 | github.com/coredhcp/coredhcp/plugins/prefix 10 | github.com/coredhcp/coredhcp/plugins/range 11 | github.com/coredhcp/coredhcp/plugins/router 12 | github.com/coredhcp/coredhcp/plugins/serverid 13 | github.com/coredhcp/coredhcp/plugins/searchdomains 14 | github.com/coredhcp/coredhcp/plugins/sleep 15 | github.com/coredhcp/coredhcp/plugins/staticroute 16 | -------------------------------------------------------------------------------- /cmds/coredhcp-generator/coredhcp.go.template: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | {{/* This file is the template source. The following comment obviously doesn't apply here */ -}} 6 | // This is a generated file, edits should be made in the corresponding source file 7 | // And this file regenerated using `coredhcp-generator --from core-plugins.txt` 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "io" 13 | "os" 14 | 15 | "github.com/coredhcp/coredhcp/config" 16 | "github.com/coredhcp/coredhcp/logger" 17 | "github.com/coredhcp/coredhcp/server" 18 | 19 | "github.com/coredhcp/coredhcp/plugins" 20 | {{- range $plugin := .}} 21 | {{- /* We import all plugins as pl_ to avoid conflicts with reserved keywords */}} 22 | {{importname $plugin}} "{{$plugin}}" 23 | {{- end}} 24 | 25 | "github.com/sirupsen/logrus" 26 | flag "github.com/spf13/pflag" 27 | ) 28 | 29 | var ( 30 | flagLogFile = flag.StringP("logfile", "l", "", "Name of the log file to append to. Default: stdout/stderr only") 31 | flagLogNoStdout = flag.BoolP("nostdout", "N", false, "Disable logging to stdout/stderr") 32 | flagLogLevel = flag.StringP("loglevel", "L", "info", fmt.Sprintf("Log level. One of %v", getLogLevels())) 33 | flagConfig = flag.StringP("conf", "c", "", "Use this configuration file instead of the default location") 34 | flagPlugins = flag.BoolP("plugins", "P", false, "list plugins") 35 | ) 36 | 37 | var logLevels = map[string]func(*logrus.Logger){ 38 | "none": func(l *logrus.Logger) { l.SetOutput(io.Discard) }, 39 | "debug": func(l *logrus.Logger) { l.SetLevel(logrus.DebugLevel) }, 40 | "info": func(l *logrus.Logger) { l.SetLevel(logrus.InfoLevel) }, 41 | "warning": func(l *logrus.Logger) { l.SetLevel(logrus.WarnLevel) }, 42 | "error": func(l *logrus.Logger) { l.SetLevel(logrus.ErrorLevel) }, 43 | "fatal": func(l *logrus.Logger) { l.SetLevel(logrus.FatalLevel) }, 44 | } 45 | 46 | func getLogLevels() []string { 47 | var levels []string 48 | for k := range logLevels { 49 | levels = append(levels, k) 50 | } 51 | return levels 52 | } 53 | 54 | var desiredPlugins = []*plugins.Plugin{ 55 | {{- range $plugin := .}} 56 | &{{importname $plugin}}.Plugin, 57 | {{- end}} 58 | } 59 | 60 | func main() { 61 | flag.Parse() 62 | 63 | if *flagPlugins { 64 | for _, p := range desiredPlugins { 65 | fmt.Println(p.Name) 66 | } 67 | os.Exit(0) 68 | } 69 | 70 | log := logger.GetLogger("main") 71 | fn, ok := logLevels[*flagLogLevel] 72 | if !ok { 73 | log.Fatalf("Invalid log level '%s'. Valid log levels are %v", *flagLogLevel, getLogLevels()) 74 | } 75 | fn(log.Logger) 76 | log.Infof("Setting log level to '%s'", *flagLogLevel) 77 | if *flagLogFile != "" { 78 | log.Infof("Logging to file %s", *flagLogFile) 79 | logger.WithFile(log, *flagLogFile) 80 | } 81 | if *flagLogNoStdout { 82 | log.Infof("Disabling logging to stdout/stderr") 83 | logger.WithNoStdOutErr(log) 84 | } 85 | config, err := config.Load(*flagConfig) 86 | if err != nil { 87 | log.Fatalf("Failed to load configuration: %v", err) 88 | } 89 | // register plugins 90 | for _, plugin := range desiredPlugins { 91 | if err := plugins.RegisterPlugin(plugin); err != nil { 92 | log.Fatalf("Failed to register plugin '%s': %v", plugin.Name, err) 93 | } 94 | } 95 | 96 | // start server 97 | srv, err := server.Start(config) 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | if err := srv.Wait(); err != nil { 102 | log.Error(err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cmds/coredhcp-generator/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "html/template" 11 | "log" 12 | "os" 13 | "path" 14 | "sort" 15 | "strings" 16 | 17 | flag "github.com/spf13/pflag" 18 | ) 19 | 20 | const ( 21 | defaultTemplateFile = "coredhcp.go.template" 22 | importBase = "github.com/coredhcp/coredhcp/" 23 | ) 24 | 25 | var ( 26 | flagTemplate = flag.StringP("template", "t", defaultTemplateFile, "Template file name") 27 | flagOutfile = flag.StringP("outfile", "o", "", "Output file path") 28 | flagFromFile = flag.StringP("from", "f", "", "Optional file name to get the plugin list from, one import path per line") 29 | ) 30 | 31 | var funcMap = template.FuncMap{ 32 | "importname": func(importPath string) (string, error) { 33 | parts := strings.Split(importPath, "/") 34 | if len(parts) < 1 { 35 | return "", fmt.Errorf("no components found in import path '%s'", importPath) 36 | } 37 | return "pl_" + parts[len(parts)-1], nil 38 | }, 39 | } 40 | 41 | func usage() { 42 | fmt.Fprintf(flag.CommandLine.Output(), 43 | "%s [-template tpl] [-outfile out] [-from pluginlist] [plugin [plugin...]]\n", 44 | os.Args[0], 45 | ) 46 | flag.PrintDefaults() 47 | fmt.Fprintln(flag.CommandLine.Output(), ` plugin 48 | Plugin name to include, as go import path. 49 | Short names can be used for builtin coredhcp plugins (eg "serverid")`) 50 | } 51 | 52 | func main() { 53 | flag.Usage = usage 54 | flag.Parse() 55 | 56 | data, err := os.ReadFile(*flagTemplate) 57 | if err != nil { 58 | log.Fatalf("Failed to read template file '%s': %v", *flagTemplate, err) 59 | } 60 | t, err := template.New("coredhcp").Funcs(funcMap).Parse(string(data)) 61 | if err != nil { 62 | log.Fatalf("Template parsing failed: %v", err) 63 | } 64 | plugins := make(map[string]bool) 65 | for _, pl := range flag.Args() { 66 | pl := strings.TrimSpace(pl) 67 | if pl == "" { 68 | continue 69 | } 70 | if !strings.ContainsRune(pl, '/') { 71 | // A bare name was specified, not a full import path. 72 | // Coredhcp plugins aren't in the standard library, and it's unlikely someone 73 | // would put them at the base of $GOPATH/src. 74 | // Assume this is one of the builtin plugins. If needed, use the -from option 75 | // which always requires (and uses) exact paths 76 | 77 | // XXX: we could also look into github.com/coredhcp/plugins 78 | pl = importBase + pl 79 | } 80 | plugins[pl] = true 81 | } 82 | if *flagFromFile != "" { 83 | // additional plugin names from a text file, one line per plugin import 84 | // path 85 | fd, err := os.Open(*flagFromFile) 86 | if err != nil { 87 | log.Fatalf("Failed to read file '%s': %v", *flagFromFile, err) 88 | } 89 | defer func() { 90 | if err := fd.Close(); err != nil { 91 | log.Printf("Error closing file '%s': %v", *flagFromFile, err) 92 | } 93 | }() 94 | sc := bufio.NewScanner(fd) 95 | for sc.Scan() { 96 | pl := strings.TrimSpace(sc.Text()) 97 | if pl == "" { 98 | continue 99 | } 100 | plugins[pl] = true 101 | } 102 | if err := sc.Err(); err != nil { 103 | log.Fatalf("Error reading file '%s': %v", *flagFromFile, err) 104 | } 105 | } 106 | if len(plugins) == 0 { 107 | log.Fatalf("No plugin specified!") 108 | } 109 | outfile := *flagOutfile 110 | if outfile == "" { 111 | tmpdir, err := os.MkdirTemp("", "coredhcp") 112 | if err != nil { 113 | log.Fatalf("Cannot create temporary directory: %v", err) 114 | } 115 | outfile = path.Join(tmpdir, "coredhcp.go") 116 | } 117 | 118 | log.Printf("Generating output file '%s' with %d plugin(s):", outfile, len(plugins)) 119 | idx := 1 120 | for pl := range plugins { 121 | log.Printf("% 3d) %s", idx, pl) 122 | idx++ 123 | } 124 | outFD, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644) 125 | if err != nil { 126 | log.Fatalf("Failed to create output file '%s': %v", outfile, err) 127 | } 128 | defer func() { 129 | if err := outFD.Close(); err != nil { 130 | log.Printf("Error while closing file descriptor for '%s': %v", outfile, err) 131 | } 132 | }() 133 | // WARNING: no escaping of the provided strings is done 134 | pluginList := make([]string, 0, len(plugins)) 135 | for pl := range plugins { 136 | pluginList = append(pluginList, pl) 137 | } 138 | sort.Strings(pluginList) 139 | if err := t.Execute(outFD, pluginList); err != nil { 140 | log.Fatalf("Template execution failed: %v", err) 141 | } 142 | log.Printf("Generated file '%s'. You can build it by running 'go build' in the output directory.", outfile) 143 | fmt.Println(path.Dir(outfile)) 144 | } 145 | -------------------------------------------------------------------------------- /cmds/coredhcp/config.yml.example: -------------------------------------------------------------------------------- 1 | # CoreDHCP configuration (yaml) 2 | 3 | # In this file, lines starting with "## " represent default values, 4 | # while uncommented lines are examples which have no default value 5 | 6 | # The base level configuration has two sections, one for each protocol version 7 | # (DHCPv4 and DHCPv6). There is no shared configuration at the moment. 8 | # At a high level, both accept the same structure of configuration 9 | 10 | # DHCPv6 configuration 11 | server6: 12 | # listen is an optional section to specify how the server binds to an 13 | # interface or address. 14 | # If unset, the server will join the link-layer multicast group for all 15 | # dhcp servers and relays on each interface, as well as the site-scoped 16 | # multicast group for all dhcp servers. 17 | # Note that in this default configuration the server will not handle 18 | # unicast datagrams, and is equivalent to: 19 | ## listen: 20 | ## - "[ff02::1:2]" 21 | ## - "[ff05::1:3]" 22 | 23 | # In general, listen takes a list of addresses, with the general syntax 24 | # "[address%interface]:port", where each part is optional. 25 | # Omitting the address results in the wildcard address being used 26 | # Omitting the interface skips binding the listener to a specific interface 27 | # and listens on all interfaces instead 28 | # Omitting the port uses the default port for DHCPv6 (547) 29 | # 30 | # For example: 31 | # - "[::]" 32 | # Listen on the wildcard address on all interfaces on the default port. 33 | # Note that no multicast group will be joined, so this will *not* work with 34 | # most clients 35 | # 36 | # - ":44480" 37 | # Listens on the wildcard address on a specific port. This can be used if 38 | # you have a relay setup that can contact this server using unicast 39 | # 40 | # - "[::%eno1]" 41 | # Listens on the wildcard address on one interface. This can be used if you 42 | # want to spawn multiple servers with different configurations for multiple 43 | # interfaces, behind a relay that can use unicast 44 | # 45 | # There are some special considerations for multicast: 46 | # - "[ff02::1:2%eno1]" 47 | # Listens on a link-layer multicast address bound to an interface. Also 48 | # used to spawn multiple servers, but for clients on the same subnet 49 | # 50 | # - "[ff05::1:3%eno1]" 51 | # Joining a multicast group with an interface allows to skip the default 52 | # routing table when responding to clients, which can be useful if 53 | # multicast is not otherwise configured system-wide 54 | # 55 | # - "[ff02::1:2]" 56 | # Using a multicast address without an interface will be auto-expanded, so 57 | # that it listens on all available interfaces 58 | 59 | 60 | # plugins is a mandatory section, which defines how requests are handled. 61 | # It is a list of maps, matching plugin names to their arguments. 62 | # The order is meaningful, as incoming requests are handled by each plugin 63 | # in turn. There is no default value for a plugin configuration, and a 64 | # plugin that is not mentioned will not be loaded at all 65 | # 66 | # The following contains examples of the most common, builtin plugins. 67 | # External plugins should document their arguments in their own 68 | # documentations or readmes 69 | plugins: 70 | # server_id is mandatory for RFC-compliant operation. 71 | # - server_id: 72 | # The supported DUID formats are LL and LLT 73 | - server_id: LL 00:de:ad:be:ef:00 74 | 75 | # file serves leases defined in a static file, matching link-layer addresses to IPs 76 | # - file: [autorefresh] 77 | # The file format is one lease per line, " " 78 | # When the 'autorefresh' argument is given, the plugin will try to refresh 79 | # the lease mapping during runtime whenever the lease file is updated. 80 | - file: "leases.txt" 81 | 82 | # dns adds information about available DNS resolvers to the responses 83 | # - dns: <... resolver IPs> 84 | - dns: 2001:4860:4860::8888 2001:4860:4860::8844 85 | 86 | # nbp can add information about the location of a network boot program 87 | # - nbp: 88 | - nbp: "http://[2001:db8:a::1]/nbp" 89 | 90 | # prefix provides prefix delegation. 91 | # - prefix: 92 | # prefix is the prefix pool from which the allocations will be carved 93 | # allocation size is the maximum size for prefixes that will be allocated to clients 94 | # EG for allocating /64 or smaller prefixes within 2001:db8::/48 : 95 | - prefix: 2001:db8::/48 64 96 | 97 | # DHCPv4 configuration 98 | server4: 99 | # listen is an optional section to specify how the server binds to an 100 | # interface or address. 101 | # If unset, the server will listen on the broadcast address on all 102 | # interfaces, equivalent to: 103 | ## listen: 104 | ## - "0.0.0.0" 105 | 106 | # In general, listen takes a list of addresses, with the general syntax 107 | # "address%interface:port", where each part is optional. 108 | # * Omitting the address results in the wildcard address being used 109 | # * Omitting the interface skips binding the listener to a specific interface 110 | # and listens on all interfaces instead 111 | # * Omitting the port uses the default port for DHCPv4 (67) 112 | # 113 | # For example: 114 | # - ":44480" Listens on a specific port. 115 | # - "%eno1" Listens on the wildcard address on one interface. 116 | # - "192.0.2.1%eno1:44480" with all parts 117 | 118 | # plugins is a mandatory section, which defines how requests are handled. 119 | # It is a list of maps, matching plugin names to their arguments. 120 | # The order is meaningful, as incoming requests are handled by each plugin 121 | # in turn. There is no default value for a plugin configuration, and a 122 | # plugin that is not mentioned will not be loaded at all 123 | # 124 | # The following contains examples of the most common, builtin plugins. 125 | # External plugins should document their arguments in their own 126 | # documentations or readmes 127 | plugins: 128 | # lease_time sets the default lease time for advertised leases 129 | # - lease_time: 130 | # The duration can be given in any format understood by go's 131 | # "ParseDuration": https://golang.org/pkg/time/#ParseDuration 132 | - lease_time: 3600s 133 | 134 | # server_id advertises a DHCP Server Identifier, to help resolve 135 | # situations where there are multiple DHCP servers on the network 136 | # - server_id: 137 | # The IP address should be one address where this server is reachable 138 | - server_id: 10.10.10.1 139 | 140 | # dns advertises DNS resolvers usable by the clients on this network 141 | # - dns: <...IP addresses> 142 | - dns: 8.8.8.8 8.8.4.4 143 | 144 | # router is mandatory, and advertises the address of the default router 145 | # for this network 146 | # - router: 147 | - router: 192.168.1.1 148 | 149 | # netmask advertises the network mask for the IPs assigned through this 150 | # server 151 | # - netmask: 152 | - netmask: 255.255.255.0 153 | 154 | # range allocates leases within a range of IPs 155 | # - range: 156 | # * the lease file is an initially empty file where the leases that are 157 | # allocated to clients will be stored across server restarts 158 | # * lease duration can be given in any format understood by go's 159 | # "ParseDuration": https://golang.org/pkg/time/#ParseDuration 160 | - range: leases.sqlite3 10.10.10.100 10.10.10.200 60s 161 | 162 | # staticroute advertises additional routes the client should install in 163 | # its routing table as described in RFC3442 164 | # - staticroute: , [, ...] 165 | # where destination should be in CIDR notation and gateway should be 166 | # the IP address of the router through which the destination is reachable 167 | # - staticroute: 10.20.20.0/24,10.10.10.1 168 | -------------------------------------------------------------------------------- /cmds/coredhcp/file_leases.txt.example: -------------------------------------------------------------------------------- 1 | 00:11:22:33:44:55 2001:2::1 2 | -------------------------------------------------------------------------------- /cmds/coredhcp/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // This is a generated file, edits should be made in the corresponding source file 6 | // And this file regenerated using `coredhcp-generator --from core-plugins.txt` 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "os" 13 | 14 | "github.com/coredhcp/coredhcp/config" 15 | "github.com/coredhcp/coredhcp/logger" 16 | "github.com/coredhcp/coredhcp/server" 17 | 18 | "github.com/coredhcp/coredhcp/plugins" 19 | pl_autoconfigure "github.com/coredhcp/coredhcp/plugins/autoconfigure" 20 | pl_dns "github.com/coredhcp/coredhcp/plugins/dns" 21 | pl_file "github.com/coredhcp/coredhcp/plugins/file" 22 | pl_ipv6only "github.com/coredhcp/coredhcp/plugins/ipv6only" 23 | pl_leasetime "github.com/coredhcp/coredhcp/plugins/leasetime" 24 | pl_mtu "github.com/coredhcp/coredhcp/plugins/mtu" 25 | pl_nbp "github.com/coredhcp/coredhcp/plugins/nbp" 26 | pl_netmask "github.com/coredhcp/coredhcp/plugins/netmask" 27 | pl_prefix "github.com/coredhcp/coredhcp/plugins/prefix" 28 | pl_range "github.com/coredhcp/coredhcp/plugins/range" 29 | pl_router "github.com/coredhcp/coredhcp/plugins/router" 30 | pl_searchdomains "github.com/coredhcp/coredhcp/plugins/searchdomains" 31 | pl_serverid "github.com/coredhcp/coredhcp/plugins/serverid" 32 | pl_sleep "github.com/coredhcp/coredhcp/plugins/sleep" 33 | pl_staticroute "github.com/coredhcp/coredhcp/plugins/staticroute" 34 | 35 | "github.com/sirupsen/logrus" 36 | flag "github.com/spf13/pflag" 37 | ) 38 | 39 | var ( 40 | flagLogFile = flag.StringP("logfile", "l", "", "Name of the log file to append to. Default: stdout/stderr only") 41 | flagLogNoStdout = flag.BoolP("nostdout", "N", false, "Disable logging to stdout/stderr") 42 | flagLogLevel = flag.StringP("loglevel", "L", "info", fmt.Sprintf("Log level. One of %v", getLogLevels())) 43 | flagConfig = flag.StringP("conf", "c", "", "Use this configuration file instead of the default location") 44 | flagPlugins = flag.BoolP("plugins", "P", false, "list plugins") 45 | ) 46 | 47 | var logLevels = map[string]func(*logrus.Logger){ 48 | "none": func(l *logrus.Logger) { l.SetOutput(io.Discard) }, 49 | "debug": func(l *logrus.Logger) { l.SetLevel(logrus.DebugLevel) }, 50 | "info": func(l *logrus.Logger) { l.SetLevel(logrus.InfoLevel) }, 51 | "warning": func(l *logrus.Logger) { l.SetLevel(logrus.WarnLevel) }, 52 | "error": func(l *logrus.Logger) { l.SetLevel(logrus.ErrorLevel) }, 53 | "fatal": func(l *logrus.Logger) { l.SetLevel(logrus.FatalLevel) }, 54 | } 55 | 56 | func getLogLevels() []string { 57 | var levels []string 58 | for k := range logLevels { 59 | levels = append(levels, k) 60 | } 61 | return levels 62 | } 63 | 64 | var desiredPlugins = []*plugins.Plugin{ 65 | &pl_autoconfigure.Plugin, 66 | &pl_dns.Plugin, 67 | &pl_file.Plugin, 68 | &pl_ipv6only.Plugin, 69 | &pl_leasetime.Plugin, 70 | &pl_mtu.Plugin, 71 | &pl_nbp.Plugin, 72 | &pl_netmask.Plugin, 73 | &pl_prefix.Plugin, 74 | &pl_range.Plugin, 75 | &pl_router.Plugin, 76 | &pl_searchdomains.Plugin, 77 | &pl_serverid.Plugin, 78 | &pl_sleep.Plugin, 79 | &pl_staticroute.Plugin, 80 | } 81 | 82 | func main() { 83 | flag.Parse() 84 | 85 | if *flagPlugins { 86 | for _, p := range desiredPlugins { 87 | fmt.Println(p.Name) 88 | } 89 | os.Exit(0) 90 | } 91 | 92 | log := logger.GetLogger("main") 93 | fn, ok := logLevels[*flagLogLevel] 94 | if !ok { 95 | log.Fatalf("Invalid log level '%s'. Valid log levels are %v", *flagLogLevel, getLogLevels()) 96 | } 97 | fn(log.Logger) 98 | log.Infof("Setting log level to '%s'", *flagLogLevel) 99 | if *flagLogFile != "" { 100 | log.Infof("Logging to file %s", *flagLogFile) 101 | logger.WithFile(log, *flagLogFile) 102 | } 103 | if *flagLogNoStdout { 104 | log.Infof("Disabling logging to stdout/stderr") 105 | logger.WithNoStdOutErr(log) 106 | } 107 | config, err := config.Load(*flagConfig) 108 | if err != nil { 109 | log.Fatalf("Failed to load configuration: %v", err) 110 | } 111 | // register plugins 112 | for _, plugin := range desiredPlugins { 113 | if err := plugins.RegisterPlugin(plugin); err != nil { 114 | log.Fatalf("Failed to register plugin '%s': %v", plugin.Name, err) 115 | } 116 | } 117 | 118 | // start server 119 | srv, err := server.Start(config) 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | if err := srv.Wait(); err != nil { 124 | log.Error(err) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package config 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/coredhcp/coredhcp/logger" 15 | "github.com/insomniacslk/dhcp/dhcpv4" 16 | "github.com/insomniacslk/dhcp/dhcpv6" 17 | "github.com/spf13/cast" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var log = logger.GetLogger("config") 22 | 23 | type protocolVersion int 24 | 25 | const ( 26 | protocolV6 protocolVersion = 6 27 | protocolV4 protocolVersion = 4 28 | ) 29 | 30 | // Config holds the DHCPv6/v4 server configuration 31 | type Config struct { 32 | v *viper.Viper 33 | Server6 *ServerConfig 34 | Server4 *ServerConfig 35 | } 36 | 37 | // New returns a new initialized instance of a Config object 38 | func New() *Config { 39 | return &Config{v: viper.New()} 40 | } 41 | 42 | // ServerConfig holds a server configuration that is specific to either the 43 | // DHCPv6 server or the DHCPv4 server. 44 | type ServerConfig struct { 45 | Addresses []net.UDPAddr 46 | Plugins []PluginConfig 47 | } 48 | 49 | // PluginConfig holds the configuration of a plugin 50 | type PluginConfig struct { 51 | Name string 52 | Args []string 53 | } 54 | 55 | // Load reads a configuration file and returns a Config object, or an error if 56 | // any. 57 | func Load(pathOverride string) (*Config, error) { 58 | log.Print("Loading configuration") 59 | c := New() 60 | c.v.SetConfigType("yml") 61 | if pathOverride != "" { 62 | c.v.SetConfigFile(pathOverride) 63 | } else { 64 | c.v.SetConfigName("config") 65 | c.v.AddConfigPath(".") 66 | c.v.AddConfigPath("$XDG_CONFIG_HOME/coredhcp/") 67 | c.v.AddConfigPath("$HOME/.coredhcp/") 68 | c.v.AddConfigPath("/etc/coredhcp/") 69 | } 70 | 71 | if err := c.v.ReadInConfig(); err != nil { 72 | return nil, err 73 | } 74 | if err := c.parseConfig(protocolV6); err != nil { 75 | return nil, err 76 | } 77 | if err := c.parseConfig(protocolV4); err != nil { 78 | return nil, err 79 | } 80 | if c.Server6 == nil && c.Server4 == nil { 81 | return nil, ConfigErrorFromString("need at least one valid config for DHCPv6 or DHCPv4") 82 | } 83 | return c, nil 84 | } 85 | 86 | func protoVersionCheck(v protocolVersion) error { 87 | if v != protocolV6 && v != protocolV4 { 88 | return fmt.Errorf("invalid protocol version: %d", v) 89 | } 90 | return nil 91 | } 92 | 93 | func parsePlugins(pluginList []interface{}) ([]PluginConfig, error) { 94 | plugins := make([]PluginConfig, 0, len(pluginList)) 95 | for idx, val := range pluginList { 96 | conf := cast.ToStringMap(val) 97 | if conf == nil { 98 | return nil, ConfigErrorFromString("dhcpv6: plugin #%d is not a string map", idx) 99 | } 100 | // make sure that only one item is specified, since it's a 101 | // map name -> args 102 | if len(conf) != 1 { 103 | return nil, ConfigErrorFromString("dhcpv6: exactly one plugin per item can be specified") 104 | } 105 | var ( 106 | name string 107 | args []string 108 | ) 109 | // only one item, as enforced above, so read just that 110 | for k, v := range conf { 111 | name = k 112 | args = strings.Fields(cast.ToString(v)) 113 | break 114 | } 115 | plugins = append(plugins, PluginConfig{Name: name, Args: args}) 116 | } 117 | return plugins, nil 118 | } 119 | 120 | // BUG(Natolumin): listen specifications of the form `[ip6]%iface:port` or 121 | // `[ip6]%iface` are not supported, even though they are the default format of 122 | // the `ss` utility in linux. Use `[ip6%iface]:port` instead 123 | 124 | // splitHostPort splits an address of the form ip%zone:port into ip,zone and port. 125 | // It still returns if any of these are unset (unlike net.SplitHostPort which 126 | // returns an error if there is no port) 127 | func splitHostPort(hostport string) (ip string, zone string, port string, err error) { 128 | ip, port, err = net.SplitHostPort(hostport) 129 | if err != nil { 130 | // Either there is no port, or a more serious error. 131 | // Supply a synthetic port to differentiate cases 132 | var altErr error 133 | if ip, _, altErr = net.SplitHostPort(hostport + ":0"); altErr != nil { 134 | // Invalid even with a fake port. Return the original error 135 | return 136 | } 137 | err = nil 138 | } 139 | if i := strings.LastIndexByte(ip, '%'); i >= 0 { 140 | ip, zone = ip[:i], ip[i+1:] 141 | } 142 | return 143 | } 144 | 145 | func (c *Config) getListenAddress(addr string, ver protocolVersion) (*net.UDPAddr, error) { 146 | if err := protoVersionCheck(ver); err != nil { 147 | return nil, err 148 | } 149 | 150 | ipStr, ifname, portStr, err := splitHostPort(addr) 151 | if err != nil { 152 | return nil, ConfigErrorFromString("dhcpv%d: %v", ver, err) 153 | } 154 | 155 | ip := net.ParseIP(ipStr) 156 | if ipStr == "" { 157 | switch ver { 158 | case protocolV4: 159 | ip = net.IPv4zero 160 | case protocolV6: 161 | ip = net.IPv6unspecified 162 | default: 163 | panic("BUG: Unknown protocol version") 164 | } 165 | } 166 | if ip == nil { 167 | return nil, ConfigErrorFromString("dhcpv%d: invalid IP address in `listen` directive: %s", ver, ipStr) 168 | } 169 | if ip4 := ip.To4(); (ver == protocolV6 && ip4 != nil) || (ver == protocolV4 && ip4 == nil) { 170 | return nil, ConfigErrorFromString("dhcpv%d: not a valid IPv%d address in `listen` directive: '%s'", ver, ver, ipStr) 171 | } 172 | 173 | var port int 174 | if portStr == "" { 175 | switch ver { 176 | case protocolV4: 177 | port = dhcpv4.ServerPort 178 | case protocolV6: 179 | port = dhcpv6.DefaultServerPort 180 | default: 181 | panic("BUG: Unknown protocol version") 182 | } 183 | } else { 184 | port, err = strconv.Atoi(portStr) 185 | if err != nil { 186 | return nil, ConfigErrorFromString("dhcpv%d: invalid `listen` port '%s'", ver, portStr) 187 | } 188 | } 189 | 190 | listener := net.UDPAddr{ 191 | IP: ip, 192 | Port: port, 193 | Zone: ifname, 194 | } 195 | return &listener, nil 196 | } 197 | 198 | func (c *Config) getPlugins(ver protocolVersion) ([]PluginConfig, error) { 199 | if err := protoVersionCheck(ver); err != nil { 200 | return nil, err 201 | } 202 | pluginList := cast.ToSlice(c.v.Get(fmt.Sprintf("server%d.plugins", ver))) 203 | if pluginList == nil { 204 | return nil, ConfigErrorFromString("dhcpv%d: invalid plugins section, not a list or no plugin specified", ver) 205 | } 206 | return parsePlugins(pluginList) 207 | } 208 | 209 | func (c *Config) parseConfig(ver protocolVersion) error { 210 | if err := protoVersionCheck(ver); err != nil { 211 | return err 212 | } 213 | if exists := c.v.Get(fmt.Sprintf("server%d", ver)); exists == nil { 214 | // it is valid to have no server configuration defined 215 | return nil 216 | } 217 | // read plugin configuration 218 | plugins, err := c.getPlugins(ver) 219 | if err != nil { 220 | return err 221 | } 222 | for _, p := range plugins { 223 | log.Printf("DHCPv%d: found plugin `%s` with %d args: %v", ver, p.Name, len(p.Args), p.Args) 224 | } 225 | 226 | listeners, err := c.parseListen(ver) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | sc := ServerConfig{ 232 | Addresses: listeners, 233 | Plugins: plugins, 234 | } 235 | if ver == protocolV6 { 236 | c.Server6 = &sc 237 | } else if ver == protocolV4 { 238 | c.Server4 = &sc 239 | } 240 | return nil 241 | } 242 | 243 | // BUG(Natolumin): When listening on link-local multicast addresses without 244 | // binding to a specific interface, new interfaces coming up after the server 245 | // starts will not be taken into account. 246 | 247 | func expandLLMulticast(addr *net.UDPAddr) ([]net.UDPAddr, error) { 248 | if !addr.IP.IsLinkLocalMulticast() && !addr.IP.IsInterfaceLocalMulticast() { 249 | return nil, errors.New("Address is not multicast") 250 | } 251 | if addr.Zone != "" { 252 | return nil, errors.New("Address is already zoned") 253 | } 254 | var needFlags = net.FlagMulticast 255 | if addr.IP.To4() != nil { 256 | // We need to be able to send broadcast responses in ipv4 257 | needFlags |= net.FlagBroadcast 258 | } 259 | 260 | ifs, err := net.Interfaces() 261 | ret := make([]net.UDPAddr, 0, len(ifs)) 262 | if err != nil { 263 | return nil, fmt.Errorf("Could not list network interfaces: %v", err) 264 | } 265 | for _, iface := range ifs { 266 | if (iface.Flags & needFlags) != needFlags { 267 | continue 268 | } 269 | caddr := *addr 270 | caddr.Zone = iface.Name 271 | ret = append(ret, caddr) 272 | } 273 | if len(ret) == 0 { 274 | return nil, errors.New("No suitable interface found for multicast listener") 275 | } 276 | return ret, nil 277 | } 278 | 279 | func defaultListen(ver protocolVersion) ([]net.UDPAddr, error) { 280 | switch ver { 281 | case protocolV4: 282 | return []net.UDPAddr{{Port: dhcpv4.ServerPort}}, nil 283 | case protocolV6: 284 | l, err := expandLLMulticast(&net.UDPAddr{IP: dhcpv6.AllDHCPRelayAgentsAndServers, Port: dhcpv6.DefaultServerPort}) 285 | if err != nil { 286 | return nil, err 287 | } 288 | l = append(l, 289 | net.UDPAddr{IP: dhcpv6.AllDHCPServers, Port: dhcpv6.DefaultServerPort}, 290 | // XXX: Do we want to listen on [::] as default ? 291 | ) 292 | return l, nil 293 | } 294 | return nil, errors.New("defaultListen: Incorrect protocol version") 295 | } 296 | 297 | func (c *Config) parseListen(ver protocolVersion) ([]net.UDPAddr, error) { 298 | if err := protoVersionCheck(ver); err != nil { 299 | return nil, err 300 | } 301 | 302 | listen := c.v.Get(fmt.Sprintf("server%d.listen", ver)) 303 | 304 | // Provide an emulation of the old keyword "interface" to avoid breaking config files 305 | if iface := c.v.Get(fmt.Sprintf("server%d.interface", ver)); iface != nil && listen != nil { 306 | return nil, ConfigErrorFromString("interface is a deprecated alias for listen, " + 307 | "both cannot be used at the same time. Choose one and remove the other.") 308 | } else if iface != nil { 309 | listen = "%" + cast.ToString(iface) 310 | } 311 | 312 | if listen == nil { 313 | return defaultListen(ver) 314 | } 315 | 316 | addrs, err := cast.ToStringSliceE(listen) 317 | if err != nil { 318 | addrs = []string{cast.ToString(listen)} 319 | } 320 | 321 | listeners := []net.UDPAddr{} 322 | for _, a := range addrs { 323 | l, err := c.getListenAddress(a, ver) 324 | if err != nil { 325 | return nil, err 326 | } 327 | 328 | if l.Zone == "" && (l.IP.IsLinkLocalMulticast() || l.IP.IsInterfaceLocalMulticast()) { 329 | // link-local multicast specified without interface gets expanded to listen on all interfaces 330 | expanded, err := expandLLMulticast(l) 331 | if err != nil { 332 | return nil, err 333 | } 334 | listeners = append(listeners, expanded...) 335 | continue 336 | } 337 | 338 | listeners = append(listeners, *l) 339 | } 340 | return listeners, nil 341 | } 342 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package config 6 | 7 | import "testing" 8 | 9 | func TestSplitHostPort(t *testing.T) { 10 | testcases := []struct { 11 | hostport string 12 | ip string 13 | zone string 14 | port string 15 | err bool // Should return an error (ie true for err != nil) 16 | }{ 17 | {"0.0.0.0:67", "0.0.0.0", "", "67", false}, 18 | {"192.0.2.0", "192.0.2.0", "", "", false}, 19 | {"192.0.2.9%eth0", "192.0.2.9", "eth0", "", false}, 20 | {"0.0.0.0%eth0:67", "0.0.0.0", "eth0", "67", false}, 21 | {"0.0.0.0:20%eth0:67", "0.0.0.0", "eth0", "67", true}, 22 | {"2001:db8::1:547", "", "", "547", true}, // [] mandatory for v6 23 | {"[::]:547", "::", "", "547", false}, 24 | {"[fe80::1%eth0]", "fe80::1", "eth0", "", false}, 25 | {"[fe80::1]:eth1", "fe80::1", "", "eth1", false}, // no validation of ports in this function 26 | {"fe80::1%eth0:547", "fe80::1", "eth0", "547", true}, // [] mandatory for v6 even with %zone 27 | {"fe80::1%eth0", "fe80::1", "eth0", "547", true}, // [] mandatory for v6 even without port 28 | {"[2001:db8::2]47", "fe80::1", "eth0", "547", true}, // garbage after [] 29 | {"[ff02::1:2]%srv_u:547", "ff02::1:2", "srv_u", "547", true}, // FIXME: Linux `ss` format, we should accept but net.SplitHostPort doesn't 30 | {":http", "", "", "http", false}, 31 | {"%eth0:80", "", "eth0", "80", false}, // janky, but looks valid enough for "[::%eth0]:80" imo 32 | {"%eth0", "", "eth0", "", false}, // janky 33 | {"fe80::1]:80", "fe80::1", "", "80", true}, // unbalanced ] 34 | {"fe80::1%eth0]", "fe80::1", "eth0", "", true}, // unbalanced ], no port 35 | {"", "", "", "", false}, // trivial case, still valid 36 | } 37 | 38 | for _, tc := range testcases { 39 | ip, zone, port, err := splitHostPort(tc.hostport) 40 | if tc.err != (err != nil) { 41 | errState := "not " 42 | if tc.err { 43 | errState = "" 44 | } 45 | t.Errorf("Mismatched error state: %s should %serror (got err: %v)\n", tc.hostport, errState, err) 46 | continue 47 | } 48 | if err == nil && (ip != tc.ip || zone != tc.zone || port != tc.port) { 49 | t.Errorf("%s => \"%s\", \"%s\", \"%s\" expected \"%s\",\"%s\",\"%s\"\n", tc.hostport, ip, zone, port, tc.ip, tc.zone, tc.port) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /config/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package config 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | // ConfigError is an error type returned upon configuration errors. 12 | type ConfigError struct { 13 | err error 14 | } 15 | 16 | // ConfigErrorFromString returns a ConfigError from the given error string. 17 | func ConfigErrorFromString(format string, args ...interface{}) *ConfigError { 18 | return &ConfigError{ 19 | err: fmt.Errorf(format, args...), 20 | } 21 | } 22 | 23 | // ConfigErrorFromError returns a ConfigError from the given error object. 24 | func ConfigErrorFromError(err error) *ConfigError { 25 | return &ConfigError{ 26 | err: err, 27 | } 28 | } 29 | 30 | func (ce ConfigError) Error() string { 31 | return fmt.Sprintf("error parsing config: %v", ce.err) 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | coredhcp: 3 | privileged: true 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - 67:67 9 | - 547:547 10 | volumes: 11 | - ./etc/coredhcp/:/etc/coredhcp/ 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coredhcp/coredhcp 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/bits-and-blooms/bitset v1.22.0 9 | github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb 10 | github.com/fsnotify/fsnotify v1.8.0 11 | github.com/google/gopacket v1.1.19 12 | github.com/insomniacslk/dhcp v0.0.0-20241203100832-a481575ed0ef 13 | github.com/mattn/go-sqlite3 v1.14.24 14 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 15 | github.com/sirupsen/logrus v1.9.3 16 | github.com/spf13/cast v1.7.1 17 | github.com/spf13/pflag v1.0.6 18 | github.com/spf13/viper v1.20.0 19 | github.com/stretchr/testify v1.10.0 20 | github.com/vishvananda/netns v0.0.5 21 | golang.org/x/net v0.38.0 22 | ) 23 | 24 | require ( 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 26 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 27 | github.com/mattn/go-colorable v0.1.13 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 30 | github.com/onsi/ginkgo v1.14.0 // indirect 31 | github.com/onsi/gomega v1.27.10 // indirect 32 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 33 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 34 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 35 | github.com/sagikazarmark/locafero v0.7.0 // indirect 36 | github.com/sourcegraph/conc v0.3.0 // indirect 37 | github.com/spf13/afero v1.12.0 // indirect 38 | github.com/subosito/gotenv v1.6.0 // indirect 39 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect 40 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect 41 | go.uber.org/multierr v1.11.0 // indirect 42 | golang.org/x/crypto v0.36.0 // indirect 43 | golang.org/x/sys v0.31.0 // indirect 44 | golang.org/x/term v0.30.0 // indirect 45 | golang.org/x/text v0.23.0 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package handler 6 | 7 | import ( 8 | "github.com/insomniacslk/dhcp/dhcpv4" 9 | "github.com/insomniacslk/dhcp/dhcpv6" 10 | ) 11 | 12 | // Handler6 is a function that is called on a given DHCPv6 packet. 13 | // It returns a DHCPv6 packet and a boolean. 14 | // If the boolean is true, this will be the last handler to be called. 15 | // The two input packets are the original request, and a response packet. 16 | // The response packet may or may not be modified by the function, and 17 | // the result will be returned by the handler. 18 | // If the returned boolean is true, the returned packet may be nil or 19 | // invalid, in which case no response will be sent. 20 | type Handler6 func(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) 21 | 22 | // Handler4 behaves like Handler6, but for DHCPv4 packets. 23 | type Handler4 func(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) 24 | -------------------------------------------------------------------------------- /integ/server6/leases-dhcpv6-test.txt: -------------------------------------------------------------------------------- 1 | de:ad:be:ef:00:00 2001:db8::10:1 2 | -------------------------------------------------------------------------------- /integ/server6/server6.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | //go:build integration 6 | // +build integration 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "log" 13 | "net" 14 | "runtime" 15 | 16 | "github.com/insomniacslk/dhcp/dhcpv6" 17 | "github.com/insomniacslk/dhcp/dhcpv6/client6" 18 | "github.com/insomniacslk/dhcp/iana" 19 | "github.com/vishvananda/netns" 20 | 21 | "github.com/coredhcp/coredhcp/config" 22 | "github.com/coredhcp/coredhcp/plugins" 23 | "github.com/coredhcp/coredhcp/server" 24 | 25 | // Plugins 26 | "github.com/coredhcp/coredhcp/plugins/file" 27 | "github.com/coredhcp/coredhcp/plugins/serverid" 28 | ) 29 | 30 | var serverConfig = config.Config{ 31 | Server6: &config.ServerConfig{ 32 | Addresses: []net.UDPAddr{ 33 | { 34 | IP: net.ParseIP("ff02::1:2"), 35 | Port: dhcpv6.DefaultServerPort, 36 | Zone: "cdhcp_srv", 37 | }, 38 | }, 39 | Plugins: []config.PluginConfig{ 40 | {Name: "server_id", Args: []string{"LL", "11:22:33:44:55:66"}}, 41 | {Name: "file", Args: []string{"./leases-dhcpv6-test.txt"}}, 42 | }, 43 | }, 44 | } 45 | 46 | // This function *must* be run in its own routine 47 | // For now this assumes ns are created outside. 48 | // TODO: dynamically create NS and interfaces directly in the test program 49 | func runServer(readyCh chan<- struct{}, nsName string, desiredPlugins []*plugins.Plugin) { 50 | runtime.LockOSThread() 51 | defer runtime.UnlockOSThread() 52 | ns, err := netns.GetFromName(nsName) 53 | if err != nil { 54 | log.Panicf("Netns `%s` not set up: %v", nsName, err) 55 | } 56 | if err := netns.Set(ns); err != nil { 57 | log.Panicf("Failed to switch to netns `%s`: %v", nsName, err) 58 | } 59 | // register plugins 60 | for _, pl := range desiredPlugins { 61 | if err := plugins.RegisterPlugin(pl); err != nil { 62 | log.Panicf("Failed to register plugin `%s`: %v", pl.Name, err) 63 | } 64 | } 65 | // start DHCP server 66 | srv, err := server.Start(&serverConfig) 67 | if err != nil { 68 | log.Panicf("Server could not start: %v", err) 69 | } 70 | readyCh <- struct{}{} 71 | if err := srv.Wait(); err != nil { 72 | log.Panicf("Server errored during run: %v", err) 73 | } 74 | } 75 | 76 | // runInNs will execute the provided cmd in the namespace nsName. 77 | // It returns the error status of the cmd. Errors in NS management will panic 78 | func runClient6(nsName, iface string, modifiers ...dhcpv6.Modifier) error { 79 | runtime.LockOSThread() 80 | defer runtime.UnlockOSThread() 81 | backupNS, err := netns.Get() 82 | if err != nil { 83 | panic("Could not save handle to original NS") 84 | } 85 | 86 | ns, err := netns.GetFromName(nsName) 87 | if err != nil { 88 | panic("netns not set up") 89 | } 90 | if err := netns.Set(ns); err != nil { 91 | panic(fmt.Sprintf("Couldn't switch to test NS: %v", err)) 92 | } 93 | 94 | client := client6.NewClient() 95 | _, cErr := client.Exchange(iface, modifiers...) 96 | 97 | if netns.Set(backupNS) != nil { 98 | panic("couldn't switch back to original NS") 99 | } 100 | 101 | return cErr 102 | } 103 | 104 | // Create a server and run a DORA exchange with it 105 | func main() { 106 | readyCh := make(chan struct{}, 1) 107 | go runServer(readyCh, 108 | "coredhcp-direct-upper", 109 | []*plugins.Plugin{ 110 | &serverid.Plugin, &file.Plugin, 111 | }, 112 | ) 113 | // wait for server to be ready before sending DHCP request 114 | <-readyCh 115 | mac, err := net.ParseMAC("de:ad:be:ef:00:00") 116 | if err != nil { 117 | panic(err) 118 | } 119 | err = runClient6( 120 | "coredhcp-direct-lower", "cdhcp_cli", 121 | dhcpv6.WithClientID(&dhcpv6.DUIDLL{ 122 | HWType: iana.HWTypeEthernet, 123 | LinkLayerAddr: mac, 124 | }), 125 | ) 126 | if err != nil { 127 | panic(err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package logger 6 | 7 | import ( 8 | "io" 9 | "sync" 10 | 11 | log_prefixed "github.com/chappjc/logrus-prefix" 12 | "github.com/rifflock/lfshook" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var ( 17 | globalLogger *logrus.Logger 18 | getLoggerMutex sync.Mutex 19 | ) 20 | 21 | // GetLogger returns a configured logger instance 22 | func GetLogger(prefix string) *logrus.Entry { 23 | if prefix == "" { 24 | prefix = "" 25 | } 26 | if globalLogger == nil { 27 | getLoggerMutex.Lock() 28 | defer getLoggerMutex.Unlock() 29 | logger := logrus.New() 30 | logger.SetFormatter(&log_prefixed.TextFormatter{ 31 | FullTimestamp: true, 32 | }) 33 | globalLogger = logger 34 | } 35 | return globalLogger.WithField("prefix", prefix) 36 | } 37 | 38 | // WithFile logs to the specified file in addition to the existing output. 39 | func WithFile(log *logrus.Entry, logfile string) { 40 | log.Logger.AddHook(lfshook.NewHook(logfile, &logrus.TextFormatter{})) 41 | } 42 | 43 | // WithNoStdOutErr disables logging to stdout/stderr. 44 | func WithNoStdOutErr(log *logrus.Entry) { 45 | log.Logger.SetOutput(io.Discard) 46 | } 47 | -------------------------------------------------------------------------------- /plugins/allocators/allocator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // Package allocators provides the interface and the algorithm(s) for allocation of ipv6 6 | // prefixes of various sizes within a larger prefix. 7 | // There are many many parallels with memory allocation. 8 | package allocators 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "net" 14 | ) 15 | 16 | // Allocator is the interface to the address allocator. It only finds and 17 | // allocates blocks and is not concerned with lease-specific questions like 18 | // expiration (ie garbage collection needs to be handled separately) 19 | type Allocator interface { 20 | // Allocate finds a suitable prefix of the given size and returns it. 21 | // 22 | // hint is a prefix, which the client desires especially, and that the 23 | // allocator MAY try to return; the allocator SHOULD try to return a prefix of 24 | // the same size as the given hint prefix. The allocator MUST NOT return an 25 | // error if a prefix was successfully assigned, even if the prefix has nothing 26 | // in common with the hinted prefix 27 | Allocate(hint net.IPNet) (net.IPNet, error) 28 | 29 | // Free returns the prefix containing the given network to the pool 30 | // 31 | // Free may return a DoubleFreeError if the prefix being returned was not 32 | // previously allocated 33 | Free(net.IPNet) error 34 | } 35 | 36 | // ErrDoubleFree is an error type returned by Allocator.Free() when a 37 | // non-allocated block is passed 38 | type ErrDoubleFree struct { 39 | Loc net.IPNet 40 | } 41 | 42 | // String returns a human-readable error message for a DoubleFree error 43 | func (err *ErrDoubleFree) Error() string { 44 | return fmt.Sprint("Attempted to free unallocated block at ", err.Loc.String()) 45 | } 46 | 47 | // ErrNoAddrAvail is returned when we can't allocate an IP because there's no unallocated space left 48 | var ErrNoAddrAvail = errors.New("no address available to allocate") 49 | -------------------------------------------------------------------------------- /plugins/allocators/bitmap/bitmap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // This allocator only returns prefixes of a single size 6 | // This is much simpler to implement (reduces the problem to an equivalent of 7 | // single ip allocations), probably makes sense in cases where the available 8 | // range is much larger than the expected number of clients. Also is what KEA 9 | // does so at least it's not worse than that 10 | 11 | package bitmap 12 | 13 | import ( 14 | "errors" 15 | "fmt" 16 | "net" 17 | "strconv" 18 | "sync" 19 | 20 | "github.com/bits-and-blooms/bitset" 21 | 22 | "github.com/coredhcp/coredhcp/logger" 23 | "github.com/coredhcp/coredhcp/plugins/allocators" 24 | ) 25 | 26 | var log = logger.GetLogger("plugins/allocators/bitmap") 27 | 28 | // Allocator is a prefix allocator allocating in chunks of a fixed size 29 | // regardless of the size requested by the client. 30 | // It consumes an amount of memory proportional to the total amount of available prefixes 31 | type Allocator struct { 32 | containing net.IPNet 33 | page int 34 | bitmap *bitset.BitSet 35 | l sync.Mutex 36 | } 37 | 38 | // prefix must verify: containing.Mask.Size < prefix.Mask.Size < page 39 | func (a *Allocator) toIndex(base net.IP) (uint, error) { 40 | value, err := allocators.Offset(base, a.containing.IP, a.page) 41 | if err != nil { 42 | return 0, fmt.Errorf("Cannot compute prefix index: %w", err) 43 | } 44 | 45 | return uint(value), nil 46 | } 47 | 48 | func (a *Allocator) toPrefix(idx uint) (net.IP, error) { 49 | return allocators.AddPrefixes(a.containing.IP, uint64(idx), uint64(a.page)) 50 | } 51 | 52 | // Allocate reserves a maxsize-sized block and returns a block of size 53 | // min(maxsize, hint.size) 54 | func (a *Allocator) Allocate(hint net.IPNet) (ret net.IPNet, err error) { 55 | 56 | // Ensure size is max(maxsize, hint.size) 57 | reqSize, hintErr := hint.Mask.Size() 58 | if reqSize < a.page || hintErr != 128 { 59 | reqSize = a.page 60 | } 61 | ret.Mask = net.CIDRMask(reqSize, 128) 62 | 63 | // Try to allocate the requested prefix 64 | a.l.Lock() 65 | defer a.l.Unlock() 66 | if hint.IP.To16() != nil && a.containing.Contains(hint.IP) { 67 | idx, hintErr := a.toIndex(hint.IP) 68 | if hintErr == nil && !a.bitmap.Test(idx) { 69 | a.bitmap.Set(idx) 70 | ret.IP, err = a.toPrefix(idx) 71 | return 72 | } 73 | } 74 | 75 | // Find a free prefix 76 | next, ok := a.bitmap.NextClear(0) 77 | if !ok { 78 | err = allocators.ErrNoAddrAvail 79 | return 80 | } 81 | a.bitmap.Set(next) 82 | ret.IP, err = a.toPrefix(next) 83 | if err != nil { 84 | // This violates the assumption that every index in the bitmap maps back to a valid prefix 85 | err = fmt.Errorf("BUG: could not get prefix from allocation: %w", err) 86 | a.bitmap.Clear(next) 87 | } 88 | return 89 | } 90 | 91 | // Free returns the given prefix to the available pool if it was taken. 92 | func (a *Allocator) Free(prefix net.IPNet) error { 93 | idx, err := a.toIndex(prefix.IP.Mask(prefix.Mask)) 94 | if err != nil { 95 | return fmt.Errorf("Could not find prefix in pool: %w", err) 96 | } 97 | 98 | a.l.Lock() 99 | defer a.l.Unlock() 100 | 101 | if !a.bitmap.Test(idx) { 102 | return &allocators.ErrDoubleFree{Loc: prefix} 103 | } 104 | a.bitmap.Clear(idx) 105 | return nil 106 | } 107 | 108 | // NewBitmapAllocator creates a new allocator, allocating /`size` prefixes 109 | // carved out of the given `pool` prefix 110 | func NewBitmapAllocator(pool net.IPNet, size int) (*Allocator, error) { 111 | 112 | poolSize, _ := pool.Mask.Size() 113 | allocOrder := size - poolSize 114 | 115 | if allocOrder < 0 { 116 | return nil, errors.New("The size of allocated prefixes cannot be larger than the pool they're allocated from") 117 | } else if allocOrder >= strconv.IntSize { 118 | return nil, fmt.Errorf("A pool with more than 2^%d items is not representable", size-poolSize) 119 | } else if allocOrder >= 32 { 120 | log.Warningln("Using a pool of more than 2^32 elements may result in large memory consumption") 121 | } 122 | 123 | if !(1< a.end-a.start { 39 | panic("BUG: offset out of bounds") 40 | } 41 | 42 | r := make(net.IP, net.IPv4len) 43 | binary.BigEndian.PutUint32(r, a.start+offset) 44 | return r 45 | } 46 | 47 | func (a *IPv4Allocator) toOffset(ip net.IP) (uint, error) { 48 | if ip.To4() == nil { 49 | return 0, errInvalidIP 50 | } 51 | 52 | intIP := binary.BigEndian.Uint32(ip.To4()) 53 | if intIP < a.start || intIP > a.end { 54 | return 0, errNotInRange 55 | } 56 | 57 | return uint(intIP - a.start), nil 58 | } 59 | 60 | // Allocate reserves an IP for a client 61 | func (a *IPv4Allocator) Allocate(hint net.IPNet) (n net.IPNet, err error) { 62 | n.Mask = net.CIDRMask(32, 32) 63 | 64 | // This is just a hint, ignore any error with it 65 | hintOffset, _ := a.toOffset(hint.IP) 66 | 67 | a.l.Lock() 68 | defer a.l.Unlock() 69 | 70 | var next uint 71 | // First try the exact match 72 | if !a.bitmap.Test(hintOffset) { 73 | next = hintOffset 74 | } else { 75 | // Then any available address 76 | avail, ok := a.bitmap.NextClear(0) 77 | if !ok { 78 | return n, allocators.ErrNoAddrAvail 79 | } 80 | next = avail 81 | } 82 | 83 | a.bitmap.Set(next) 84 | n.IP = a.toIP(uint32(next)) 85 | return 86 | } 87 | 88 | // Free releases the given IP 89 | func (a *IPv4Allocator) Free(n net.IPNet) error { 90 | offset, err := a.toOffset(n.IP) 91 | if err != nil { 92 | return errNotInRange 93 | } 94 | 95 | a.l.Lock() 96 | defer a.l.Unlock() 97 | 98 | if !a.bitmap.Test(uint(offset)) { 99 | return &allocators.ErrDoubleFree{Loc: n} 100 | } 101 | a.bitmap.Clear(offset) 102 | return nil 103 | } 104 | 105 | // NewIPv4Allocator creates a new allocator suitable for giving out IPv4 addresses 106 | func NewIPv4Allocator(start, end net.IP) (*IPv4Allocator, error) { 107 | if start.To4() == nil || end.To4() == nil { 108 | return nil, fmt.Errorf("invalid IPv4 addresses given to create the allocator: [%s,%s]", start, end) 109 | } 110 | 111 | alloc := IPv4Allocator{ 112 | start: binary.BigEndian.Uint32(start.To4()), 113 | end: binary.BigEndian.Uint32(end.To4()), 114 | } 115 | 116 | if alloc.start > alloc.end { 117 | return nil, errors.New("no IPs in the given range to allocate") 118 | } 119 | alloc.bitmap = bitset.New(uint(alloc.end - alloc.start + 1)) 120 | 121 | return &alloc, nil 122 | } 123 | -------------------------------------------------------------------------------- /plugins/allocators/bitmap/bitmap_ipv4_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package bitmap 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | ) 11 | 12 | func getv4Allocator() *IPv4Allocator { 13 | alloc, err := NewIPv4Allocator(net.IPv4(192, 0, 2, 0), net.IPv4(192, 0, 2, 255)) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | return alloc 19 | } 20 | func Test4Alloc(t *testing.T) { 21 | alloc := getv4Allocator() 22 | 23 | net1, err := alloc.Allocate(net.IPNet{}) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | net2, err := alloc.Allocate(net.IPNet{}) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if net1.IP.Equal(net2.IP) { 34 | t.Fatal("That address was already allocated") 35 | } 36 | 37 | err = alloc.Free(net1) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | err = alloc.Free(net1) 43 | if err == nil { 44 | t.Fatal("Expected DoubleFree error") 45 | } 46 | } 47 | 48 | func Test4OutOfPool(t *testing.T) { 49 | alloc := getv4Allocator() 50 | 51 | hint := net.IPv4(198, 51, 100, 5) 52 | res, err := alloc.Allocate(net.IPNet{IP: hint, Mask: net.CIDRMask(32, 32)}) 53 | if err != nil { 54 | t.Fatalf("Failed to allocate with invalid hint: %v", err) 55 | } 56 | _, prefix, _ := net.ParseCIDR("192.0.2.0/24") 57 | if !prefix.Contains(res.IP) { 58 | t.Fatal("Obtained prefix outside of range: ", res) 59 | } 60 | if prefLen, totalLen := res.Mask.Size(); prefLen != 32 || totalLen != 32 { 61 | t.Fatalf("Prefixes have wrong size %d/%d", prefLen, totalLen) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plugins/allocators/bitmap/bitmap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package bitmap 6 | 7 | import ( 8 | "math" 9 | "math/rand" 10 | "net" 11 | "testing" 12 | 13 | "github.com/bits-and-blooms/bitset" 14 | ) 15 | 16 | func getAllocator(bits int) *Allocator { 17 | _, prefix, err := net.ParseCIDR("2001:db8::/56") 18 | if err != nil { 19 | panic(err) 20 | } 21 | alloc, err := NewBitmapAllocator(*prefix, 56+bits) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | return alloc 27 | } 28 | func TestAlloc(t *testing.T) { 29 | alloc := getAllocator(8) 30 | 31 | net, err := alloc.Allocate(net.IPNet{}) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | err = alloc.Free(net) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | err = alloc.Free(net) 42 | if err == nil { 43 | t.Fatal("Expected DoubleFree error") 44 | } 45 | } 46 | 47 | func TestExhaust(t *testing.T) { 48 | _, prefix, _ := net.ParseCIDR("2001:db8::/62") 49 | alloc, _ := NewBitmapAllocator(*prefix, 64) 50 | 51 | allocd := []net.IPNet{} 52 | for i := 0; i < 4; i++ { 53 | net, err := alloc.Allocate(net.IPNet{Mask: net.CIDRMask(64, 128)}) 54 | if err != nil { 55 | t.Fatalf("Error before exhaustion: %v", err) 56 | } 57 | allocd = append(allocd, net) 58 | } 59 | 60 | _, err := alloc.Allocate(net.IPNet{}) 61 | if err == nil { 62 | t.Fatalf("Successfully allocated more prefixes than there are in the pool") 63 | } 64 | 65 | err = alloc.Free(allocd[1]) 66 | if err != nil { 67 | t.Fatalf("Could not free: %v", err) 68 | } 69 | net, err := alloc.Allocate(allocd[1]) 70 | if err != nil { 71 | t.Fatalf("Could not reallocate after free: %v", err) 72 | } 73 | if !net.IP.Equal(allocd[1].IP) || net.Mask.String() != allocd[1].Mask.String() { 74 | t.Fatalf("Did not obtain the right network after free: got %v, expected %v", net, allocd[1]) 75 | } 76 | 77 | } 78 | 79 | func TestOutOfPool(t *testing.T) { 80 | alloc := getAllocator(8) 81 | _, prefix, _ := net.ParseCIDR("fe80:abcd::/48") 82 | 83 | res, err := alloc.Allocate(*prefix) 84 | if err != nil { 85 | t.Fatalf("Failed to allocate with invalid hint: %v", err) 86 | } 87 | if !alloc.containing.Contains(res.IP) { 88 | t.Fatal("Obtained prefix outside of range: ", res) 89 | } 90 | if prefLen, totalLen := res.Mask.Size(); prefLen != 64 || totalLen != 128 { 91 | t.Fatalf("Prefixes have wrong size %d/%d", prefLen, totalLen) 92 | } 93 | } 94 | 95 | func prefixSizeForAllocs(allocs int) int { 96 | return int(math.Ceil(math.Log2(float64(allocs)))) 97 | } 98 | 99 | // Benchmark parallel Allocate, when the bitmap is mostly empty and we're allocating few values 100 | // compared to the available allocations 101 | func BenchmarkParallelAllocInitiallyEmpty(b *testing.B) { 102 | // Run with -race to debug concurrency issues 103 | 104 | alloc := getAllocator(prefixSizeForAllocs(b.N) + 2) // Use max 25% of the bitmap (initially empty) 105 | 106 | b.RunParallel(func(pb *testing.PB) { 107 | for pb.Next() { 108 | if net, err := alloc.Allocate(net.IPNet{}); err != nil { 109 | b.Logf("Could not allocate (got %v and an error): %v", net, err) 110 | b.Fail() 111 | } 112 | } 113 | }) 114 | } 115 | 116 | func BenchmarkParallelAllocPartiallyFilled(b *testing.B) { 117 | // We'll make a bitmap with 2x the number of allocs we want to make. 118 | // Then randomly fill it to about 50% utilization 119 | alloc := getAllocator(prefixSizeForAllocs(b.N) + 1) 120 | 121 | // Build a replacement bitmap that we'll put in the allocator, with approx. 50% of values filled 122 | newbmap := make([]uint64, alloc.bitmap.Len()) 123 | for i := uint(0); i < alloc.bitmap.Len(); i++ { 124 | newbmap[i] = rand.Uint64() 125 | } 126 | alloc.bitmap = bitset.From(newbmap) 127 | 128 | b.ResetTimer() 129 | b.RunParallel(func(pb *testing.PB) { 130 | for pb.Next() { 131 | if net, err := alloc.Allocate(net.IPNet{}); err != nil { 132 | b.Logf("Could not allocate (got %v and an error): %v", net, err) 133 | b.Fail() 134 | } 135 | } 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /plugins/allocators/ipcalc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // Provides functions to add/subtract ipv6 addresses, for use in offset 6 | // calculations in allocators 7 | 8 | package allocators 9 | 10 | import ( 11 | "bytes" 12 | "encoding/binary" 13 | "errors" 14 | "math/bits" 15 | "net" 16 | ) 17 | 18 | // ErrOverflow is returned when arithmetic operations on IPs carry bits 19 | // over/under the 0th or 128th bit respectively 20 | var ErrOverflow = errors.New("Operation overflows") 21 | 22 | // Offset returns the absolute distance between addresses `a` and `b` in units 23 | // of /`prefixLength` subnets. 24 | // Both addresses will have a /`prefixLength` mask applied to them, any 25 | // differences of less than that will be discarded 26 | // If the distance is larger than 2^64 units of /`prefixLength` an error is returned 27 | // 28 | // This function is used in allocators to index bitmaps by an offset from the 29 | // first ip of the range 30 | func Offset(a, b net.IP, prefixLength int) (uint64, error) { 31 | if prefixLength > 128 || prefixLength < 0 { 32 | return 0, errors.New("prefix out of range") 33 | } 34 | 35 | reverse := bytes.Compare(a, b) 36 | if reverse == 0 { 37 | return 0, nil 38 | } else if reverse < 0 { 39 | a, b = b, a 40 | } 41 | 42 | // take an example of [a:b:c:d:e:f:g:h] [1:2:3:4:5:6:7:8] 43 | // Cut the addresses as such: [a:b:c:d|e:f:g:h] [1:2:3:4|5:6:7:8] so we can use 44 | // native integers for computation 45 | ah, bh := binary.BigEndian.Uint64(a[:8]), binary.BigEndian.Uint64(b[:8]) 46 | 47 | if prefixLength <= 64 { 48 | // [(a:b:c):d|e:f:g:h] - [(1:2:3):4|5:6:7:8] 49 | // Only the high bits matter, so the distance always fits within 64 bits. 50 | // We shift to remove anything to the right of the cut 51 | // [(a:b:c):d] => [0:a:b:c] 52 | return (ah - bh) >> (64 - uint(prefixLength)), nil 53 | } 54 | 55 | // General case where both high and low bits matter 56 | al, bl := binary.BigEndian.Uint64(a[8:]), binary.BigEndian.Uint64(b[8:]) 57 | distanceLow, borrow := bits.Sub64(al, bl, 0) 58 | 59 | // This is the distance between the high bits. depending on the prefix unit, we 60 | // will shift this distance left or right 61 | distanceHigh, _ := bits.Sub64(ah, bh, borrow) // [a:b:c:d] - [1:2:3:4] 62 | 63 | // [a:b:c:(d|e:f:g):h] - [1:2:3:(4|5:6:7):8] 64 | // we cut in the low bits (eg. between the parentheses) 65 | // To ensure we stay within 64 bits, we need to ensure [a:b:c:d] - [1:2:3:4] = [0:0:0:d-4] 66 | // so that we don't overflow when adding to the low bits 67 | if distanceHigh >= (1 << (128 - uint(prefixLength))) { 68 | return 0, ErrOverflow 69 | } 70 | 71 | // Schema of the carry and shifts: 72 | // [a:b:c:(d] 73 | // [e:f:g):h] 74 | // <---------------> prefixLen 75 | // <-> 128 - prefixLen (cut right) 76 | // <-----> prefixLen - 64 (cut left) 77 | // 78 | // [a:b:c:(d] => [d:0:0:0] 79 | distanceHigh <<= uint(prefixLength) - 64 80 | // [e:f:g):h] => [0:e:f:g] 81 | distanceLow >>= 128 - uint(prefixLength) 82 | // [d:0:0:0] + [0:e:f:g] = (d:e:f:g) 83 | return distanceHigh + distanceLow, nil 84 | } 85 | 86 | // AddPrefixes returns the `n`th /`unit` subnet after the `ip` base subnet. It 87 | // is the converse operation of Offset(), used to retrieve a prefix from the 88 | // index within the allocator table 89 | func AddPrefixes(ip net.IP, n, unit uint64) (net.IP, error) { 90 | if unit == 0 && n != 0 { 91 | return net.IP{}, ErrOverflow 92 | } else if n == 0 { 93 | return ip, nil 94 | } 95 | if len(ip) != 16 { 96 | // We don't actually care if they're true v6 or v4-mapped, 97 | // but they need to be 128-bit to handle as 64-bit ints 98 | return net.IP{}, errors.New("AddPrefixes needs 128-bit IPs") 99 | } 100 | 101 | // Compute as pairs of uint64 for easier operations 102 | // This could all be 1 function call if go had 128-bit integers 103 | iph, ipl := binary.BigEndian.Uint64(ip[:8]), binary.BigEndian.Uint64(ip[8:]) 104 | 105 | // Compute `n` /`unit` subnets as uint64 pair 106 | var offh, offl uint64 107 | if unit <= 64 { 108 | offh = n << (64 - unit) 109 | } else { 110 | offh, offl = bits.Mul64(n, 1<<(128-unit)) 111 | } 112 | 113 | // Now add the 2, check for overflow 114 | ipl, carry := bits.Add64(offl, ipl, 0) 115 | iph, carry = bits.Add64(offh, iph, carry) 116 | if carry != 0 { 117 | return net.IP{}, ErrOverflow 118 | } 119 | 120 | // Finally convert back to net.IP 121 | ret := make(net.IP, net.IPv6len) 122 | binary.BigEndian.PutUint64(ret[:8], iph) 123 | binary.BigEndian.PutUint64(ret[8:], ipl) 124 | 125 | return ret, nil 126 | } 127 | -------------------------------------------------------------------------------- /plugins/allocators/ipcalc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package allocators 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "testing" 11 | 12 | "math/rand" 13 | ) 14 | 15 | func ExampleOffset() { 16 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 0)) 17 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 16)) 18 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 32)) 19 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 48)) 20 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 64)) 21 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 73)) 22 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 80)) 23 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 96)) 24 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 112)) 25 | fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 128)) 26 | // Output: 27 | // 0 28 | // 0 29 | // 0 30 | // 254 31 | // 16667973 32 | // 8534002176 33 | // 1092352278528 34 | // 71588398925611008 35 | // 0 Operation overflows 36 | // 0 Operation overflows 37 | } 38 | 39 | func ExampleAddPrefixes() { 40 | fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 64)) 41 | fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0x1, 128)) 42 | fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 32)) 43 | fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0x1, 16)) 44 | fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 65)) 45 | // Error cases 46 | fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 8)) 47 | fmt.Println(AddPrefixes(net.IP{10, 0, 0, 1}, 64, 32)) 48 | // Output: 49 | // 2001:db8:0:ff:: 50 | // 2001:db8::1 51 | // 2001:eb7:: 52 | // 2002:db8:: 53 | // 2001:db8:0:7f:8000:: 54 | // Operation overflows 55 | // AddPrefixes needs 128-bit IPs 56 | } 57 | 58 | // Offset is used as a hash function, so it needs to be reasonably fast 59 | func BenchmarkOffset(b *testing.B) { 60 | // Need predictable randomness for benchmark reproducibility 61 | rng := rand.New(rand.NewSource(0)) 62 | addresses := make([]byte, b.N*net.IPv6len*2) 63 | _, err := rng.Read(addresses) 64 | if err != nil { 65 | b.Fatalf("Could not generate random addresses: %v", err) 66 | } 67 | b.ResetTimer() 68 | for i := 0; i < b.N; i++ { 69 | // The arrays will be in cache, so this should amortize to measure mostly just the offset 70 | // computation itself 71 | _, _ = Offset( 72 | addresses[i*2*net.IPv6len:(i*2+1)*net.IPv6len], 73 | addresses[(i*2+1)*net.IPv6len:(i+1)*2*net.IPv6len], 74 | (i*4)%128, 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /plugins/autoconfigure/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package autoconfigure 6 | 7 | // This plugin implements RFC2563: 8 | // 1. If the client has been allocated an IP address, do nothing 9 | // 2. If the client has not been allocated an IP address 10 | // (yiaddr=0.0.0.0), then: 11 | // 2a. If the client has requested the "AutoConfigure" option, 12 | // then add the defined value to the response 13 | // 2b. Otherwise, terminate processing and send no reply 14 | // 15 | // This plugin should be used at the end of the plugin chain, 16 | // after any IP address allocation has taken place. 17 | // 18 | // The optional argument is the string "DoNotAutoConfigure" or 19 | // "AutoConfigure" (or "0" or "1" respectively). The default 20 | // is DoNotAutoConfigure. 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | 26 | "github.com/coredhcp/coredhcp/handler" 27 | "github.com/coredhcp/coredhcp/logger" 28 | "github.com/coredhcp/coredhcp/plugins" 29 | "github.com/insomniacslk/dhcp/dhcpv4" 30 | "github.com/sirupsen/logrus" 31 | ) 32 | 33 | var log = logger.GetLogger("plugins/autoconfigure") 34 | 35 | var autoconfigure dhcpv4.AutoConfiguration 36 | 37 | var Plugin = plugins.Plugin{ 38 | Name: "autoconfigure", 39 | Setup4: setup4, 40 | } 41 | 42 | var argMap = map[string]dhcpv4.AutoConfiguration{ 43 | "0": dhcpv4.AutoConfiguration(0), 44 | "1": dhcpv4.AutoConfiguration(1), 45 | "DoNotAutoConfigure": dhcpv4.DoNotAutoConfigure, 46 | "AutoConfigure": dhcpv4.AutoConfigure, 47 | } 48 | 49 | func setup4(args ...string) (handler.Handler4, error) { 50 | if len(args) > 0 { 51 | var ok bool 52 | autoconfigure, ok = argMap[args[0]] 53 | if !ok { 54 | return nil, fmt.Errorf("unexpected value '%v' for autoconfigure argument", args[0]) 55 | } 56 | } 57 | if len(args) > 1 { 58 | return nil, errors.New("too many arguments") 59 | } 60 | return Handler4, nil 61 | } 62 | 63 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 64 | if resp.MessageType() != dhcpv4.MessageTypeOffer || !resp.YourIPAddr.IsUnspecified() { 65 | return resp, false 66 | } 67 | 68 | ac, ok := req.AutoConfigure() 69 | if ok { 70 | resp.UpdateOption(dhcpv4.OptAutoConfigure(autoconfigure)) 71 | log.WithFields(logrus.Fields{ 72 | "mac": req.ClientHWAddr.String(), 73 | "autoconfigure": fmt.Sprintf("%v", ac), 74 | }).Debugf("Responded with autoconfigure %v", autoconfigure) 75 | return resp, false 76 | } 77 | 78 | log.WithFields(logrus.Fields{ 79 | "mac": req.ClientHWAddr.String(), 80 | "autoconfigure": "nil", 81 | }).Debugf("Client does not support autoconfigure") 82 | // RFC2563 2.3: if no address is chosen for the host [...] 83 | // If the DHCPDISCOVER does not contain the Auto-Configure option, 84 | // it is not answered. 85 | return nil, true 86 | } 87 | -------------------------------------------------------------------------------- /plugins/autoconfigure/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package autoconfigure 6 | 7 | import ( 8 | "bytes" 9 | "net" 10 | "testing" 11 | 12 | "github.com/insomniacslk/dhcp/dhcpv4" 13 | ) 14 | 15 | func TestOptionRequested0(t *testing.T) { 16 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | req.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionAutoConfigure, []byte{1})) 21 | stub, err := dhcpv4.NewReplyFromRequest(req, 22 | dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer), 23 | ) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | resp, stop := Handler4(req, stub) 29 | if resp == nil { 30 | t.Fatal("plugin did not return a message") 31 | } 32 | if stop { 33 | t.Error("plugin interrupted processing") 34 | } 35 | opt := resp.Options.Get(dhcpv4.OptionAutoConfigure) 36 | if opt == nil { 37 | t.Fatal("plugin did not return the Auto-Configure option") 38 | } 39 | if !bytes.Equal(opt, []byte{0}) { 40 | t.Errorf("plugin gave wrong option response: %v", opt) 41 | } 42 | } 43 | 44 | func TestOptionRequested1(t *testing.T) { 45 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | req.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionAutoConfigure, []byte{1})) 50 | stub, err := dhcpv4.NewReplyFromRequest(req, 51 | dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer), 52 | ) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | autoconfigure = 1 58 | resp, stop := Handler4(req, stub) 59 | if resp == nil { 60 | t.Fatal("plugin did not return a message") 61 | } 62 | if stop { 63 | t.Error("plugin interrupted processing") 64 | } 65 | opt := resp.Options.Get(dhcpv4.OptionAutoConfigure) 66 | if opt == nil { 67 | t.Fatal("plugin did not return the Auto-Configure option") 68 | } 69 | if !bytes.Equal(opt, []byte{1}) { 70 | t.Errorf("plugin gave wrong option response: %v", opt) 71 | } 72 | } 73 | 74 | func TestNotRequestedAssignedIP(t *testing.T) { 75 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | stub, err := dhcpv4.NewReplyFromRequest(req, 80 | dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer), 81 | ) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | stub.YourIPAddr = net.ParseIP("192.0.2.100") 86 | 87 | resp, stop := Handler4(req, stub) 88 | if resp == nil { 89 | t.Fatal("plugin did not return a message") 90 | } 91 | if stop { 92 | t.Error("plugin interrupted processing") 93 | } 94 | if resp.Options.Get(dhcpv4.OptionAutoConfigure) != nil { 95 | t.Error("plugin responsed with AutoConfigure option") 96 | } 97 | } 98 | 99 | func TestNotRequestedNoIP(t *testing.T) { 100 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | stub, err := dhcpv4.NewReplyFromRequest(req, 105 | dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer), 106 | ) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | resp, stop := Handler4(req, stub) 112 | if resp != nil { 113 | t.Error("plugin returned a message") 114 | } 115 | if !stop { 116 | t.Error("plugin did not interrupt processing") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /plugins/dns/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package dns 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | 11 | "github.com/coredhcp/coredhcp/handler" 12 | "github.com/coredhcp/coredhcp/logger" 13 | "github.com/coredhcp/coredhcp/plugins" 14 | "github.com/insomniacslk/dhcp/dhcpv4" 15 | "github.com/insomniacslk/dhcp/dhcpv6" 16 | ) 17 | 18 | var log = logger.GetLogger("plugins/dns") 19 | 20 | // Plugin wraps the DNS plugin information. 21 | var Plugin = plugins.Plugin{ 22 | Name: "dns", 23 | Setup6: setup6, 24 | Setup4: setup4, 25 | } 26 | 27 | var ( 28 | dnsServers6 []net.IP 29 | dnsServers4 []net.IP 30 | ) 31 | 32 | func setup6(args ...string) (handler.Handler6, error) { 33 | if len(args) < 1 { 34 | return nil, errors.New("need at least one DNS server") 35 | } 36 | for _, arg := range args { 37 | server := net.ParseIP(arg) 38 | if server.To16() == nil { 39 | return Handler6, errors.New("expected an DNS server address, got: " + arg) 40 | } 41 | dnsServers6 = append(dnsServers6, server) 42 | } 43 | log.Infof("loaded %d DNS servers.", len(dnsServers6)) 44 | return Handler6, nil 45 | } 46 | 47 | func setup4(args ...string) (handler.Handler4, error) { 48 | log.Printf("loaded plugin for DHCPv4.") 49 | if len(args) < 1 { 50 | return nil, errors.New("need at least one DNS server") 51 | } 52 | for _, arg := range args { 53 | DNSServer := net.ParseIP(arg) 54 | if DNSServer.To4() == nil { 55 | return Handler4, errors.New("expected an DNS server address, got: " + arg) 56 | } 57 | dnsServers4 = append(dnsServers4, DNSServer) 58 | } 59 | log.Infof("loaded %d DNS servers.", len(dnsServers4)) 60 | return Handler4, nil 61 | } 62 | 63 | // Handler6 handles DHCPv6 packets for the dns plugin 64 | func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 65 | decap, err := req.GetInnerMessage() 66 | if err != nil { 67 | log.Errorf("Could not decapsulate relayed message, aborting: %v", err) 68 | return nil, true 69 | } 70 | 71 | if decap.IsOptionRequested(dhcpv6.OptionDNSRecursiveNameServer) { 72 | resp.UpdateOption(dhcpv6.OptDNS(dnsServers6...)) 73 | } 74 | return resp, false 75 | } 76 | 77 | //Handler4 handles DHCPv4 packets for the dns plugin 78 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 79 | if req.IsOptionRequested(dhcpv4.OptionDomainNameServer) { 80 | resp.Options.Update(dhcpv4.OptDNS(dnsServers4...)) 81 | } 82 | return resp, false 83 | } 84 | -------------------------------------------------------------------------------- /plugins/dns/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package dns 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/insomniacslk/dhcp/dhcpv4" 12 | "github.com/insomniacslk/dhcp/dhcpv6" 13 | ) 14 | 15 | func TestAddServer6(t *testing.T) { 16 | req, err := dhcpv6.NewMessage() 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | req.MessageType = dhcpv6.MessageTypeRequest 21 | req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionDNSRecursiveNameServer)) 22 | 23 | stub, err := dhcpv6.NewMessage() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | stub.MessageType = dhcpv6.MessageTypeReply 28 | 29 | dnsServers6 = []net.IP{ 30 | net.ParseIP("2001:db8::1"), 31 | net.ParseIP("2001:db8::3"), 32 | } 33 | 34 | resp, stop := Handler6(req, stub) 35 | if resp == nil { 36 | t.Fatal("plugin did not return a message") 37 | } 38 | 39 | if stop { 40 | t.Error("plugin interrupted processing") 41 | } 42 | opts := resp.GetOption(dhcpv6.OptionDNSRecursiveNameServer) 43 | if len(opts) != 1 { 44 | t.Fatalf("Expected 1 RDNSS option, got %d: %v", len(opts), opts) 45 | } 46 | foundServers := resp.(*dhcpv6.Message).Options.DNS() 47 | // XXX: is enforcing the order relevant here ? 48 | for i, srv := range foundServers { 49 | if !srv.Equal(dnsServers6[i]) { 50 | t.Errorf("Found server %s, expected %s", srv, dnsServers6[i]) 51 | } 52 | } 53 | if len(foundServers) != len(dnsServers6) { 54 | t.Errorf("Found %d servers, expected %d", len(foundServers), len(dnsServers6)) 55 | } 56 | } 57 | 58 | func TestNotRequested6(t *testing.T) { 59 | req, err := dhcpv6.NewMessage() 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | req.MessageType = dhcpv6.MessageTypeRequest 64 | req.AddOption(dhcpv6.OptRequestedOption()) 65 | 66 | stub, err := dhcpv6.NewMessage() 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | stub.MessageType = dhcpv6.MessageTypeReply 71 | 72 | dnsServers6 = []net.IP{ 73 | net.ParseIP("2001:db8::1"), 74 | } 75 | 76 | resp, stop := Handler6(req, stub) 77 | if resp == nil { 78 | t.Fatal("plugin did not return a message") 79 | } 80 | if stop { 81 | t.Error("plugin interrupted processing") 82 | } 83 | 84 | opts := resp.GetOption(dhcpv6.OptionDNSRecursiveNameServer) 85 | if len(opts) != 0 { 86 | t.Errorf("RDNSS options were added when not requested: %v", opts) 87 | } 88 | } 89 | 90 | func TestAddServer4(t *testing.T) { 91 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | stub, err := dhcpv4.NewReplyFromRequest(req) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | dnsServers4 = []net.IP{ 101 | net.ParseIP("192.0.2.1"), 102 | net.ParseIP("192.0.2.3"), 103 | } 104 | 105 | resp, stop := Handler4(req, stub) 106 | if resp == nil { 107 | t.Fatal("plugin did not return a message") 108 | } 109 | if stop { 110 | t.Error("plugin interrupted processing") 111 | } 112 | servers := resp.DNS() 113 | for i, srv := range servers { 114 | if !srv.Equal(dnsServers4[i]) { 115 | t.Errorf("Found server %s, expected %s", srv, dnsServers4[i]) 116 | } 117 | } 118 | if len(servers) != len(dnsServers4) { 119 | t.Errorf("Found %d servers, expected %d", len(servers), len(dnsServers4)) 120 | } 121 | } 122 | 123 | func TestNotRequested4(t *testing.T) { 124 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | stub, err := dhcpv4.NewReplyFromRequest(req) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | dnsServers4 = []net.IP{ 134 | net.ParseIP("192.0.2.1"), 135 | } 136 | req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress)) 137 | 138 | resp, stop := Handler4(req, stub) 139 | if resp == nil { 140 | t.Fatal("plugin did not return a message") 141 | } 142 | if stop { 143 | t.Error("plugin interrupted processing") 144 | } 145 | servers := dhcpv4.GetIPs(dhcpv4.OptionDomainNameServer, resp.Options) 146 | if len(servers) != 0 { 147 | t.Errorf("Found %d DNS servers when explicitly not requested", len(servers)) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /plugins/example/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package example 6 | 7 | // This is an example plugin that inspects a packet and prints it out. The code 8 | // is commented in a way that should walk you through the implementation of your 9 | // own plugins. 10 | // Feedback is welcome! 11 | 12 | import ( 13 | "github.com/coredhcp/coredhcp/handler" 14 | "github.com/coredhcp/coredhcp/logger" 15 | "github.com/coredhcp/coredhcp/plugins" 16 | "github.com/insomniacslk/dhcp/dhcpv4" 17 | "github.com/insomniacslk/dhcp/dhcpv6" 18 | ) 19 | 20 | // We use a customizable logger, as part of the `logger` package. You can use 21 | // `logger.GetLogger()` to get a singleton instance of the logger. Then just use 22 | // it with the `logrus` interface (https://github.com/sirupsen/logrus). More 23 | // information in the docstring of the logger package. 24 | var log = logger.GetLogger("plugins/example") 25 | 26 | // Plugin wraps the information necessary to register a plugin. 27 | // In the main package, you need to export a `plugins.Plugin` object called 28 | // `Plugin`, so it can be registered into the plugin registry. 29 | // Just import your plugin, and fill the structure with plugin name and setup 30 | // functions: 31 | // 32 | // import ( 33 | // "github.com/coredhcp/coredhcp/plugins" 34 | // "github.com/coredhcp/coredhcp/plugins/example" 35 | // ) 36 | // 37 | // var Plugin = plugins.Plugin{ 38 | // Name: "example", 39 | // Setup6: setup6, 40 | // Setup4: setup4, 41 | // } 42 | // 43 | // Name is simply the name used to register the plugin. It must be unique to 44 | // other registered plugins, or the operation will fail. In other words, don't 45 | // declare plugins with colliding names. 46 | // 47 | // Setup6 and Setup4 are the setup functions for DHCPv6 and DHCPv4 traffic 48 | // handlers. They conform to the `plugins.SetupFunc6` and `plugins.SetupFunc4` 49 | // interfaces, so they must return a `plugins.Handler6` and a `plugins.Handler4` 50 | // respectively. 51 | // A `nil` setup function means that that protocol won't be handled by this 52 | // plugin. 53 | // 54 | // Note that importing the plugin is not enough to use it: you have to 55 | // explicitly specify the intention to use it in the `config.yml` file, in the 56 | // plugins section. For example: 57 | // 58 | // server6: 59 | // listen: '[::]547' 60 | // - example: 61 | // - server_id: LL aa:bb:cc:dd:ee:ff 62 | // - file: "leases.txt" 63 | // 64 | var Plugin = plugins.Plugin{ 65 | Name: "example", 66 | Setup6: setup6, 67 | Setup4: setup4, 68 | } 69 | 70 | // setup6 is the setup function to initialize the handler for DHCPv6 71 | // traffic. This function implements the `plugin.SetupFunc6` interface. 72 | // This function returns a `handler.Handler6` function, and an error if any. 73 | // In this example we do very little in the setup function, and just return the 74 | // `exampleHandler6` function. Such function will be called for every DHCPv6 75 | // packet that the server receives. Remember that a handler may not be called 76 | // for each packet, if the handler chain is interrupted before reaching it. 77 | func setup6(args ...string) (handler.Handler6, error) { 78 | log.Printf("loaded plugin for DHCPv6.") 79 | return exampleHandler6, nil 80 | } 81 | 82 | // setup4 behaves like setupExample6, but for DHCPv4 packets. It 83 | // implements the `plugin.SetupFunc4` interface. 84 | func setup4(args ...string) (handler.Handler4, error) { 85 | log.Printf("loaded plugin for DHCPv4.") 86 | return exampleHandler4, nil 87 | } 88 | 89 | // exampleHandler6 handles DHCPv6 packets for the example plugin. It implements 90 | // the `handler.Handler6` interface. The input arguments are the request packet 91 | // that the server received from a client, and the response packet that has been 92 | // computed so far. This function returns the response packet to be sent back to 93 | // the client, and a boolean. 94 | // The response can be either the same response packet received as input, a 95 | // modified response packet, or nil. If nil, the server will not reply to the 96 | // client, basically dropping the request. 97 | // The returned boolean indicates to the server whether the chain of plugins 98 | // should continue or not. If `true`, the server will stop at this plugin, and 99 | // respond to the client (or drop the response, if nil). If `false`, the server 100 | // will call the next plugin in the chan, using the returned response packet as 101 | // input for the next plugin. 102 | func exampleHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 103 | log.Printf("received DHCPv6 packet: %s", req.Summary()) 104 | // return the unmodified response, and false. This means that the next 105 | // plugin in the chain will be called, and the unmodified response packet 106 | // will be used as its input. 107 | return resp, false 108 | } 109 | 110 | // exampleHandler4 behaves like exampleHandler6, but for DHCPv4 packets. It 111 | // implements the `handler.Handler4` interface. 112 | func exampleHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 113 | log.Printf("received DHCPv4 packet: %s", req.Summary()) 114 | // return the unmodified response, and false. This means that the next 115 | // plugin in the chain will be called, and the unmodified response packet 116 | // will be used as its input. 117 | return resp, false 118 | } 119 | -------------------------------------------------------------------------------- /plugins/file/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // Package file enables static mapping of MAC <--> IP addresses. 6 | // The mapping is stored in a text file, where each mapping is described by one line containing 7 | // two fields separated by spaces: MAC address, and IP address. For example: 8 | // 9 | // $ cat file_leases.txt 10 | // 00:11:22:33:44:55 10.0.0.1 11 | // 01:23:45:67:89:01 10.0.10.10 12 | // 13 | // To specify the plugin configuration in the server6/server4 sections of the config file, just 14 | // pass the leases file name as plugin argument, e.g.: 15 | // 16 | // $ cat config.yml 17 | // 18 | // server6: 19 | // ... 20 | // plugins: 21 | // - file: "file_leases.txt" [autorefresh] 22 | // ... 23 | // 24 | // If the file path is not absolute, it is relative to the cwd where coredhcp is run. 25 | // 26 | // Optionally, when the 'autorefresh' argument is given, the plugin will try to refresh 27 | // the lease mapping during runtime whenever the lease file is updated. 28 | package file 29 | 30 | import ( 31 | "bytes" 32 | "errors" 33 | "fmt" 34 | "net" 35 | "os" 36 | "strings" 37 | "sync" 38 | "time" 39 | 40 | "github.com/coredhcp/coredhcp/handler" 41 | "github.com/coredhcp/coredhcp/logger" 42 | "github.com/coredhcp/coredhcp/plugins" 43 | "github.com/fsnotify/fsnotify" 44 | "github.com/insomniacslk/dhcp/dhcpv4" 45 | "github.com/insomniacslk/dhcp/dhcpv6" 46 | ) 47 | 48 | const ( 49 | autoRefreshArg = "autorefresh" 50 | ) 51 | 52 | var log = logger.GetLogger("plugins/file") 53 | 54 | // Plugin wraps plugin registration information 55 | var Plugin = plugins.Plugin{ 56 | Name: "file", 57 | Setup6: setup6, 58 | Setup4: setup4, 59 | } 60 | 61 | var recLock sync.RWMutex 62 | 63 | // StaticRecords holds a MAC -> IP address mapping 64 | var StaticRecords map[string]net.IP 65 | 66 | // DHCPv6Records and DHCPv4Records are mappings between MAC addresses in 67 | // form of a string, to network configurations. 68 | var ( 69 | DHCPv6Records map[string]net.IP 70 | DHCPv4Records map[string]net.IP 71 | ) 72 | 73 | // LoadDHCPv4Records loads the DHCPv4Records global map with records stored on 74 | // the specified file. The records have to be one per line, a mac address and an 75 | // IPv4 address. 76 | func LoadDHCPv4Records(filename string) (map[string]net.IP, error) { 77 | log.Infof("reading leases from %s", filename) 78 | data, err := os.ReadFile(filename) 79 | if err != nil { 80 | return nil, err 81 | } 82 | records := make(map[string]net.IP) 83 | for _, lineBytes := range bytes.Split(data, []byte{'\n'}) { 84 | line := string(lineBytes) 85 | if len(line) == 0 { 86 | continue 87 | } 88 | if strings.HasPrefix(line, "#") { 89 | continue 90 | } 91 | tokens := strings.Fields(line) 92 | if len(tokens) != 2 { 93 | return nil, fmt.Errorf("malformed line, want 2 fields, got %d: %s", len(tokens), line) 94 | } 95 | hwaddr, err := net.ParseMAC(tokens[0]) 96 | if err != nil { 97 | return nil, fmt.Errorf("malformed hardware address: %s", tokens[0]) 98 | } 99 | ipaddr := net.ParseIP(tokens[1]) 100 | if ipaddr.To4() == nil { 101 | return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr) 102 | } 103 | records[hwaddr.String()] = ipaddr 104 | } 105 | 106 | return records, nil 107 | } 108 | 109 | // LoadDHCPv6Records loads the DHCPv6Records global map with records stored on 110 | // the specified file. The records have to be one per line, a mac address and an 111 | // IPv6 address. 112 | func LoadDHCPv6Records(filename string) (map[string]net.IP, error) { 113 | log.Infof("reading leases from %s", filename) 114 | data, err := os.ReadFile(filename) 115 | if err != nil { 116 | return nil, err 117 | } 118 | records := make(map[string]net.IP) 119 | for _, lineBytes := range bytes.Split(data, []byte{'\n'}) { 120 | line := string(lineBytes) 121 | if len(line) == 0 { 122 | continue 123 | } 124 | if strings.HasPrefix(line, "#") { 125 | continue 126 | } 127 | tokens := strings.Fields(line) 128 | if len(tokens) != 2 { 129 | return nil, fmt.Errorf("malformed line, want 2 fields, got %d: %s", len(tokens), line) 130 | } 131 | hwaddr, err := net.ParseMAC(tokens[0]) 132 | if err != nil { 133 | return nil, fmt.Errorf("malformed hardware address: %s", tokens[0]) 134 | } 135 | ipaddr := net.ParseIP(tokens[1]) 136 | if ipaddr.To16() == nil || ipaddr.To4() != nil { 137 | return nil, fmt.Errorf("expected an IPv6 address, got: %v", ipaddr) 138 | } 139 | records[hwaddr.String()] = ipaddr 140 | } 141 | return records, nil 142 | } 143 | 144 | // Handler6 handles DHCPv6 packets for the file plugin 145 | func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 146 | m, err := req.GetInnerMessage() 147 | if err != nil { 148 | log.Errorf("BUG: could not decapsulate: %v", err) 149 | return nil, true 150 | } 151 | 152 | if m.Options.OneIANA() == nil { 153 | log.Debug("No address requested") 154 | return resp, false 155 | } 156 | 157 | mac, err := dhcpv6.ExtractMAC(req) 158 | if err != nil { 159 | log.Warningf("Could not find client MAC, passing") 160 | return resp, false 161 | } 162 | log.Debugf("looking up an IP address for MAC %s", mac.String()) 163 | 164 | recLock.RLock() 165 | defer recLock.RUnlock() 166 | 167 | ipaddr, ok := StaticRecords[mac.String()] 168 | if !ok { 169 | log.Warningf("MAC address %s is unknown", mac.String()) 170 | return resp, false 171 | } 172 | log.Debugf("found IP address %s for MAC %s", ipaddr, mac.String()) 173 | 174 | resp.AddOption(&dhcpv6.OptIANA{ 175 | IaId: m.Options.OneIANA().IaId, 176 | Options: dhcpv6.IdentityOptions{Options: []dhcpv6.Option{ 177 | &dhcpv6.OptIAAddress{ 178 | IPv6Addr: ipaddr, 179 | PreferredLifetime: 3600 * time.Second, 180 | ValidLifetime: 3600 * time.Second, 181 | }, 182 | }}, 183 | }) 184 | return resp, false 185 | } 186 | 187 | // Handler4 handles DHCPv4 packets for the file plugin 188 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 189 | recLock.RLock() 190 | defer recLock.RUnlock() 191 | 192 | ipaddr, ok := StaticRecords[req.ClientHWAddr.String()] 193 | if !ok { 194 | log.Warningf("MAC address %s is unknown", req.ClientHWAddr.String()) 195 | return resp, false 196 | } 197 | resp.YourIPAddr = ipaddr 198 | log.Debugf("found IP address %s for MAC %s", ipaddr, req.ClientHWAddr.String()) 199 | return resp, true 200 | } 201 | 202 | func setup6(args ...string) (handler.Handler6, error) { 203 | h6, _, err := setupFile(true, args...) 204 | return h6, err 205 | } 206 | 207 | func setup4(args ...string) (handler.Handler4, error) { 208 | _, h4, err := setupFile(false, args...) 209 | return h4, err 210 | } 211 | 212 | func setupFile(v6 bool, args ...string) (handler.Handler6, handler.Handler4, error) { 213 | var err error 214 | if len(args) < 1 { 215 | return nil, nil, errors.New("need a file name") 216 | } 217 | filename := args[0] 218 | if filename == "" { 219 | return nil, nil, errors.New("got empty file name") 220 | } 221 | 222 | // load initial database from lease file 223 | if err = loadFromFile(v6, filename); err != nil { 224 | return nil, nil, err 225 | } 226 | 227 | // when the 'autorefresh' argument was passed, watch the lease file for 228 | // changes and reload the lease mapping on any event 229 | if len(args) > 1 && args[1] == autoRefreshArg { 230 | // creates a new file watcher 231 | watcher, err := fsnotify.NewWatcher() 232 | if err != nil { 233 | return nil, nil, fmt.Errorf("failed to create watcher: %w", err) 234 | } 235 | 236 | // have file watcher watch over lease file 237 | if err = watcher.Add(filename); err != nil { 238 | return nil, nil, fmt.Errorf("failed to watch %s: %w", filename, err) 239 | } 240 | 241 | // very simple watcher on the lease file to trigger a refresh on any event 242 | // on the file 243 | go func() { 244 | for range watcher.Events { 245 | err := loadFromFile(v6, filename) 246 | if err != nil { 247 | log.Warningf("failed to refresh from %s: %s", filename, err) 248 | 249 | continue 250 | } 251 | 252 | log.Infof("updated to %d leases from %s", len(StaticRecords), filename) 253 | } 254 | }() 255 | } 256 | 257 | log.Infof("loaded %d leases from %s", len(StaticRecords), filename) 258 | return Handler6, Handler4, nil 259 | } 260 | 261 | func loadFromFile(v6 bool, filename string) error { 262 | var err error 263 | var records map[string]net.IP 264 | var protver int 265 | if v6 { 266 | protver = 6 267 | records, err = LoadDHCPv6Records(filename) 268 | } else { 269 | protver = 4 270 | records, err = LoadDHCPv4Records(filename) 271 | } 272 | if err != nil { 273 | return fmt.Errorf("failed to load DHCPv%d records: %w", protver, err) 274 | } 275 | 276 | recLock.Lock() 277 | defer recLock.Unlock() 278 | 279 | StaticRecords = records 280 | 281 | return nil 282 | } 283 | -------------------------------------------------------------------------------- /plugins/file/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package file 6 | 7 | import ( 8 | "net" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/insomniacslk/dhcp/dhcpv4" 14 | "github.com/insomniacslk/dhcp/dhcpv6" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestLoadDHCPv4Records(t *testing.T) { 20 | t.Run("valid leases", func(t *testing.T) { 21 | // setup temp leases file 22 | tmp, err := os.CreateTemp("", "test_plugin_file") 23 | require.NoError(t, err) 24 | defer func() { 25 | tmp.Close() 26 | os.Remove(tmp.Name()) 27 | }() 28 | 29 | // fill temp file with valid lease lines and some comments 30 | _, err = tmp.WriteString("00:11:22:33:44:55 192.0.2.100\n") 31 | require.NoError(t, err) 32 | _, err = tmp.WriteString("11:22:33:44:55:66 192.0.2.101\n") 33 | require.NoError(t, err) 34 | _, err = tmp.WriteString("# this is a comment\n") 35 | require.NoError(t, err) 36 | 37 | records, err := LoadDHCPv4Records(tmp.Name()) 38 | if !assert.NoError(t, err) { 39 | return 40 | } 41 | 42 | if assert.Equal(t, 2, len(records)) { 43 | if assert.Contains(t, records, "00:11:22:33:44:55") { 44 | assert.Equal(t, net.ParseIP("192.0.2.100"), records["00:11:22:33:44:55"]) 45 | } 46 | if assert.Contains(t, records, "11:22:33:44:55:66") { 47 | assert.Equal(t, net.ParseIP("192.0.2.101"), records["11:22:33:44:55:66"]) 48 | } 49 | } 50 | }) 51 | 52 | t.Run("missing field", func(t *testing.T) { 53 | // setup temp leases file 54 | tmp, err := os.CreateTemp("", "test_plugin_file") 55 | require.NoError(t, err) 56 | defer func() { 57 | tmp.Close() 58 | os.Remove(tmp.Name()) 59 | }() 60 | 61 | // add line with too few fields 62 | _, err = tmp.WriteString("foo\n") 63 | require.NoError(t, err) 64 | _, err = LoadDHCPv4Records(tmp.Name()) 65 | assert.Error(t, err) 66 | }) 67 | 68 | t.Run("invalid MAC", func(t *testing.T) { 69 | // setup temp leases file 70 | tmp, err := os.CreateTemp("", "test_plugin_file") 71 | require.NoError(t, err) 72 | defer func() { 73 | tmp.Close() 74 | os.Remove(tmp.Name()) 75 | }() 76 | 77 | // add line with invalid MAC address to trigger an error 78 | _, err = tmp.WriteString("abcd 192.0.2.102\n") 79 | require.NoError(t, err) 80 | _, err = LoadDHCPv4Records(tmp.Name()) 81 | assert.Error(t, err) 82 | }) 83 | 84 | t.Run("invalid IP address", func(t *testing.T) { 85 | // setup temp leases file 86 | tmp, err := os.CreateTemp("", "test_plugin_file") 87 | require.NoError(t, err) 88 | defer func() { 89 | tmp.Close() 90 | os.Remove(tmp.Name()) 91 | }() 92 | 93 | // add line with invalid MAC address to trigger an error 94 | _, err = tmp.WriteString("22:33:44:55:66:77 bcde\n") 95 | require.NoError(t, err) 96 | _, err = LoadDHCPv4Records(tmp.Name()) 97 | assert.Error(t, err) 98 | }) 99 | 100 | t.Run("lease with IPv6 address", func(t *testing.T) { 101 | // setup temp leases file 102 | tmp, err := os.CreateTemp("", "test_plugin_file") 103 | require.NoError(t, err) 104 | defer func() { 105 | tmp.Close() 106 | os.Remove(tmp.Name()) 107 | }() 108 | 109 | // add line with IPv6 address instead to trigger an error 110 | _, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n") 111 | require.NoError(t, err) 112 | _, err = LoadDHCPv4Records(tmp.Name()) 113 | assert.Error(t, err) 114 | }) 115 | } 116 | 117 | func TestLoadDHCPv6Records(t *testing.T) { 118 | t.Run("valid leases", func(t *testing.T) { 119 | // setup temp leases file 120 | tmp, err := os.CreateTemp("", "test_plugin_file") 121 | require.NoError(t, err) 122 | defer func() { 123 | tmp.Close() 124 | os.Remove(tmp.Name()) 125 | }() 126 | 127 | // fill temp file with valid lease lines and some comments 128 | _, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n") 129 | require.NoError(t, err) 130 | _, err = tmp.WriteString("11:22:33:44:55:66 2001:db8::10:2\n") 131 | require.NoError(t, err) 132 | _, err = tmp.WriteString("# this is a comment\n") 133 | require.NoError(t, err) 134 | 135 | records, err := LoadDHCPv6Records(tmp.Name()) 136 | if !assert.NoError(t, err) { 137 | return 138 | } 139 | 140 | if assert.Equal(t, 2, len(records)) { 141 | if assert.Contains(t, records, "00:11:22:33:44:55") { 142 | assert.Equal(t, net.ParseIP("2001:db8::10:1"), records["00:11:22:33:44:55"]) 143 | } 144 | if assert.Contains(t, records, "11:22:33:44:55:66") { 145 | assert.Equal(t, net.ParseIP("2001:db8::10:2"), records["11:22:33:44:55:66"]) 146 | } 147 | } 148 | }) 149 | 150 | t.Run("missing field", func(t *testing.T) { 151 | // setup temp leases file 152 | tmp, err := os.CreateTemp("", "test_plugin_file") 153 | require.NoError(t, err) 154 | defer func() { 155 | tmp.Close() 156 | os.Remove(tmp.Name()) 157 | }() 158 | 159 | // add line with too few fields 160 | _, err = tmp.WriteString("foo\n") 161 | require.NoError(t, err) 162 | _, err = LoadDHCPv6Records(tmp.Name()) 163 | assert.Error(t, err) 164 | }) 165 | 166 | t.Run("invalid MAC", func(t *testing.T) { 167 | // setup temp leases file 168 | tmp, err := os.CreateTemp("", "test_plugin_file") 169 | require.NoError(t, err) 170 | defer func() { 171 | tmp.Close() 172 | os.Remove(tmp.Name()) 173 | }() 174 | 175 | // add line with invalid MAC address to trigger an error 176 | _, err = tmp.WriteString("abcd 2001:db8::10:3\n") 177 | require.NoError(t, err) 178 | _, err = LoadDHCPv6Records(tmp.Name()) 179 | assert.Error(t, err) 180 | }) 181 | 182 | t.Run("invalid IP address", func(t *testing.T) { 183 | // setup temp leases file 184 | tmp, err := os.CreateTemp("", "test_plugin_file") 185 | require.NoError(t, err) 186 | defer func() { 187 | tmp.Close() 188 | os.Remove(tmp.Name()) 189 | }() 190 | 191 | // add line with invalid MAC address to trigger an error 192 | _, err = tmp.WriteString("22:33:44:55:66:77 bcde\n") 193 | require.NoError(t, err) 194 | _, err = LoadDHCPv6Records(tmp.Name()) 195 | assert.Error(t, err) 196 | }) 197 | 198 | t.Run("lease with IPv4 address", func(t *testing.T) { 199 | // setup temp leases file 200 | tmp, err := os.CreateTemp("", "test_plugin_file") 201 | require.NoError(t, err) 202 | defer func() { 203 | tmp.Close() 204 | os.Remove(tmp.Name()) 205 | }() 206 | 207 | // add line with IPv4 address instead to trigger an error 208 | _, err = tmp.WriteString("00:11:22:33:44:55 192.0.2.100\n") 209 | require.NoError(t, err) 210 | _, err = LoadDHCPv6Records(tmp.Name()) 211 | assert.Error(t, err) 212 | }) 213 | } 214 | 215 | func TestHandler4(t *testing.T) { 216 | t.Run("unknown MAC", func(t *testing.T) { 217 | // prepare DHCPv4 request 218 | mac := "00:11:22:33:44:55" 219 | claddr, _ := net.ParseMAC(mac) 220 | req := &dhcpv4.DHCPv4{ 221 | ClientHWAddr: claddr, 222 | } 223 | resp := &dhcpv4.DHCPv4{} 224 | assert.Nil(t, resp.ClientIPAddr) 225 | 226 | // if we handle this DHCP request, nothing should change since the lease is 227 | // unknown 228 | result, stop := Handler4(req, resp) 229 | assert.Same(t, result, resp) 230 | assert.False(t, stop) 231 | assert.Nil(t, result.YourIPAddr) 232 | }) 233 | 234 | t.Run("known MAC", func(t *testing.T) { 235 | // prepare DHCPv4 request 236 | mac := "00:11:22:33:44:55" 237 | claddr, _ := net.ParseMAC(mac) 238 | req := &dhcpv4.DHCPv4{ 239 | ClientHWAddr: claddr, 240 | } 241 | resp := &dhcpv4.DHCPv4{} 242 | assert.Nil(t, resp.ClientIPAddr) 243 | 244 | // add lease for the MAC in the lease map 245 | clIPAddr := net.ParseIP("192.0.2.100") 246 | StaticRecords = map[string]net.IP{ 247 | mac: clIPAddr, 248 | } 249 | 250 | // if we handle this DHCP request, the YourIPAddr field should be set 251 | // in the result 252 | result, stop := Handler4(req, resp) 253 | assert.Same(t, result, resp) 254 | assert.True(t, stop) 255 | assert.Equal(t, clIPAddr, result.YourIPAddr) 256 | 257 | // cleanup 258 | StaticRecords = make(map[string]net.IP) 259 | }) 260 | } 261 | 262 | func TestHandler6(t *testing.T) { 263 | t.Run("unknown MAC", func(t *testing.T) { 264 | // prepare DHCPv6 request 265 | mac := "11:22:33:44:55:66" 266 | claddr, _ := net.ParseMAC(mac) 267 | req, err := dhcpv6.NewSolicit(claddr) 268 | require.NoError(t, err) 269 | resp, err := dhcpv6.NewAdvertiseFromSolicit(req) 270 | require.NoError(t, err) 271 | assert.Equal(t, 0, len(resp.GetOption(dhcpv6.OptionIANA))) 272 | 273 | // if we handle this DHCP request, nothing should change since the lease is 274 | // unknown 275 | result, stop := Handler6(req, resp) 276 | assert.False(t, stop) 277 | assert.Equal(t, 0, len(result.GetOption(dhcpv6.OptionIANA))) 278 | }) 279 | 280 | t.Run("known MAC", func(t *testing.T) { 281 | // prepare DHCPv6 request 282 | mac := "11:22:33:44:55:66" 283 | claddr, _ := net.ParseMAC(mac) 284 | req, err := dhcpv6.NewSolicit(claddr) 285 | require.NoError(t, err) 286 | resp, err := dhcpv6.NewAdvertiseFromSolicit(req) 287 | require.NoError(t, err) 288 | assert.Equal(t, 0, len(resp.GetOption(dhcpv6.OptionIANA))) 289 | 290 | // add lease for the MAC in the lease map 291 | clIPAddr := net.ParseIP("2001:db8::10:1") 292 | StaticRecords = map[string]net.IP{ 293 | mac: clIPAddr, 294 | } 295 | 296 | // if we handle this DHCP request, there should be a specific IANA option 297 | // set in the resulting response 298 | result, stop := Handler6(req, resp) 299 | assert.False(t, stop) 300 | if assert.Equal(t, 1, len(result.GetOption(dhcpv6.OptionIANA))) { 301 | opt := result.GetOneOption(dhcpv6.OptionIANA) 302 | assert.Contains(t, opt.String(), "IP=2001:db8::10:1") 303 | } 304 | 305 | // cleanup 306 | StaticRecords = make(map[string]net.IP) 307 | }) 308 | } 309 | 310 | func TestSetupFile(t *testing.T) { 311 | // too few arguments 312 | _, _, err := setupFile(false) 313 | assert.Error(t, err) 314 | 315 | // empty file name 316 | _, _, err = setupFile(false, "") 317 | assert.Error(t, err) 318 | 319 | // trigger error in LoadDHCPv*Records 320 | _, _, err = setupFile(false, "/foo/bar") 321 | assert.Error(t, err) 322 | 323 | _, _, err = setupFile(true, "/foo/bar") 324 | assert.Error(t, err) 325 | 326 | // setup temp leases file 327 | tmp, err := os.CreateTemp("", "test_plugin_file") 328 | require.NoError(t, err) 329 | defer func() { 330 | tmp.Close() 331 | os.Remove(tmp.Name()) 332 | }() 333 | 334 | t.Run("typical case", func(t *testing.T) { 335 | _, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n") 336 | require.NoError(t, err) 337 | _, err = tmp.WriteString("11:22:33:44:55:66 2001:db8::10:2\n") 338 | require.NoError(t, err) 339 | 340 | assert.Equal(t, 0, len(StaticRecords)) 341 | 342 | // leases should show up in StaticRecords 343 | _, _, err = setupFile(true, tmp.Name()) 344 | if assert.NoError(t, err) { 345 | assert.Equal(t, 2, len(StaticRecords)) 346 | } 347 | }) 348 | 349 | t.Run("autorefresh enabled", func(t *testing.T) { 350 | _, _, err = setupFile(true, tmp.Name(), autoRefreshArg) 351 | if assert.NoError(t, err) { 352 | assert.Equal(t, 2, len(StaticRecords)) 353 | } 354 | // we add more leases to the file 355 | // this should trigger an event to refresh the leases database 356 | // without calling setupFile again 357 | _, err = tmp.WriteString("22:33:44:55:66:77 2001:db8::10:3\n") 358 | require.NoError(t, err) 359 | // since the event is processed asynchronously, give it a little time 360 | time.Sleep(time.Millisecond * 100) 361 | // an additional record should show up in the database 362 | // but we should respect the locking first 363 | recLock.RLock() 364 | defer recLock.RUnlock() 365 | 366 | assert.Equal(t, 3, len(StaticRecords)) 367 | }) 368 | } 369 | -------------------------------------------------------------------------------- /plugins/ipv6only/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package ipv6only 6 | 7 | // This plugin implements RFC8925: if the client has requested the 8 | // IPv6-Only Preferred option, then add the option response and then 9 | // terminate processing immediately. 10 | // 11 | // This module should be invoked *before* any IP address 12 | // allocation has been done, so that the yiaddr is 0.0.0.0 and 13 | // no pool addresses are consumed for compatible clients. 14 | // 15 | // The optional argument is the V6ONLY_WAIT configuration variable, 16 | // described in RFC8925 section 3.2. 17 | 18 | import ( 19 | "errors" 20 | "time" 21 | 22 | "github.com/coredhcp/coredhcp/handler" 23 | "github.com/coredhcp/coredhcp/logger" 24 | "github.com/coredhcp/coredhcp/plugins" 25 | "github.com/insomniacslk/dhcp/dhcpv4" 26 | "github.com/sirupsen/logrus" 27 | ) 28 | 29 | var log = logger.GetLogger("plugins/ipv6only") 30 | 31 | var v6only_wait time.Duration 32 | 33 | var Plugin = plugins.Plugin{ 34 | Name: "ipv6only", 35 | Setup4: setup4, 36 | } 37 | 38 | func setup4(args ...string) (handler.Handler4, error) { 39 | if len(args) > 0 { 40 | dur, err := time.ParseDuration(args[0]) 41 | if err != nil { 42 | log.Errorf("invalid duration: %v", args[0]) 43 | return nil, errors.New("ipv6only failed to initialize") 44 | } 45 | v6only_wait = dur 46 | } 47 | if len(args) > 1 { 48 | return nil, errors.New("too many arguments") 49 | } 50 | return Handler4, nil 51 | } 52 | 53 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 54 | v6pref := req.IsOptionRequested(dhcpv4.OptionIPv6OnlyPreferred) 55 | log.WithFields(logrus.Fields{ 56 | "mac": req.ClientHWAddr.String(), 57 | "ipv6only": v6pref, 58 | }).Debug("ipv6only status") 59 | if v6pref { 60 | resp.UpdateOption(dhcpv4.OptIPv6OnlyPreferred(v6only_wait)) 61 | return resp, true 62 | } 63 | return resp, false 64 | } 65 | -------------------------------------------------------------------------------- /plugins/ipv6only/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package ipv6only 6 | 7 | import ( 8 | "bytes" 9 | "net" 10 | "testing" 11 | "time" 12 | 13 | "github.com/insomniacslk/dhcp/dhcpv4" 14 | ) 15 | 16 | func TestOptionRequested(t *testing.T) { 17 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress, dhcpv4.OptionIPv6OnlyPreferred)) 22 | stub, err := dhcpv4.NewReplyFromRequest(req) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | v6only_wait = 0x1234 * time.Second 28 | 29 | resp, stop := Handler4(req, stub) 30 | if resp == nil { 31 | t.Fatal("plugin did not return a message") 32 | } 33 | if !stop { 34 | t.Error("plugin did not interrupt processing") 35 | } 36 | opt := resp.Options.Get(dhcpv4.OptionIPv6OnlyPreferred) 37 | if opt == nil { 38 | t.Fatal("plugin did not return the IPv6-Only Preferred option") 39 | } 40 | if !bytes.Equal(opt, []byte{0x00, 0x00, 0x12, 0x34}) { 41 | t.Errorf("plugin gave wrong option response: %v", opt) 42 | } 43 | } 44 | 45 | func TestNotRequested(t *testing.T) { 46 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | stub, err := dhcpv4.NewReplyFromRequest(req) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | resp, stop := Handler4(req, stub) 56 | if resp == nil { 57 | t.Fatal("plugin did not return a message") 58 | } 59 | if stop { 60 | t.Error("plugin interrupted processing") 61 | } 62 | if resp.Options.Get(dhcpv4.OptionIPv6OnlyPreferred) != nil { 63 | t.Error("Found IPv6-Only Preferred option when not requested") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /plugins/leasetime/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package leasetime 6 | 7 | import ( 8 | "errors" 9 | "time" 10 | 11 | "github.com/coredhcp/coredhcp/handler" 12 | "github.com/coredhcp/coredhcp/logger" 13 | "github.com/coredhcp/coredhcp/plugins" 14 | "github.com/insomniacslk/dhcp/dhcpv4" 15 | ) 16 | 17 | // Plugin wraps plugin registration information 18 | var Plugin = plugins.Plugin{ 19 | Name: "lease_time", 20 | // currently not supported for DHCPv6 21 | Setup6: nil, 22 | Setup4: setup4, 23 | } 24 | 25 | var ( 26 | log = logger.GetLogger("plugins/lease_time") 27 | v4LeaseTime time.Duration 28 | ) 29 | 30 | // Handler4 handles DHCPv4 packets for the lease_time plugin. 31 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 32 | if req.OpCode != dhcpv4.OpcodeBootRequest { 33 | return resp, false 34 | } 35 | // Set lease time unless it has already been set 36 | if !resp.Options.Has(dhcpv4.OptionIPAddressLeaseTime) { 37 | resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(v4LeaseTime)) 38 | } 39 | return resp, false 40 | } 41 | 42 | func setup4(args ...string) (handler.Handler4, error) { 43 | log.Print("loading `lease_time` plugin for DHCPv4") 44 | if len(args) < 1 { 45 | log.Error("No default lease time provided") 46 | return nil, errors.New("lease_time failed to initialize") 47 | } 48 | 49 | leaseTime, err := time.ParseDuration(args[0]) 50 | if err != nil { 51 | log.Errorf("invalid duration: %v", args[0]) 52 | return nil, errors.New("lease_time failed to initialize") 53 | } 54 | v4LeaseTime = leaseTime 55 | 56 | return Handler4, nil 57 | } 58 | -------------------------------------------------------------------------------- /plugins/mtu/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package mtu 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "strconv" 11 | 12 | "github.com/insomniacslk/dhcp/dhcpv4" 13 | 14 | "github.com/coredhcp/coredhcp/handler" 15 | "github.com/coredhcp/coredhcp/logger" 16 | "github.com/coredhcp/coredhcp/plugins" 17 | ) 18 | 19 | var log = logger.GetLogger("plugins/mtu") 20 | 21 | // Plugin wraps the MTU plugin information. 22 | var Plugin = plugins.Plugin{ 23 | Name: "mtu", 24 | Setup4: setup4, 25 | // No Setup6 since DHCPv6 does not have MTU-related options 26 | } 27 | 28 | var ( 29 | mtu int 30 | ) 31 | 32 | func setup4(args ...string) (handler.Handler4, error) { 33 | if len(args) != 1 { 34 | return nil, errors.New("need one mtu value") 35 | } 36 | var err error 37 | if mtu, err = strconv.Atoi(args[0]); err != nil { 38 | return nil, fmt.Errorf("invalid mtu: %v", args[0]) 39 | } 40 | log.Infof("loaded mtu %d.", mtu) 41 | return Handler4, nil 42 | } 43 | 44 | // Handler4 handles DHCPv4 packets for the mtu plugin 45 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 46 | if req.IsOptionRequested(dhcpv4.OptionInterfaceMTU) { 47 | resp.Options.Update(dhcpv4.Option{Code: dhcpv4.OptionInterfaceMTU, Value: dhcpv4.Uint16(mtu)}) 48 | } 49 | return resp, false 50 | } 51 | -------------------------------------------------------------------------------- /plugins/mtu/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package mtu 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/insomniacslk/dhcp/dhcpv4" 12 | ) 13 | 14 | func TestAddServer4(t *testing.T) { 15 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, dhcpv4.WithRequestedOptions(dhcpv4.OptionInterfaceMTU)) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | stub, err := dhcpv4.NewReplyFromRequest(req) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | mtu = 1500 25 | 26 | resp, stop := Handler4(req, stub) 27 | if resp == nil { 28 | t.Fatal("plugin did not return a message") 29 | } 30 | if stop { 31 | t.Error("plugin interrupted processing") 32 | } 33 | rMTU, err := dhcpv4.GetUint16(dhcpv4.OptionInterfaceMTU, resp.Options) 34 | if err != nil { 35 | t.Errorf("Failed to retrieve mtu from response") 36 | } 37 | 38 | if mtu != int(rMTU) { 39 | t.Errorf("Found %d mtu, expected %d", rMTU, mtu) 40 | } 41 | } 42 | 43 | func TestNotRequested4(t *testing.T) { 44 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | stub, err := dhcpv4.NewReplyFromRequest(req) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | mtu = 1500 54 | req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress)) 55 | 56 | resp, stop := Handler4(req, stub) 57 | if resp == nil { 58 | t.Fatal("plugin did not return a message") 59 | } 60 | if stop { 61 | t.Error("plugin interrupted processing") 62 | } 63 | if mtu, err := dhcpv4.GetUint16(dhcpv4.OptionInterfaceMTU, resp.Options); err == nil { 64 | t.Errorf("Retrieve mtu %d in response, expected none", mtu) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /plugins/nbp/nbp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // Package nbp implements handling of an NBP (Network Boot Program) using an 6 | // URL, e.g. http://[fe80::abcd:efff:fe12:3456]/my-nbp or tftp://10.0.0.1/my-nbp . 7 | // The NBP information is only added if it is requested by the client. 8 | // 9 | // Note that for DHCPv4, unless the URL is prefixed with a "http", "https" or 10 | // "ftp" scheme, the URL will be split into TFTP server name (option 66) 11 | // and Bootfile name (option 67), so the scheme will be stripped out, and it 12 | // will be treated as a TFTP URL. Anything other than host name and file path 13 | // will be ignored (no port, no query string, etc). 14 | // 15 | // For DHCPv6 OPT_BOOTFILE_URL (option 59) is used, and the value is passed 16 | // unmodified. If the query string is specified and contains a "param" key, 17 | // its value is also passed as OPT_BOOTFILE_PARAM (option 60), so it will be 18 | // duplicated between option 59 and 60. 19 | // 20 | // Example usage: 21 | // 22 | // server6: 23 | // - plugins: 24 | // - nbp: http://[2001:db8:a::1]/nbp 25 | // 26 | // server4: 27 | // - plugins: 28 | // - nbp: tftp://10.0.0.254/nbp 29 | // 30 | package nbp 31 | 32 | import ( 33 | "fmt" 34 | "net/url" 35 | 36 | "github.com/coredhcp/coredhcp/handler" 37 | "github.com/coredhcp/coredhcp/logger" 38 | "github.com/coredhcp/coredhcp/plugins" 39 | "github.com/insomniacslk/dhcp/dhcpv4" 40 | "github.com/insomniacslk/dhcp/dhcpv6" 41 | ) 42 | 43 | var log = logger.GetLogger("plugins/nbp") 44 | 45 | // Plugin wraps plugin registration information 46 | var Plugin = plugins.Plugin{ 47 | Name: "nbp", 48 | Setup6: setup6, 49 | Setup4: setup4, 50 | } 51 | 52 | var ( 53 | opt59, opt60 dhcpv6.Option 54 | opt66, opt67 *dhcpv4.Option 55 | ) 56 | 57 | func parseArgs(args ...string) (*url.URL, error) { 58 | if len(args) != 1 { 59 | return nil, fmt.Errorf("Exactly one argument must be passed to NBP plugin, got %d", len(args)) 60 | } 61 | return url.Parse(args[0]) 62 | } 63 | 64 | func setup6(args ...string) (handler.Handler6, error) { 65 | u, err := parseArgs(args...) 66 | if err != nil { 67 | return nil, err 68 | } 69 | opt59 = dhcpv6.OptBootFileURL(u.String()) 70 | params := u.Query().Get("params") 71 | if params != "" { 72 | opt60 = &dhcpv6.OptionGeneric{ 73 | OptionCode: dhcpv6.OptionBootfileParam, 74 | OptionData: []byte(params), 75 | } 76 | } 77 | log.Printf("loaded NBP plugin for DHCPv6.") 78 | return nbpHandler6, nil 79 | } 80 | 81 | func setup4(args ...string) (handler.Handler4, error) { 82 | u, err := parseArgs(args...) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | var otsn, obfn dhcpv4.Option 88 | switch u.Scheme { 89 | case "http", "https", "ftp": 90 | obfn = dhcpv4.OptBootFileName(u.String()) 91 | default: 92 | otsn = dhcpv4.OptTFTPServerName(u.Host) 93 | obfn = dhcpv4.OptBootFileName(u.Path) 94 | opt66 = &otsn 95 | } 96 | 97 | opt67 = &obfn 98 | log.Printf("loaded NBP plugin for DHCPv4.") 99 | return nbpHandler4, nil 100 | } 101 | 102 | func nbpHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 103 | if opt59 == nil { 104 | // nothing to do 105 | return resp, true 106 | } 107 | decap, err := req.GetInnerMessage() 108 | if err != nil { 109 | log.Errorf("Could not decapsulate request: %v", err) 110 | // drop the request, this is probably a critical error in the packet. 111 | return nil, true 112 | } 113 | for _, code := range decap.Options.RequestedOptions() { 114 | if code == dhcpv6.OptionBootfileURL { 115 | // bootfile URL is requested 116 | resp.AddOption(opt59) 117 | } else if code == dhcpv6.OptionBootfileParam { 118 | // optionally add opt60, bootfile params, if requested 119 | if opt60 != nil { 120 | resp.AddOption(opt60) 121 | } 122 | } 123 | } 124 | log.Debugf("Added NBP %s to request", opt59) 125 | return resp, true 126 | } 127 | 128 | func nbpHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 129 | if opt67 == nil { 130 | // nothing to do 131 | return resp, true 132 | } 133 | if req.IsOptionRequested(dhcpv4.OptionTFTPServerName) && opt66 != nil { 134 | resp.Options.Update(*opt66) 135 | log.Debugf("Added NBP %s / %s to request", opt66, opt67) 136 | } 137 | if req.IsOptionRequested(dhcpv4.OptionBootfileName) { 138 | resp.Options.Update(*opt67) 139 | log.Debugf("Added NBP %s to request", opt67) 140 | } 141 | return resp, true 142 | } 143 | -------------------------------------------------------------------------------- /plugins/netmask/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package netmask 6 | 7 | import ( 8 | "encoding/binary" 9 | "errors" 10 | "net" 11 | 12 | "github.com/coredhcp/coredhcp/handler" 13 | "github.com/coredhcp/coredhcp/logger" 14 | "github.com/coredhcp/coredhcp/plugins" 15 | "github.com/insomniacslk/dhcp/dhcpv4" 16 | ) 17 | 18 | var log = logger.GetLogger("plugins/netmask") 19 | 20 | // Plugin wraps plugin registration information 21 | var Plugin = plugins.Plugin{ 22 | Name: "netmask", 23 | Setup4: setup4, 24 | } 25 | 26 | var ( 27 | netmask net.IPMask 28 | ) 29 | 30 | func setup4(args ...string) (handler.Handler4, error) { 31 | log.Printf("loaded plugin for DHCPv4.") 32 | if len(args) != 1 { 33 | return nil, errors.New("need at least one netmask IP address") 34 | } 35 | netmaskIP := net.ParseIP(args[0]) 36 | if netmaskIP.IsUnspecified() { 37 | return nil, errors.New("netmask is not valid, got: " + args[0]) 38 | } 39 | netmaskIP = netmaskIP.To4() 40 | if netmaskIP == nil { 41 | return nil, errors.New("expected an netmask address, got: " + args[0]) 42 | } 43 | netmask = net.IPv4Mask(netmaskIP[0], netmaskIP[1], netmaskIP[2], netmaskIP[3]) 44 | if !checkValidNetmask(netmask) { 45 | return nil, errors.New("netmask is not valid, got: " + args[0]) 46 | } 47 | log.Printf("loaded client netmask") 48 | return Handler4, nil 49 | } 50 | 51 | //Handler4 handles DHCPv4 packets for the netmask plugin 52 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 53 | resp.Options.Update(dhcpv4.OptSubnetMask(netmask)) 54 | return resp, false 55 | } 56 | 57 | func checkValidNetmask(netmask net.IPMask) bool { 58 | netmaskInt := binary.BigEndian.Uint32(netmask) 59 | x := ^netmaskInt 60 | y := x + 1 61 | return (y & x) == 0 62 | } 63 | -------------------------------------------------------------------------------- /plugins/netmask/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package netmask 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/insomniacslk/dhcp/dhcpv4" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestCheckValidNetmask(t *testing.T) { 16 | assert.True(t, checkValidNetmask(net.IPv4Mask(255, 255, 255, 0))) 17 | assert.True(t, checkValidNetmask(net.IPv4Mask(255, 255, 0, 0))) 18 | assert.True(t, checkValidNetmask(net.IPv4Mask(255, 0, 0, 0))) 19 | assert.True(t, checkValidNetmask(net.IPv4Mask(0, 0, 0, 0))) 20 | 21 | assert.False(t, checkValidNetmask(net.IPv4Mask(0, 255, 255, 255))) 22 | assert.False(t, checkValidNetmask(net.IPv4Mask(0, 0, 255, 255))) 23 | assert.False(t, checkValidNetmask(net.IPv4Mask(0, 0, 0, 255))) 24 | } 25 | 26 | func TestHandler4(t *testing.T) { 27 | // set plugin netmask 28 | netmask = net.IPv4Mask(255, 255, 255, 0) 29 | 30 | // prepare DHCPv4 request 31 | req := &dhcpv4.DHCPv4{} 32 | resp := &dhcpv4.DHCPv4{ 33 | Options: dhcpv4.Options{}, 34 | } 35 | 36 | // if we handle this DHCP request, the netmask should be one of the options 37 | // of the result 38 | result, stop := Handler4(req, resp) 39 | assert.Same(t, result, resp) 40 | assert.False(t, stop) 41 | assert.EqualValues(t, netmask, resp.Options.Get(dhcpv4.OptionSubnetMask)) 42 | } 43 | 44 | func TestSetup4(t *testing.T) { 45 | // valid configuration 46 | _, err := setup4("255.255.255.0") 47 | assert.NoError(t, err) 48 | assert.EqualValues(t, netmask, net.IPv4Mask(255, 255, 255, 0)) 49 | 50 | // no configuration 51 | _, err = setup4() 52 | assert.Error(t, err) 53 | 54 | // unspecified netmask 55 | _, err = setup4("0.0.0.0") 56 | assert.Error(t, err) 57 | 58 | // ipv6 prefix 59 | _, err = setup4("ff02::/64") 60 | assert.Error(t, err) 61 | 62 | // invalid netmask 63 | _, err = setup4("0.0.0.255") 64 | assert.Error(t, err) 65 | } 66 | -------------------------------------------------------------------------------- /plugins/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package plugins 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/coredhcp/coredhcp/config" 11 | "github.com/coredhcp/coredhcp/handler" 12 | "github.com/coredhcp/coredhcp/logger" 13 | ) 14 | 15 | var log = logger.GetLogger("plugins") 16 | 17 | // Plugin represents a plugin object. 18 | // Setup6 and Setup4 are the setup functions for DHCPv6 and DHCPv4 handlers 19 | // respectively. Both setup functions can be nil. 20 | type Plugin struct { 21 | Name string 22 | Setup6 SetupFunc6 23 | Setup4 SetupFunc4 24 | } 25 | 26 | // RegisteredPlugins maps a plugin name to a Plugin instance. 27 | var RegisteredPlugins = make(map[string]*Plugin) 28 | 29 | // SetupFunc6 defines a plugin setup function for DHCPv6 30 | type SetupFunc6 func(args ...string) (handler.Handler6, error) 31 | 32 | // SetupFunc4 defines a plugin setup function for DHCPv6 33 | type SetupFunc4 func(args ...string) (handler.Handler4, error) 34 | 35 | // RegisterPlugin registers a plugin. 36 | func RegisterPlugin(plugin *Plugin) error { 37 | if plugin == nil { 38 | return errors.New("cannot register nil plugin") 39 | } 40 | log.Printf("Registering plugin '%s'", plugin.Name) 41 | if _, ok := RegisteredPlugins[plugin.Name]; ok { 42 | // TODO this highlights that asking the plugins to register themselves 43 | // is not the right approach. Need to register them in the main program. 44 | log.Panicf("Plugin '%s' is already registered", plugin.Name) 45 | } 46 | RegisteredPlugins[plugin.Name] = plugin 47 | return nil 48 | } 49 | 50 | // LoadPlugins reads a Config object and loads the plugins as specified in the 51 | // `plugins` section, in order. For a plugin to be available, it must have been 52 | // previously registered with plugins.RegisterPlugin. This is normally done at 53 | // plugin import time. 54 | // This function returns the list of loaded v6 plugins, the list of loaded v4 55 | // plugins, and an error if any. 56 | func LoadPlugins(conf *config.Config) ([]handler.Handler4, []handler.Handler6, error) { 57 | log.Print("Loading plugins...") 58 | handlers4 := make([]handler.Handler4, 0) 59 | handlers6 := make([]handler.Handler6, 0) 60 | 61 | if conf.Server6 == nil && conf.Server4 == nil { 62 | return nil, nil, errors.New("no configuration found for either DHCPv6 or DHCPv4") 63 | } 64 | 65 | // now load the plugins. We need to call its setup function with 66 | // the arguments extracted above. The setup function is mapped in 67 | // plugins.RegisteredPlugins . 68 | 69 | // Load DHCPv6 plugins. 70 | if conf.Server6 != nil { 71 | for _, pluginConf := range conf.Server6.Plugins { 72 | if plugin, ok := RegisteredPlugins[pluginConf.Name]; ok { 73 | log.Printf("DHCPv6: loading plugin `%s`", pluginConf.Name) 74 | if plugin.Setup6 == nil { 75 | log.Warningf("DHCPv6: plugin `%s` has no setup function for DHCPv6", pluginConf.Name) 76 | continue 77 | } 78 | h6, err := plugin.Setup6(pluginConf.Args...) 79 | if err != nil { 80 | return nil, nil, err 81 | } else if h6 == nil { 82 | return nil, nil, config.ConfigErrorFromString("no DHCPv6 handler for plugin %s", pluginConf.Name) 83 | } 84 | handlers6 = append(handlers6, h6) 85 | } else { 86 | return nil, nil, config.ConfigErrorFromString("DHCPv6: unknown plugin `%s`", pluginConf.Name) 87 | } 88 | } 89 | } 90 | // Load DHCPv4 plugins. Yes, duplicated code, there's not really much that 91 | // can be deduplicated here. 92 | if conf.Server4 != nil { 93 | for _, pluginConf := range conf.Server4.Plugins { 94 | if plugin, ok := RegisteredPlugins[pluginConf.Name]; ok { 95 | log.Printf("DHCPv4: loading plugin `%s`", pluginConf.Name) 96 | if plugin.Setup4 == nil { 97 | log.Warningf("DHCPv4: plugin `%s` has no setup function for DHCPv4", pluginConf.Name) 98 | continue 99 | } 100 | h4, err := plugin.Setup4(pluginConf.Args...) 101 | if err != nil { 102 | return nil, nil, err 103 | } else if h4 == nil { 104 | return nil, nil, config.ConfigErrorFromString("no DHCPv4 handler for plugin %s", pluginConf.Name) 105 | } 106 | handlers4 = append(handlers4, h4) 107 | } else { 108 | return nil, nil, config.ConfigErrorFromString("DHCPv4: unknown plugin `%s`", pluginConf.Name) 109 | } 110 | } 111 | } 112 | 113 | return handlers4, handlers6, nil 114 | } 115 | -------------------------------------------------------------------------------- /plugins/prefix/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // Package prefix implements a plugin offering prefixes to clients requesting them 6 | // This plugin attributes prefixes to clients requesting them with IA_PREFIX requests. 7 | // 8 | // Arguments for the plugin configuration are as follows, in this order: 9 | // - prefix: The base prefix from which assigned prefixes are carved 10 | // - max: maximum size of the prefix delegated to clients. When a client requests a larger prefix 11 | // than this, this is the size of the offered prefix 12 | package prefix 13 | 14 | // FIXME: various settings will be hardcoded (default size, minimum size, lease times) pending a 15 | // better configuration system 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "fmt" 21 | "net" 22 | "strconv" 23 | "sync" 24 | "time" 25 | 26 | "github.com/bits-and-blooms/bitset" 27 | "github.com/insomniacslk/dhcp/dhcpv6" 28 | dhcpIana "github.com/insomniacslk/dhcp/iana" 29 | 30 | "github.com/coredhcp/coredhcp/handler" 31 | "github.com/coredhcp/coredhcp/logger" 32 | "github.com/coredhcp/coredhcp/plugins" 33 | "github.com/coredhcp/coredhcp/plugins/allocators" 34 | "github.com/coredhcp/coredhcp/plugins/allocators/bitmap" 35 | ) 36 | 37 | var log = logger.GetLogger("plugins/prefix") 38 | 39 | // Plugin registers the prefix. Prefix delegation only exists for DHCPv6 40 | var Plugin = plugins.Plugin{ 41 | Name: "prefix", 42 | Setup6: setupPrefix, 43 | } 44 | 45 | const leaseDuration = 3600 * time.Second 46 | 47 | func setupPrefix(args ...string) (handler.Handler6, error) { 48 | // - prefix: 2001:db8::/48 64 49 | if len(args) < 2 { 50 | return nil, errors.New("Need both a subnet and an allocation max size") 51 | } 52 | 53 | _, prefix, err := net.ParseCIDR(args[0]) 54 | if err != nil { 55 | return nil, fmt.Errorf("Invalid pool subnet: %v", err) 56 | } 57 | 58 | allocSize, err := strconv.Atoi(args[1]) 59 | if err != nil || allocSize > 128 || allocSize < 0 { 60 | return nil, fmt.Errorf("Invalid prefix length: %v", err) 61 | } 62 | 63 | // TODO: select allocators based on heuristics or user configuration 64 | alloc, err := bitmap.NewBitmapAllocator(*prefix, allocSize) 65 | if err != nil { 66 | return nil, fmt.Errorf("Could not initialize prefix allocator: %v", err) 67 | } 68 | 69 | return (&Handler{ 70 | Records: make(map[string][]lease), 71 | allocator: alloc, 72 | }).Handle, nil 73 | } 74 | 75 | type lease struct { 76 | Prefix net.IPNet 77 | Expire time.Time 78 | } 79 | 80 | // Handler holds state of allocations for the plugin 81 | type Handler struct { 82 | // Mutex here is the simplest implementation fit for purpose. 83 | // We can revisit for perf when we move lease management to separate plugins 84 | sync.Mutex 85 | // Records has a string'd []byte as key, because []byte can't be a key itself 86 | // Since it's not valid utf-8 we can't use any other string function though 87 | Records map[string][]lease 88 | allocator allocators.Allocator 89 | } 90 | 91 | // samePrefix returns true if both prefixes are defined and equal 92 | // The empty prefix is equal to nothing, not even itself 93 | func samePrefix(a, b *net.IPNet) bool { 94 | if a == nil || b == nil { 95 | return false 96 | } 97 | return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask) 98 | } 99 | 100 | // recordKey computes the key for the Records array from the client ID 101 | func recordKey(d dhcpv6.DUID) string { 102 | return string(d.ToBytes()) 103 | } 104 | 105 | // Handle processes DHCPv6 packets for the prefix plugin for a given allocator/leaseset 106 | func (h *Handler) Handle(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 107 | msg, err := req.GetInnerMessage() 108 | if err != nil { 109 | log.Error(err) 110 | return nil, true 111 | } 112 | 113 | client := msg.Options.ClientID() 114 | if client == nil { 115 | log.Error("Invalid packet received, no clientID") 116 | return nil, true 117 | } 118 | 119 | // Each request IA_PD requires an IA_PD response 120 | for _, iapd := range msg.Options.IAPD() { 121 | if err != nil { 122 | log.Errorf("Malformed IAPD received: %v", err) 123 | resp.AddOption(&dhcpv6.OptStatusCode{StatusCode: dhcpIana.StatusMalformedQuery}) 124 | return resp, true 125 | } 126 | 127 | iapdResp := &dhcpv6.OptIAPD{ 128 | IaId: iapd.IaId, 129 | } 130 | 131 | // First figure out what prefixes the client wants 132 | hints := iapd.Options.Prefixes() 133 | if len(hints) == 0 { 134 | // If there are no IAPrefix hints, this is still a valid IA_PD request (just 135 | // unspecified) and we must attempt to allocate a prefix; so we include an empty hint 136 | // which is equivalent to no hint 137 | hints = []*dhcpv6.OptIAPrefix{{Prefix: &net.IPNet{}}} 138 | } 139 | 140 | // Bitmap to track which requests are already satisfied or not 141 | satisfied := bitset.New(uint(len(hints))) 142 | 143 | // A possible simple optimization here would be to be able to lock single map values 144 | // individually instead of the whole map, since we lock for some amount of time 145 | h.Lock() 146 | knownLeases := h.Records[recordKey(client)] 147 | // Bitmap to track which leases are already given in this exchange 148 | givenOut := bitset.New(uint(len(knownLeases))) 149 | 150 | // This is, for now, a set of heuristics, to reconcile the requests (prefix hints asked 151 | // by the clients) with what's on offer (existing leases for this client, plus new blocks) 152 | 153 | // Try to find leases that exactly match a hint, and extend them to satisfy the request 154 | // This is the safest heuristic, if the lease matches exactly we know we aren't missing 155 | // assigning it to a better candidate request 156 | for hintIdx, h := range hints { 157 | for leaseIdx := range knownLeases { 158 | if samePrefix(h.Prefix, &knownLeases[leaseIdx].Prefix) { 159 | expire := time.Now().Add(leaseDuration) 160 | if knownLeases[leaseIdx].Expire.Before(expire) { 161 | knownLeases[leaseIdx].Expire = expire 162 | } 163 | satisfied.Set(uint(hintIdx)) 164 | givenOut.Set(uint(leaseIdx)) 165 | addPrefix(iapdResp, knownLeases[leaseIdx]) 166 | } 167 | } 168 | } 169 | 170 | // Then handle the empty hints, by giving out any remaining lease we 171 | // have already assigned to this client 172 | for hintIdx, h := range hints { 173 | if satisfied.Test(uint(hintIdx)) || 174 | (h.Prefix != nil && !h.Prefix.IP.Equal(net.IPv6zero)) { 175 | continue 176 | } 177 | for leaseIdx, l := range knownLeases { 178 | if givenOut.Test(uint(leaseIdx)) { 179 | continue 180 | } 181 | 182 | // If a length was requested, only give out prefixes of that length 183 | // This is a bad heuristic depending on the allocator behavior, to be improved 184 | if hintPrefixLen, _ := h.Prefix.Mask.Size(); hintPrefixLen != 0 { 185 | leasePrefixLen, _ := l.Prefix.Mask.Size() 186 | if hintPrefixLen != leasePrefixLen { 187 | continue 188 | } 189 | } 190 | expire := time.Now().Add(leaseDuration) 191 | if knownLeases[leaseIdx].Expire.Before(expire) { 192 | knownLeases[leaseIdx].Expire = expire 193 | } 194 | satisfied.Set(uint(hintIdx)) 195 | givenOut.Set(uint(leaseIdx)) 196 | addPrefix(iapdResp, knownLeases[leaseIdx]) 197 | } 198 | } 199 | 200 | // Now remains requests with a hint that we can't trivially satisfy, and possibly expired 201 | // leases that haven't been explicitly requested again. 202 | // A possible improvement here would be to try to widen existing leases, to satisfy wider 203 | // requests that contain an existing leases; and to try to break down existing leases into 204 | // smaller allocations, to satisfy requests for a subnet of an existing lease 205 | // We probably don't need such complex behavior (the vast majority of requests will come 206 | // with an empty, or length-only hint) 207 | 208 | // Assign a new lease to satisfy the request 209 | var newLeases []lease 210 | for i, prefix := range hints { 211 | if satisfied.Test(uint(i)) { 212 | continue 213 | } 214 | 215 | if prefix.Prefix == nil { 216 | // XXX: replace usage of dhcp.OptIAPrefix with a better struct in this inner 217 | // function to avoid repeated nullpointer checks 218 | prefix.Prefix = &net.IPNet{} 219 | } 220 | allocated, err := h.allocator.Allocate(*prefix.Prefix) 221 | if err != nil { 222 | log.Debugf("Nothing allocated for hinted prefix %s", prefix) 223 | continue 224 | } 225 | l := lease{ 226 | Expire: time.Now().Add(leaseDuration), 227 | Prefix: allocated, 228 | } 229 | 230 | addPrefix(iapdResp, l) 231 | newLeases = append(knownLeases, l) 232 | log.Debugf("Allocated %s to %s (IAID: %x)", &allocated, client, iapd.IaId) 233 | } 234 | 235 | if newLeases != nil { 236 | h.Records[recordKey(client)] = newLeases 237 | } 238 | h.Unlock() 239 | 240 | if len(iapdResp.Options.Options) == 0 { 241 | log.Debugf("No valid prefix to return for IAID %x", iapd.IaId) 242 | iapdResp.Options.Add(&dhcpv6.OptStatusCode{ 243 | StatusCode: dhcpIana.StatusNoPrefixAvail, 244 | }) 245 | } 246 | 247 | resp.AddOption(iapdResp) 248 | } 249 | 250 | return resp, false 251 | } 252 | 253 | func addPrefix(resp *dhcpv6.OptIAPD, l lease) { 254 | lifetime := time.Until(l.Expire) 255 | 256 | resp.Options.Add(&dhcpv6.OptIAPrefix{ 257 | PreferredLifetime: lifetime, 258 | ValidLifetime: lifetime, 259 | Prefix: dup(&l.Prefix), 260 | }) 261 | } 262 | 263 | func dup(src *net.IPNet) (dst *net.IPNet) { 264 | dst = &net.IPNet{ 265 | IP: make(net.IP, net.IPv6len), 266 | Mask: make(net.IPMask, net.IPv6len), 267 | } 268 | copy(dst.IP, src.IP) 269 | copy(dst.Mask, src.Mask) 270 | return dst 271 | } 272 | -------------------------------------------------------------------------------- /plugins/prefix/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package prefix 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/insomniacslk/dhcp/dhcpv6" 12 | dhcpIana "github.com/insomniacslk/dhcp/iana" 13 | ) 14 | 15 | func TestRoundTrip(t *testing.T) { 16 | reqIAID := [4]uint8{0x12, 0x34, 0x56, 0x78} 17 | 18 | req, err := dhcpv6.NewMessage() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | req.AddOption(dhcpv6.OptClientID(&dhcpv6.DUIDLL{ 23 | HWType: dhcpIana.HWTypeEthernet, 24 | LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, 25 | })) 26 | req.AddOption(&dhcpv6.OptIAPD{ 27 | IaId: reqIAID, 28 | T1: 0, 29 | T2: 0, 30 | }) 31 | 32 | resp, err := dhcpv6.NewAdvertiseFromSolicit(req) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | handler, err := setupPrefix("2001:db8::/48", "64") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | result, final := handler(req, resp) 43 | if final { 44 | t.Log("Handler declared final") 45 | } 46 | t.Logf("%#v", result) 47 | 48 | // Sanity checks on the response 49 | success := result.GetOption(dhcpv6.OptionStatusCode) 50 | var mo dhcpv6.MessageOptions 51 | if len(success) > 1 { 52 | t.Fatal("Got multiple StatusCode options") 53 | } else if len(success) == 0 { // Everything OK 54 | } else if err := mo.FromBytes(success[0].ToBytes()); err != nil || mo.Status().StatusCode != dhcpIana.StatusSuccess { 55 | t.Fatalf("Did not get a (implicit or explicit) success status code: %v", success) 56 | } 57 | 58 | var iapd *dhcpv6.OptIAPD 59 | { 60 | // Check for IA_PD 61 | iapds := result.(*dhcpv6.Message).Options.IAPD() 62 | if len(iapds) != 1 { 63 | t.Fatal("Malformed response, expected exactly 1 IAPD") 64 | } 65 | iapd = iapds[0] 66 | } 67 | if iapd.IaId != reqIAID { 68 | t.Fatalf("IAID doesn't match: request %x, response: %x", iapd.IaId, reqIAID) 69 | } 70 | 71 | // Check the status code 72 | if status := result.(*dhcpv6.Message).Options.Status(); status != nil && status.StatusCode != dhcpIana.StatusSuccess { 73 | t.Fatalf("Did not get a (implicit or explicit) success status code: %v", success) 74 | } 75 | 76 | t.Logf("%#v", iapd) 77 | // Check IAPrefix within IAPD 78 | if len(iapd.Options.Prefixes()) != 1 { 79 | t.Fatalf("Response did not contain exactly one prefix in the IA_PD option (found %s)", 80 | iapd.Options.Prefixes()) 81 | } 82 | } 83 | 84 | func TestDup(t *testing.T) { 85 | _, prefix, err := net.ParseCIDR("2001:db8::/48") 86 | if err != nil { 87 | panic("bad cidr") 88 | } 89 | dupPrefix := dup(prefix) 90 | if !samePrefix(dupPrefix, prefix) { 91 | t.Fatalf("dup doesn't work: got %v expected %v", dupPrefix, prefix) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /plugins/range/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package rangeplugin 6 | 7 | import ( 8 | "database/sql" 9 | "encoding/binary" 10 | "errors" 11 | "fmt" 12 | "net" 13 | "sync" 14 | "time" 15 | 16 | "github.com/coredhcp/coredhcp/handler" 17 | "github.com/coredhcp/coredhcp/logger" 18 | "github.com/coredhcp/coredhcp/plugins" 19 | "github.com/coredhcp/coredhcp/plugins/allocators" 20 | "github.com/coredhcp/coredhcp/plugins/allocators/bitmap" 21 | "github.com/insomniacslk/dhcp/dhcpv4" 22 | ) 23 | 24 | var log = logger.GetLogger("plugins/range") 25 | 26 | // Plugin wraps plugin registration information 27 | var Plugin = plugins.Plugin{ 28 | Name: "range", 29 | Setup4: setupRange, 30 | } 31 | 32 | //Record holds an IP lease record 33 | type Record struct { 34 | IP net.IP 35 | expires int 36 | hostname string 37 | } 38 | 39 | // PluginState is the data held by an instance of the range plugin 40 | type PluginState struct { 41 | // Rough lock for the whole plugin, we'll get better performance once we use leasestorage 42 | sync.Mutex 43 | // Recordsv4 holds a MAC -> IP address and lease time mapping 44 | Recordsv4 map[string]*Record 45 | LeaseTime time.Duration 46 | leasedb *sql.DB 47 | allocator allocators.Allocator 48 | } 49 | 50 | // Handler4 handles DHCPv4 packets for the range plugin 51 | func (p *PluginState) Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 52 | p.Lock() 53 | defer p.Unlock() 54 | record, ok := p.Recordsv4[req.ClientHWAddr.String()] 55 | hostname := req.HostName() 56 | if !ok { 57 | // Allocating new address since there isn't one allocated 58 | log.Printf("MAC address %s is new, leasing new IPv4 address", req.ClientHWAddr.String()) 59 | ip, err := p.allocator.Allocate(net.IPNet{}) 60 | if err != nil { 61 | log.Errorf("Could not allocate IP for MAC %s: %v", req.ClientHWAddr.String(), err) 62 | return nil, true 63 | } 64 | rec := Record{ 65 | IP: ip.IP.To4(), 66 | expires: int(time.Now().Add(p.LeaseTime).Unix()), 67 | hostname: hostname, 68 | } 69 | err = p.saveIPAddress(req.ClientHWAddr, &rec) 70 | if err != nil { 71 | log.Errorf("SaveIPAddress for MAC %s failed: %v", req.ClientHWAddr.String(), err) 72 | } 73 | p.Recordsv4[req.ClientHWAddr.String()] = &rec 74 | record = &rec 75 | } else { 76 | // Ensure we extend the existing lease at least past when the one we're giving expires 77 | expiry := time.Unix(int64(record.expires), 0) 78 | if expiry.Before(time.Now().Add(p.LeaseTime)) { 79 | record.expires = int(time.Now().Add(p.LeaseTime).Round(time.Second).Unix()) 80 | record.hostname = hostname 81 | err := p.saveIPAddress(req.ClientHWAddr, record) 82 | if err != nil { 83 | log.Errorf("Could not persist lease for MAC %s: %v", req.ClientHWAddr.String(), err) 84 | } 85 | } 86 | } 87 | resp.YourIPAddr = record.IP 88 | resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second))) 89 | log.Printf("found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String()) 90 | return resp, false 91 | } 92 | 93 | func setupRange(args ...string) (handler.Handler4, error) { 94 | var ( 95 | err error 96 | p PluginState 97 | ) 98 | 99 | if len(args) < 4 { 100 | return nil, fmt.Errorf("invalid number of arguments, want: 4 (file name, start IP, end IP, lease time), got: %d", len(args)) 101 | } 102 | filename := args[0] 103 | if filename == "" { 104 | return nil, errors.New("file name cannot be empty") 105 | } 106 | ipRangeStart := net.ParseIP(args[1]) 107 | if ipRangeStart.To4() == nil { 108 | return nil, fmt.Errorf("invalid IPv4 address: %v", args[1]) 109 | } 110 | ipRangeEnd := net.ParseIP(args[2]) 111 | if ipRangeEnd.To4() == nil { 112 | return nil, fmt.Errorf("invalid IPv4 address: %v", args[2]) 113 | } 114 | if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) { 115 | return nil, errors.New("start of IP range has to be lower than the end of an IP range") 116 | } 117 | 118 | p.allocator, err = bitmap.NewIPv4Allocator(ipRangeStart, ipRangeEnd) 119 | if err != nil { 120 | return nil, fmt.Errorf("could not create an allocator: %w", err) 121 | } 122 | 123 | p.LeaseTime, err = time.ParseDuration(args[3]) 124 | if err != nil { 125 | return nil, fmt.Errorf("invalid lease duration: %v", args[3]) 126 | } 127 | 128 | if err := p.registerBackingDB(filename); err != nil { 129 | return nil, fmt.Errorf("could not setup lease storage: %w", err) 130 | } 131 | p.Recordsv4, err = loadRecords(p.leasedb) 132 | if err != nil { 133 | return nil, fmt.Errorf("could not load records from file: %v", err) 134 | } 135 | 136 | log.Printf("Loaded %d DHCPv4 leases from %s", len(p.Recordsv4), filename) 137 | 138 | for _, v := range p.Recordsv4 { 139 | ip, err := p.allocator.Allocate(net.IPNet{IP: v.IP}) 140 | if err != nil { 141 | return nil, fmt.Errorf("failed to re-allocate leased ip %v: %v", v.IP.String(), err) 142 | } 143 | if ip.IP.String() != v.IP.String() { 144 | return nil, fmt.Errorf("allocator did not re-allocate requested leased ip %v: %v", v.IP.String(), ip.String()) 145 | } 146 | } 147 | 148 | return p.Handler4, nil 149 | } 150 | -------------------------------------------------------------------------------- /plugins/range/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package rangeplugin 6 | 7 | import ( 8 | "database/sql" 9 | "errors" 10 | "fmt" 11 | "net" 12 | 13 | _ "github.com/mattn/go-sqlite3" 14 | ) 15 | 16 | func loadDB(path string) (*sql.DB, error) { 17 | db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", path)) 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to open database (%T): %w", err, err) 20 | } 21 | if _, err := db.Exec("create table if not exists leases4 (mac string not null, ip string not null, expiry int, hostname string not null, primary key (mac, ip))"); err != nil { 22 | return nil, fmt.Errorf("table creation failed: %w", err) 23 | } 24 | return db, nil 25 | } 26 | 27 | // loadRecords loads the DHCPv6/v4 Records global map with records stored on 28 | // the specified file. The records have to be one per line, a mac address and an 29 | // IP address. 30 | func loadRecords(db *sql.DB) (map[string]*Record, error) { 31 | rows, err := db.Query("select mac, ip, expiry, hostname from leases4") 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to query leases database: %w", err) 34 | } 35 | defer rows.Close() 36 | var ( 37 | mac, ip, hostname string 38 | expiry int 39 | records = make(map[string]*Record) 40 | ) 41 | for rows.Next() { 42 | if err := rows.Scan(&mac, &ip, &expiry, &hostname); err != nil { 43 | return nil, fmt.Errorf("failed to scan row: %w", err) 44 | } 45 | hwaddr, err := net.ParseMAC(mac) 46 | if err != nil { 47 | return nil, fmt.Errorf("malformed hardware address: %s", mac) 48 | } 49 | ipaddr := net.ParseIP(ip) 50 | if ipaddr.To4() == nil { 51 | return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr) 52 | } 53 | records[hwaddr.String()] = &Record{IP: ipaddr, expires: expiry, hostname: hostname} 54 | } 55 | if err := rows.Err(); err != nil { 56 | return nil, fmt.Errorf("failed lease database row scanning: %w", err) 57 | } 58 | return records, nil 59 | } 60 | 61 | // saveIPAddress writes out a lease to storage 62 | func (p *PluginState) saveIPAddress(mac net.HardwareAddr, record *Record) error { 63 | stmt, err := p.leasedb.Prepare(`insert or replace into leases4(mac, ip, expiry, hostname) values (?, ?, ?, ?)`) 64 | if err != nil { 65 | return fmt.Errorf("statement preparation failed: %w", err) 66 | } 67 | defer stmt.Close() 68 | if _, err := stmt.Exec( 69 | mac.String(), 70 | record.IP.String(), 71 | record.expires, 72 | record.hostname, 73 | ); err != nil { 74 | return fmt.Errorf("record insert/update failed: %w", err) 75 | } 76 | return nil 77 | } 78 | 79 | // registerBackingDB installs a database connection string as the backing store for leases 80 | func (p *PluginState) registerBackingDB(filename string) error { 81 | if p.leasedb != nil { 82 | return errors.New("cannot swap out a lease database while running") 83 | } 84 | // We never close this, but that's ok because plugins are never stopped/unregistered 85 | newLeaseDB, err := loadDB(filename) 86 | if err != nil { 87 | return fmt.Errorf("failed to open lease database %s: %w", filename, err) 88 | } 89 | p.leasedb = newLeaseDB 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /plugins/range/storage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package rangeplugin 6 | 7 | import ( 8 | "database/sql" 9 | "fmt" 10 | "net" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func testDBSetup() (*sql.DB, error) { 18 | db, err := loadDB(":memory:") 19 | if err != nil { 20 | return nil, err 21 | } 22 | for _, record := range records { 23 | stmt, err := db.Prepare("insert into leases4(mac, ip, expiry, hostname) values (?, ?, ?, ?)") 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to prepare insert statement: %w", err) 26 | } 27 | defer stmt.Close() 28 | if _, err := stmt.Exec(record.mac, record.ip.IP.String(), record.ip.expires, record.ip.hostname); err != nil { 29 | return nil, fmt.Errorf("failed to insert record into test db: %w", err) 30 | } 31 | } 32 | return db, nil 33 | } 34 | 35 | var expire = int(time.Date(2000, 01, 01, 00, 00, 00, 00, time.UTC).Unix()) 36 | var records = []struct { 37 | mac string 38 | ip *Record 39 | }{ 40 | {"02:00:00:00:00:00", &Record{IP: net.IPv4(10, 0, 0, 0), expires: expire, hostname: "zero"}}, 41 | {"02:00:00:00:00:01", &Record{IP: net.IPv4(10, 0, 0, 1), expires: expire, hostname: "one"}}, 42 | {"02:00:00:00:00:02", &Record{IP: net.IPv4(10, 0, 0, 2), expires: expire, hostname: "two"}}, 43 | {"02:00:00:00:00:03", &Record{IP: net.IPv4(10, 0, 0, 3), expires: expire, hostname: "three"}}, 44 | {"02:00:00:00:00:04", &Record{IP: net.IPv4(10, 0, 0, 4), expires: expire, hostname: "four"}}, 45 | {"02:00:00:00:00:05", &Record{IP: net.IPv4(10, 0, 0, 5), expires: expire, hostname: "five"}}, 46 | } 47 | 48 | func TestLoadRecords(t *testing.T) { 49 | db, err := testDBSetup() 50 | if err != nil { 51 | t.Fatalf("Failed to set up test DB: %v", err) 52 | } 53 | 54 | parsedRec, err := loadRecords(db) 55 | if err != nil { 56 | t.Fatalf("Failed to load records from file: %v", err) 57 | } 58 | 59 | mapRec := make(map[string]*Record) 60 | for _, rec := range records { 61 | var ( 62 | ip, mac, hostname string 63 | expiry int 64 | ) 65 | if err := db.QueryRow("select mac, ip, expiry, hostname from leases4 where mac = ?", rec.mac).Scan(&mac, &ip, &expiry, &hostname); err != nil { 66 | t.Fatalf("record not found for mac=%s: %v", rec.mac, err) 67 | } 68 | mapRec[mac] = &Record{IP: net.ParseIP(ip), expires: expiry, hostname: hostname} 69 | } 70 | 71 | assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB") 72 | } 73 | 74 | func TestWriteRecords(t *testing.T) { 75 | pl := PluginState{} 76 | if err := pl.registerBackingDB(":memory:"); err != nil { 77 | t.Fatalf("Could not setup file") 78 | } 79 | 80 | mapRec := make(map[string]*Record) 81 | for _, rec := range records { 82 | hwaddr, err := net.ParseMAC(rec.mac) 83 | if err != nil { 84 | // bug in testdata 85 | panic(err) 86 | } 87 | if err := pl.saveIPAddress(hwaddr, rec.ip); err != nil { 88 | t.Errorf("Failed to save ip for %s: %v", hwaddr, err) 89 | } 90 | mapRec[hwaddr.String()] = &Record{IP: rec.ip.IP, expires: rec.ip.expires, hostname: rec.ip.hostname} 91 | } 92 | 93 | parsedRec, err := loadRecords(pl.leasedb) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB") 99 | } 100 | -------------------------------------------------------------------------------- /plugins/router/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package router 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | 11 | "github.com/coredhcp/coredhcp/handler" 12 | "github.com/coredhcp/coredhcp/logger" 13 | "github.com/coredhcp/coredhcp/plugins" 14 | "github.com/insomniacslk/dhcp/dhcpv4" 15 | ) 16 | 17 | var log = logger.GetLogger("plugins/router") 18 | 19 | // Plugin wraps plugin registration information 20 | var Plugin = plugins.Plugin{ 21 | Name: "router", 22 | Setup4: setup4, 23 | } 24 | 25 | var ( 26 | routers []net.IP 27 | ) 28 | 29 | func setup4(args ...string) (handler.Handler4, error) { 30 | log.Printf("Loaded plugin for DHCPv4.") 31 | if len(args) < 1 { 32 | return nil, errors.New("need at least one router IP address") 33 | } 34 | for _, arg := range args { 35 | router := net.ParseIP(arg) 36 | if router.To4() == nil { 37 | return Handler4, errors.New("expected an router IP address, got: " + arg) 38 | } 39 | routers = append(routers, router) 40 | } 41 | log.Infof("loaded %d router IP addresses.", len(routers)) 42 | return Handler4, nil 43 | } 44 | 45 | //Handler4 handles DHCPv4 packets for the router plugin 46 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 47 | resp.Options.Update(dhcpv4.OptRouter(routers...)) 48 | return resp, false 49 | } 50 | -------------------------------------------------------------------------------- /plugins/searchdomains/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package searchdomains 6 | 7 | // This is an searchdomains plugin that adds default DNS search domains. 8 | 9 | import ( 10 | "github.com/coredhcp/coredhcp/handler" 11 | "github.com/coredhcp/coredhcp/logger" 12 | "github.com/coredhcp/coredhcp/plugins" 13 | "github.com/insomniacslk/dhcp/dhcpv4" 14 | "github.com/insomniacslk/dhcp/dhcpv6" 15 | "github.com/insomniacslk/dhcp/rfc1035label" 16 | ) 17 | 18 | var log = logger.GetLogger("plugins/searchdomains") 19 | 20 | // Plugin wraps the default DNS search domain options. 21 | // Note that importing the plugin is not enough to use it: you have to 22 | // explicitly specify the intention to use it in the `config.yml` file, in the 23 | // plugins section. For searchdomains: 24 | // 25 | // server6: 26 | // listen: '[::]547' 27 | // - searchdomains: domain.a domain.b 28 | // - server_id: LL aa:bb:cc:dd:ee:ff 29 | // - file: "leases.txt" 30 | // 31 | var Plugin = plugins.Plugin{ 32 | Name: "searchdomains", 33 | Setup6: setup6, 34 | Setup4: setup4, 35 | } 36 | 37 | // These are the DNS search domains that are set by the plugin. 38 | // Note that DHCPv4 and DHCPv6 options are totally independent. 39 | // If you need the same settings for both, you'll need to configure 40 | // this plugin once for the v4 and once for the v6 server. 41 | var v4SearchList []string 42 | var v6SearchList []string 43 | 44 | // copySlice creates a new copy of a string slice in memory. 45 | // This helps to ensure that downstream plugins can't corrupt 46 | // this plugin's configuration 47 | func copySlice(original []string) []string { 48 | copied := make([]string, len(original)) 49 | copy(copied, original) 50 | return copied 51 | } 52 | 53 | func setup6(args ...string) (handler.Handler6, error) { 54 | v6SearchList = args 55 | log.Printf("Registered domain search list (DHCPv6) %s", v6SearchList) 56 | return domainSearchListHandler6, nil 57 | } 58 | 59 | func setup4(args ...string) (handler.Handler4, error) { 60 | v4SearchList = args 61 | log.Printf("Registered domain search list (DHCPv4) %s", v4SearchList) 62 | return domainSearchListHandler4, nil 63 | } 64 | 65 | func domainSearchListHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 66 | resp.UpdateOption(dhcpv6.OptDomainSearchList(&rfc1035label.Labels{ 67 | Labels: copySlice(v6SearchList), 68 | })) 69 | return resp, false 70 | } 71 | 72 | func domainSearchListHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 73 | resp.UpdateOption(dhcpv4.OptDomainSearch(&rfc1035label.Labels{ 74 | Labels: copySlice(v4SearchList), 75 | })) 76 | return resp, false 77 | } 78 | -------------------------------------------------------------------------------- /plugins/searchdomains/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package searchdomains 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/insomniacslk/dhcp/dhcpv4" 12 | "github.com/insomniacslk/dhcp/dhcpv6" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestAddDomains6(t *testing.T) { 18 | assert := assert.New(t) 19 | 20 | // Search domains we will expect the DHCP server to assign 21 | searchDomains := []string{"domain.a", "domain.b"} 22 | 23 | // Init plugin 24 | handler6, err := Plugin.Setup6(searchDomains...) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | // Fake request 30 | req, err := dhcpv6.NewMessage() 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | req.MessageType = dhcpv6.MessageTypeRequest 35 | req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionDNSRecursiveNameServer)) 36 | 37 | // Fake response input 38 | stub, err := dhcpv6.NewMessage() 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | stub.MessageType = dhcpv6.MessageTypeReply 43 | 44 | // Call plugin 45 | resp, stop := handler6(req, stub) 46 | if resp == nil { 47 | t.Fatal("plugin did not return a message") 48 | } 49 | if stop { 50 | t.Error("plugin interrupted processing") 51 | } 52 | 53 | searchLabels := resp.(*dhcpv6.Message).Options.DomainSearchList().Labels 54 | assert.Equal(searchDomains, searchLabels) 55 | } 56 | 57 | func TestAddDomains4(t *testing.T) { 58 | assert := assert.New(t) 59 | 60 | // Search domains we will expect the DHCP server to assign 61 | // NOTE: these domains should be different from the v6 test domains; 62 | // this tests that we haven't accidentally set the v6 domains in the 63 | // v4 plugin handler or vice versa. 64 | searchDomains := []string{"domain.b", "domain.c"} 65 | 66 | // Init plugin 67 | handler4, err := Plugin.Setup4(searchDomains...) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | // Fake request 73 | req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | // Fake response input 79 | stub, err := dhcpv4.NewReplyFromRequest(req) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | // Call plugin 85 | resp, stop := handler4(req, stub) 86 | if resp == nil { 87 | t.Fatal("plugin did not return a message") 88 | } 89 | if stop { 90 | t.Error("plugin interrupted processing") 91 | } 92 | 93 | searchLabels := resp.DomainSearch().Labels 94 | assert.Equal(searchDomains, searchLabels) 95 | 96 | } 97 | -------------------------------------------------------------------------------- /plugins/serverid/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package serverid 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | "strings" 11 | 12 | "github.com/coredhcp/coredhcp/handler" 13 | "github.com/coredhcp/coredhcp/logger" 14 | "github.com/coredhcp/coredhcp/plugins" 15 | "github.com/insomniacslk/dhcp/dhcpv4" 16 | "github.com/insomniacslk/dhcp/dhcpv6" 17 | "github.com/insomniacslk/dhcp/iana" 18 | ) 19 | 20 | var log = logger.GetLogger("plugins/server_id") 21 | 22 | // Plugin wraps plugin registration information 23 | var Plugin = plugins.Plugin{ 24 | Name: "server_id", 25 | Setup6: setup6, 26 | Setup4: setup4, 27 | } 28 | 29 | // v6ServerID is the DUID of the v6 server 30 | var ( 31 | v6ServerID dhcpv6.DUID 32 | v4ServerID net.IP 33 | ) 34 | 35 | // Handler6 handles DHCPv6 packets for the server_id plugin. 36 | func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 37 | if v6ServerID == nil { 38 | log.Fatal("BUG: Plugin is running uninitialized!") 39 | return nil, true 40 | } 41 | 42 | msg, err := req.GetInnerMessage() 43 | if err != nil { 44 | // BUG: this should already have failed in the main handler. Abort 45 | log.Error(err) 46 | return nil, true 47 | } 48 | 49 | if sid := msg.Options.ServerID(); sid != nil { 50 | // RFC8415 §16.{2,5,7} 51 | // These message types MUST be discarded if they contain *any* ServerID option 52 | if msg.MessageType == dhcpv6.MessageTypeSolicit || 53 | msg.MessageType == dhcpv6.MessageTypeConfirm || 54 | msg.MessageType == dhcpv6.MessageTypeRebind { 55 | return nil, true 56 | } 57 | 58 | // Approximately all others MUST be discarded if the ServerID doesn't match 59 | if !sid.Equal(v6ServerID) { 60 | log.Infof("requested server ID does not match this server's ID. Got %v, want %v", sid, v6ServerID) 61 | return nil, true 62 | } 63 | } else if msg.MessageType == dhcpv6.MessageTypeRequest || 64 | msg.MessageType == dhcpv6.MessageTypeRenew || 65 | msg.MessageType == dhcpv6.MessageTypeDecline || 66 | msg.MessageType == dhcpv6.MessageTypeRelease { 67 | // RFC8415 §16.{6,8,10,11} 68 | // These message types MUST be discarded if they *don't* contain a ServerID option 69 | return nil, true 70 | } 71 | dhcpv6.WithServerID(v6ServerID)(resp) 72 | return resp, false 73 | } 74 | 75 | // Handler4 handles DHCPv4 packets for the server_id plugin. 76 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 77 | if v4ServerID == nil { 78 | log.Fatal("BUG: Plugin is running uninitialized!") 79 | return nil, true 80 | } 81 | if req.OpCode != dhcpv4.OpcodeBootRequest { 82 | log.Warningf("not a BootRequest, ignoring") 83 | return resp, false 84 | } 85 | if req.ServerIPAddr != nil && 86 | !req.ServerIPAddr.Equal(net.IPv4zero) && 87 | !req.ServerIPAddr.Equal(v4ServerID) { 88 | // This request is not for us, drop it. 89 | log.Infof("requested server ID does not match this server's ID. Got %v, want %v", req.ServerIPAddr, v4ServerID) 90 | return nil, true 91 | } 92 | resp.ServerIPAddr = make(net.IP, net.IPv4len) 93 | copy(resp.ServerIPAddr[:], v4ServerID) 94 | resp.UpdateOption(dhcpv4.OptServerIdentifier(v4ServerID)) 95 | return resp, false 96 | } 97 | 98 | func setup4(args ...string) (handler.Handler4, error) { 99 | log.Printf("loading `server_id` plugin for DHCPv4 with args: %v", args) 100 | if len(args) < 1 { 101 | return nil, errors.New("need an argument") 102 | } 103 | serverID := net.ParseIP(args[0]) 104 | if serverID == nil { 105 | return nil, errors.New("invalid or empty IP address") 106 | } 107 | if serverID.To4() == nil { 108 | return nil, errors.New("not a valid IPv4 address") 109 | } 110 | v4ServerID = serverID.To4() 111 | return Handler4, nil 112 | } 113 | 114 | func setup6(args ...string) (handler.Handler6, error) { 115 | log.Printf("loading `server_id` plugin for DHCPv6 with args: %v", args) 116 | if len(args) < 2 { 117 | return nil, errors.New("need a DUID type and value") 118 | } 119 | duidType := args[0] 120 | if duidType == "" { 121 | return nil, errors.New("got empty DUID type") 122 | } 123 | duidValue := args[1] 124 | if duidValue == "" { 125 | return nil, errors.New("got empty DUID value") 126 | } 127 | duidType = strings.ToLower(duidType) 128 | hwaddr, err := net.ParseMAC(duidValue) 129 | if err != nil { 130 | return nil, err 131 | } 132 | switch duidType { 133 | case "ll", "duid-ll", "duid_ll": 134 | v6ServerID = &dhcpv6.DUIDLL{ 135 | // sorry, only ethernet for now 136 | HWType: iana.HWTypeEthernet, 137 | LinkLayerAddr: hwaddr, 138 | } 139 | case "llt", "duid-llt", "duid_llt": 140 | v6ServerID = &dhcpv6.DUIDLLT{ 141 | // sorry, zero-time for now 142 | Time: 0, 143 | // sorry, only ethernet for now 144 | HWType: iana.HWTypeEthernet, 145 | LinkLayerAddr: hwaddr, 146 | } 147 | case "en", "uuid": 148 | return nil, errors.New("EN/UUID DUID type not supported yet") 149 | default: 150 | return nil, errors.New("Opaque DUID type not supported yet") 151 | } 152 | log.Printf("using %s %s", duidType, duidValue) 153 | 154 | return Handler6, nil 155 | } 156 | -------------------------------------------------------------------------------- /plugins/serverid/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package serverid 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/insomniacslk/dhcp/dhcpv6" 12 | ) 13 | 14 | func makeTestDUID(uuid string) dhcpv6.DUID { 15 | var uuidb [16]byte 16 | copy(uuidb[:], uuid) 17 | return &dhcpv6.DUIDUUID{ 18 | UUID: uuidb, 19 | } 20 | } 21 | 22 | func TestRejectBadServerIDV6(t *testing.T) { 23 | req, err := dhcpv6.NewMessage() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | v6ServerID = makeTestDUID("0000000000000000") 28 | 29 | req.MessageType = dhcpv6.MessageTypeRenew 30 | dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req) 31 | dhcpv6.WithServerID(makeTestDUID("0000000000000001"))(req) 32 | 33 | stub, err := dhcpv6.NewReplyFromMessage(req) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | resp, stop := Handler6(req, stub) 39 | if resp != nil { 40 | t.Error("server_id is sending a response message to a request with mismatched ServerID") 41 | } 42 | if !stop { 43 | t.Error("server_id did not interrupt processing on a request with mismatched ServerID") 44 | } 45 | } 46 | 47 | func TestRejectUnexpectedServerIDV6(t *testing.T) { 48 | req, err := dhcpv6.NewMessage() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | v6ServerID = makeTestDUID("0000000000000000") 53 | 54 | req.MessageType = dhcpv6.MessageTypeSolicit 55 | dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req) 56 | dhcpv6.WithServerID(makeTestDUID("0000000000000000"))(req) 57 | 58 | stub, err := dhcpv6.NewAdvertiseFromSolicit(req) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | resp, stop := Handler6(req, stub) 64 | if resp != nil { 65 | t.Error("server_id is sending a response message to a solicit with a ServerID") 66 | } 67 | if !stop { 68 | t.Error("server_id did not interrupt processing on a solicit with a ServerID") 69 | } 70 | } 71 | 72 | func TestAddServerIDV6(t *testing.T) { 73 | req, err := dhcpv6.NewMessage() 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | v6ServerID = makeTestDUID("0000000000000000") 78 | 79 | req.MessageType = dhcpv6.MessageTypeRebind 80 | dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req) 81 | 82 | stub, err := dhcpv6.NewReplyFromMessage(req) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | resp, _ := Handler6(req, stub) 88 | if resp == nil { 89 | t.Fatal("plugin did not return an answer") 90 | } 91 | 92 | if opt := resp.(*dhcpv6.Message).Options.ServerID(); opt == nil { 93 | t.Fatal("plugin did not add a ServerID option") 94 | } else if !opt.Equal(v6ServerID) { 95 | t.Fatalf("Got unexpected DUID: expected %v, got %v", v6ServerID, opt) 96 | } 97 | } 98 | 99 | func TestRejectInnerMessageServerID(t *testing.T) { 100 | req, err := dhcpv6.NewMessage() 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | v6ServerID = makeTestDUID("0000000000000000") 105 | 106 | req.MessageType = dhcpv6.MessageTypeSolicit 107 | dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req) 108 | dhcpv6.WithServerID(makeTestDUID("0000000000000000"))(req) 109 | 110 | stub, err := dhcpv6.NewAdvertiseFromSolicit(req) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | relayedRequest, err := dhcpv6.EncapsulateRelay(req, dhcpv6.MessageTypeRelayForward, net.IPv6loopback, net.IPv6loopback) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | resp, stop := Handler6(relayedRequest, stub) 121 | if resp != nil { 122 | t.Error("server_id is sending a response message to a relayed solicit with a ServerID") 123 | } 124 | if !stop { 125 | t.Error("server_id did not interrupt processing on a relayed solicit with a ServerID") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /plugins/sleep/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package sleep 6 | 7 | // This plugin introduces a delay in the DHCP response. 8 | 9 | import ( 10 | "fmt" 11 | "time" 12 | 13 | "github.com/coredhcp/coredhcp/handler" 14 | "github.com/coredhcp/coredhcp/logger" 15 | "github.com/coredhcp/coredhcp/plugins" 16 | "github.com/insomniacslk/dhcp/dhcpv4" 17 | "github.com/insomniacslk/dhcp/dhcpv6" 18 | ) 19 | 20 | var ( 21 | pluginName = "sleep" 22 | log = logger.GetLogger("plugins/" + pluginName) 23 | ) 24 | 25 | // Example configuration of the `sleep` plugin: 26 | // 27 | // server4: 28 | // plugins: 29 | // - sleep 300ms 30 | // - file: "leases4.txt" 31 | // 32 | // server6: 33 | // plugins: 34 | // - sleep 1s 35 | // - file: "leases6.txt" 36 | // 37 | // For the duration format, see the documentation of `time.ParseDuration`, 38 | // https://golang.org/pkg/time/#ParseDuration . 39 | 40 | // Plugin contains the `sleep` plugin data. 41 | var Plugin = plugins.Plugin{ 42 | Name: pluginName, 43 | Setup6: setup6, 44 | Setup4: setup4, 45 | } 46 | 47 | func setup6(args ...string) (handler.Handler6, error) { 48 | if len(args) != 1 { 49 | return nil, fmt.Errorf("want exactly one argument, got %d", len(args)) 50 | } 51 | delay, err := time.ParseDuration(args[0]) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to parse duration: %w", err) 54 | } 55 | log.Printf("loaded plugin for DHCPv6.") 56 | return makeSleepHandler6(delay), nil 57 | } 58 | 59 | func setup4(args ...string) (handler.Handler4, error) { 60 | if len(args) != 1 { 61 | return nil, fmt.Errorf("want exactly one argument, got %d", len(args)) 62 | } 63 | delay, err := time.ParseDuration(args[0]) 64 | if err != nil { 65 | return nil, fmt.Errorf("failed to parse duration: %w", err) 66 | } 67 | log.Printf("loaded plugin for DHCPv4.") 68 | return makeSleepHandler4(delay), nil 69 | } 70 | 71 | func makeSleepHandler6(delay time.Duration) handler.Handler6 { 72 | return func(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { 73 | log.Printf("introducing delay of %s in response", delay) 74 | // return the unmodified response, and instruct coredhcp to continue to 75 | // the next plugin. 76 | time.Sleep(delay) 77 | return resp, false 78 | } 79 | } 80 | 81 | func makeSleepHandler4(delay time.Duration) handler.Handler4 { 82 | return func(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 83 | log.Printf("introducing delay of %s in response", delay) 84 | // return the unmodified response, and instruct coredhcp to continue to 85 | // the next plugin. 86 | time.Sleep(delay) 87 | return resp, false 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /plugins/staticroute/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package staticroute 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | "strings" 11 | 12 | "github.com/coredhcp/coredhcp/handler" 13 | "github.com/coredhcp/coredhcp/logger" 14 | "github.com/coredhcp/coredhcp/plugins" 15 | "github.com/insomniacslk/dhcp/dhcpv4" 16 | ) 17 | 18 | var log = logger.GetLogger("plugins/staticroute") 19 | 20 | // Plugin wraps the information necessary to register a plugin. 21 | var Plugin = plugins.Plugin{ 22 | Name: "staticroute", 23 | Setup4: setup4, 24 | } 25 | 26 | var routes dhcpv4.Routes 27 | 28 | func setup4(args ...string) (handler.Handler4, error) { 29 | log.Printf("loaded plugin for DHCPv4.") 30 | routes = make(dhcpv4.Routes, 0) 31 | 32 | if len(args) < 1 { 33 | return nil, errors.New("need at least one static route") 34 | } 35 | 36 | var err error 37 | for _, arg := range args { 38 | fields := strings.Split(arg, ",") 39 | if len(fields) != 2 { 40 | return Handler4, errors.New("expected a destination/gateway pair, got: " + arg) 41 | } 42 | 43 | route := &dhcpv4.Route{} 44 | _, route.Dest, err = net.ParseCIDR(fields[0]) 45 | if err != nil { 46 | return Handler4, errors.New("expected a destination subnet, got: " + fields[0]) 47 | } 48 | 49 | route.Router = net.ParseIP(fields[1]) 50 | if route.Router == nil { 51 | return Handler4, errors.New("expected a gateway address, got: " + fields[1]) 52 | } 53 | 54 | routes = append(routes, route) 55 | log.Debugf("adding static route %s", route) 56 | } 57 | 58 | log.Printf("loaded %d static routes.", len(routes)) 59 | 60 | return Handler4, nil 61 | } 62 | 63 | // Handler4 handles DHCPv4 packets for the static routes plugin 64 | func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { 65 | if len(routes) > 0 { 66 | resp.Options.Update(dhcpv4.Option{ 67 | Code: dhcpv4.OptionCode(dhcpv4.OptionClasslessStaticRoute), 68 | Value: routes, 69 | }) 70 | } 71 | 72 | return resp, false 73 | } 74 | -------------------------------------------------------------------------------- /plugins/staticroute/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package staticroute 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSetup4(t *testing.T) { 14 | assert.Empty(t, routes) 15 | 16 | var err error 17 | // no args 18 | _, err = setup4() 19 | if assert.Error(t, err) { 20 | assert.Equal(t, "need at least one static route", err.Error()) 21 | } 22 | 23 | // invalid arg 24 | _, err = setup4("foo") 25 | if assert.Error(t, err) { 26 | assert.Equal(t, "expected a destination/gateway pair, got: foo", err.Error()) 27 | } 28 | 29 | // invalid destination 30 | _, err = setup4("foo,") 31 | if assert.Error(t, err) { 32 | assert.Equal(t, "expected a destination subnet, got: foo", err.Error()) 33 | } 34 | 35 | // invalid gateway 36 | _, err = setup4("10.0.0.0/8,foo") 37 | if assert.Error(t, err) { 38 | assert.Equal(t, "expected a gateway address, got: foo", err.Error()) 39 | } 40 | 41 | // valid route 42 | _, err = setup4("10.0.0.0/8,192.168.1.1") 43 | if assert.NoError(t, err) { 44 | if assert.Equal(t, 1, len(routes)) { 45 | assert.Equal(t, "10.0.0.0/8", routes[0].Dest.String()) 46 | assert.Equal(t, "192.168.1.1", routes[0].Router.String()) 47 | } 48 | } 49 | 50 | // multiple valid routes 51 | _, err = setup4("10.0.0.0/8,192.168.1.1", "192.168.2.0/24,192.168.1.100") 52 | if assert.NoError(t, err) { 53 | if assert.Equal(t, 2, len(routes)) { 54 | assert.Equal(t, "10.0.0.0/8", routes[0].Dest.String()) 55 | assert.Equal(t, "192.168.1.1", routes[0].Router.String()) 56 | assert.Equal(t, "192.168.2.0/24", routes[1].Dest.String()) 57 | assert.Equal(t, "192.168.1.100", routes[1].Router.String()) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/handle.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package server 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net" 11 | "sync" 12 | 13 | "golang.org/x/net/ipv4" 14 | "golang.org/x/net/ipv6" 15 | 16 | "github.com/insomniacslk/dhcp/dhcpv4" 17 | "github.com/insomniacslk/dhcp/dhcpv6" 18 | ) 19 | 20 | // HandleMsg6 runs for every received DHCPv6 packet. It will run every 21 | // registered handler in sequence, and reply with the resulting response. 22 | // It will not reply if the resulting response is `nil`. 23 | func (l *listener6) HandleMsg6(buf []byte, oob *ipv6.ControlMessage, peer *net.UDPAddr) { 24 | d, err := dhcpv6.FromBytes(buf) 25 | bufpool.Put(&buf) 26 | if err != nil { 27 | log.Printf("Error parsing DHCPv6 request: %v", err) 28 | return 29 | } 30 | 31 | // decapsulate the relay message 32 | msg, err := d.GetInnerMessage() 33 | if err != nil { 34 | log.Warningf("DHCPv6: cannot get inner message: %v", err) 35 | return 36 | } 37 | 38 | // Create a suitable basic response packet 39 | var resp dhcpv6.DHCPv6 40 | switch msg.Type() { 41 | case dhcpv6.MessageTypeSolicit: 42 | if msg.GetOneOption(dhcpv6.OptionRapidCommit) != nil { 43 | resp, err = dhcpv6.NewReplyFromMessage(msg) 44 | } else { 45 | resp, err = dhcpv6.NewAdvertiseFromSolicit(msg) 46 | } 47 | case dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeConfirm, dhcpv6.MessageTypeRenew, 48 | dhcpv6.MessageTypeRebind, dhcpv6.MessageTypeRelease, dhcpv6.MessageTypeInformationRequest: 49 | resp, err = dhcpv6.NewReplyFromMessage(msg) 50 | default: 51 | err = fmt.Errorf("MainHandler6: message type %d not supported", msg.Type()) 52 | } 53 | if err != nil { 54 | log.Printf("MainHandler6: NewReplyFromDHCPv6Message failed: %v", err) 55 | return 56 | } 57 | 58 | var stop bool 59 | for _, handler := range l.handlers { 60 | resp, stop = handler(d, resp) 61 | if stop { 62 | break 63 | } 64 | } 65 | if resp == nil { 66 | log.Print("MainHandler6: dropping request because response is nil") 67 | return 68 | } 69 | 70 | // if the request was relayed, re-encapsulate the response 71 | if d.IsRelay() { 72 | if rmsg, ok := resp.(*dhcpv6.Message); !ok { 73 | log.Warningf("DHCPv6: response is a relayed message, not reencapsulating") 74 | } else { 75 | tmp, err := dhcpv6.NewRelayReplFromRelayForw(d.(*dhcpv6.RelayMessage), rmsg) 76 | if err != nil { 77 | log.Warningf("DHCPv6: cannot create relay-repl from relay-forw: %v", err) 78 | return 79 | } 80 | resp = tmp 81 | } 82 | } 83 | 84 | var woob *ipv6.ControlMessage 85 | if peer.IP.IsLinkLocalUnicast() { 86 | // LL need to be directed to the correct interface. Globally reachable 87 | // addresses should use the default route, in case of asymetric routing. 88 | switch { 89 | case l.Interface.Index != 0: 90 | woob = &ipv6.ControlMessage{IfIndex: l.Interface.Index} 91 | case oob != nil && oob.IfIndex != 0: 92 | woob = &ipv6.ControlMessage{IfIndex: oob.IfIndex} 93 | default: 94 | log.Errorf("HandleMsg6: Did not receive interface information") 95 | } 96 | } 97 | if _, err := l.WriteTo(resp.ToBytes(), woob, peer); err != nil { 98 | log.Printf("MainHandler6: conn.Write to %v failed: %v", peer, err) 99 | } 100 | } 101 | 102 | func (l *listener4) HandleMsg4(buf []byte, oob *ipv4.ControlMessage, _peer net.Addr) { 103 | var ( 104 | resp, tmp *dhcpv4.DHCPv4 105 | err error 106 | stop bool 107 | ) 108 | 109 | req, err := dhcpv4.FromBytes(buf) 110 | bufpool.Put(&buf) 111 | if err != nil { 112 | log.Printf("Error parsing DHCPv4 request: %v", err) 113 | return 114 | } 115 | 116 | if req.OpCode != dhcpv4.OpcodeBootRequest { 117 | log.Printf("MainHandler4: unsupported opcode %d. Only BootRequest (%d) is supported", req.OpCode, dhcpv4.OpcodeBootRequest) 118 | return 119 | } 120 | tmp, err = dhcpv4.NewReplyFromRequest(req) 121 | if err != nil { 122 | log.Printf("MainHandler4: failed to build reply: %v", err) 123 | return 124 | } 125 | switch mt := req.MessageType(); mt { 126 | case dhcpv4.MessageTypeDiscover: 127 | tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) 128 | case dhcpv4.MessageTypeRequest: 129 | tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) 130 | default: 131 | log.Printf("plugins/server: Unhandled message type: %v", mt) 132 | return 133 | } 134 | 135 | resp = tmp 136 | for _, handler := range l.handlers { 137 | resp, stop = handler(req, resp) 138 | if stop { 139 | break 140 | } 141 | } 142 | 143 | if resp != nil { 144 | useEthernet := false 145 | var peer *net.UDPAddr 146 | if !req.GatewayIPAddr.IsUnspecified() { 147 | // TODO: make RFC8357 compliant 148 | peer = &net.UDPAddr{IP: req.GatewayIPAddr, Port: dhcpv4.ServerPort} 149 | } else if resp.MessageType() == dhcpv4.MessageTypeNak { 150 | peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort} 151 | } else if !req.ClientIPAddr.IsUnspecified() { 152 | peer = &net.UDPAddr{IP: req.ClientIPAddr, Port: dhcpv4.ClientPort} 153 | } else if req.IsBroadcast() { 154 | peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort} 155 | } else { 156 | //sends a layer2 frame so that we can define the destination MAC address 157 | peer = &net.UDPAddr{IP: resp.YourIPAddr, Port: dhcpv4.ClientPort} 158 | useEthernet = true 159 | } 160 | 161 | var woob *ipv4.ControlMessage 162 | if peer.IP.Equal(net.IPv4bcast) || peer.IP.IsLinkLocalUnicast() || useEthernet { 163 | // Direct broadcasts, link-local and layer2 unicasts to the interface the request was 164 | // received on. Other packets should use the normal routing table in 165 | // case of asymetric routing 166 | switch { 167 | case l.Interface.Index != 0: 168 | woob = &ipv4.ControlMessage{IfIndex: l.Interface.Index} 169 | case oob != nil && oob.IfIndex != 0: 170 | woob = &ipv4.ControlMessage{IfIndex: oob.IfIndex} 171 | default: 172 | log.Errorf("HandleMsg4: Did not receive interface information") 173 | } 174 | } 175 | 176 | if useEthernet { 177 | intf, err := net.InterfaceByIndex(woob.IfIndex) 178 | if err != nil { 179 | log.Errorf("MainHandler4: Can not get Interface for index %d %v", woob.IfIndex, err) 180 | return 181 | } 182 | err = sendEthernet(*intf, resp) 183 | if err != nil { 184 | log.Errorf("MainHandler4: Cannot send Ethernet packet: %v", err) 185 | } 186 | } else { 187 | if _, err := l.WriteTo(resp.ToBytes(), woob, peer); err != nil { 188 | log.Errorf("MainHandler4: conn.Write to %v failed: %v", peer, err) 189 | } 190 | } 191 | } else { 192 | log.Print("MainHandler4: dropping request because response is nil") 193 | } 194 | } 195 | 196 | // XXX: performance-wise, Pool may or may not be good (see https://github.com/golang/go/issues/23199) 197 | // Interface is good for what we want. Maybe "just" trust the GC and we'll be fine ? 198 | var bufpool = sync.Pool{New: func() interface{} { r := make([]byte, MaxDatagram); return &r }} 199 | 200 | // MaxDatagram is the maximum length of message that can be received. 201 | const MaxDatagram = 1 << 16 202 | 203 | // XXX: investigate using RecvMsgs to batch messages and reduce syscalls 204 | 205 | // Serve6 handles datagrams received on conn and passes them to the pluginchain 206 | func (l *listener6) Serve() error { 207 | log.Printf("Listen %s", l.LocalAddr()) 208 | for { 209 | b := *bufpool.Get().(*[]byte) 210 | b = b[:MaxDatagram] //Reslice to max capacity in case the buffer in pool was resliced smaller 211 | 212 | n, oob, peer, err := l.ReadFrom(b) 213 | if errors.Is(err, net.ErrClosed) { 214 | // Server is quitting 215 | return nil 216 | } else if err != nil { 217 | log.Printf("Error reading from connection: %v", err) 218 | return err 219 | } 220 | go l.HandleMsg6(b[:n], oob, peer.(*net.UDPAddr)) 221 | } 222 | } 223 | 224 | // Serve6 handles datagrams received on conn and passes them to the pluginchain 225 | func (l *listener4) Serve() error { 226 | log.Printf("Listen %s", l.LocalAddr()) 227 | for { 228 | b := *bufpool.Get().(*[]byte) 229 | b = b[:MaxDatagram] //Reslice to max capacity in case the buffer in pool was resliced smaller 230 | 231 | n, oob, peer, err := l.ReadFrom(b) 232 | if errors.Is(err, net.ErrClosed) { 233 | // Server is quitting 234 | return nil 235 | } else if err != nil { 236 | log.Printf("Error reading from connection: %v", err) 237 | return err 238 | } 239 | go l.HandleMsg4(b[:n], oob, peer.(*net.UDPAddr)) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /server/sendEthernet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | // +build linux 6 | 7 | package server 8 | 9 | import ( 10 | "fmt" 11 | "net" 12 | "syscall" 13 | 14 | "github.com/google/gopacket" 15 | "github.com/google/gopacket/layers" 16 | "github.com/insomniacslk/dhcp/dhcpv4" 17 | ) 18 | 19 | //this function sends an unicast to the hardware address defined in resp.ClientHWAddr, 20 | //the layer3 destination address is still the broadcast address; 21 | //iface: the interface where the DHCP message should be sent; 22 | //resp: DHCPv4 struct, which should be sent; 23 | func sendEthernet(iface net.Interface, resp *dhcpv4.DHCPv4) error { 24 | 25 | eth := layers.Ethernet{ 26 | EthernetType: layers.EthernetTypeIPv4, 27 | SrcMAC: iface.HardwareAddr, 28 | DstMAC: resp.ClientHWAddr, 29 | } 30 | ip := layers.IPv4{ 31 | Version: 4, 32 | TTL: 64, 33 | SrcIP: resp.ServerIPAddr, 34 | DstIP: resp.YourIPAddr, 35 | Protocol: layers.IPProtocolUDP, 36 | Flags: layers.IPv4DontFragment, 37 | } 38 | udp := layers.UDP{ 39 | SrcPort: dhcpv4.ServerPort, 40 | DstPort: dhcpv4.ClientPort, 41 | } 42 | 43 | err := udp.SetNetworkLayerForChecksum(&ip) 44 | if err != nil { 45 | return fmt.Errorf("Send Ethernet: Couldn't set network layer: %v", err) 46 | } 47 | 48 | buf := gopacket.NewSerializeBuffer() 49 | opts := gopacket.SerializeOptions{ 50 | ComputeChecksums: true, 51 | FixLengths: true, 52 | } 53 | 54 | // Decode a packet 55 | packet := gopacket.NewPacket(resp.ToBytes(), layers.LayerTypeDHCPv4, gopacket.NoCopy) 56 | dhcpLayer := packet.Layer(layers.LayerTypeDHCPv4) 57 | dhcp, ok := dhcpLayer.(gopacket.SerializableLayer) 58 | if !ok { 59 | return fmt.Errorf("Layer %s is not serializable", dhcpLayer.LayerType().String()) 60 | } 61 | err = gopacket.SerializeLayers(buf, opts, ð, &ip, &udp, dhcp) 62 | if err != nil { 63 | return fmt.Errorf("Cannot serialize layer: %v", err) 64 | } 65 | data := buf.Bytes() 66 | 67 | fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, 0) 68 | if err != nil { 69 | return fmt.Errorf("Send Ethernet: Cannot open socket: %v", err) 70 | } 71 | defer func() { 72 | err = syscall.Close(fd) 73 | if err != nil { 74 | log.Errorf("Send Ethernet: Cannot close socket: %v", err) 75 | } 76 | }() 77 | 78 | err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) 79 | if err != nil { 80 | log.Errorf("Send Ethernet: Cannot set option for socket: %v", err) 81 | } 82 | 83 | var hwAddr [8]byte 84 | copy(hwAddr[0:6], resp.ClientHWAddr[0:6]) 85 | ethAddr := syscall.SockaddrLinklayer{ 86 | Protocol: 0, 87 | Ifindex: iface.Index, 88 | Halen: 6, 89 | Addr: hwAddr, //not used 90 | } 91 | err = syscall.Sendto(fd, data, 0, ðAddr) 92 | if err != nil { 93 | return fmt.Errorf("Cannot send frame via socket: %v", err) 94 | } 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /server/serve.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-present the CoreDHCP Authors. All rights reserved 2 | // This source code is licensed under the MIT license found in the 3 | // LICENSE file in the root directory of this source tree. 4 | 5 | package server 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | 13 | "golang.org/x/net/ipv4" 14 | "golang.org/x/net/ipv6" 15 | 16 | "github.com/coredhcp/coredhcp/config" 17 | "github.com/coredhcp/coredhcp/handler" 18 | "github.com/coredhcp/coredhcp/logger" 19 | "github.com/coredhcp/coredhcp/plugins" 20 | "github.com/insomniacslk/dhcp/dhcpv4/server4" 21 | "github.com/insomniacslk/dhcp/dhcpv6/server6" 22 | ) 23 | 24 | var log = logger.GetLogger("server") 25 | 26 | type listener6 struct { 27 | *ipv6.PacketConn 28 | net.Interface 29 | handlers []handler.Handler6 30 | } 31 | 32 | type listener4 struct { 33 | *ipv4.PacketConn 34 | net.Interface 35 | handlers []handler.Handler4 36 | } 37 | 38 | type listener interface { 39 | io.Closer 40 | } 41 | 42 | // Servers contains state for a running server (with possibly multiple interfaces/listeners) 43 | type Servers struct { 44 | listeners []listener 45 | errors chan error 46 | } 47 | 48 | func listen4(a *net.UDPAddr) (*listener4, error) { 49 | var err error 50 | l4 := listener4{} 51 | udpConn, err := server4.NewIPv4UDPConn(a.Zone, a) 52 | if err != nil { 53 | return nil, err 54 | } 55 | l4.PacketConn = ipv4.NewPacketConn(udpConn) 56 | var ifi *net.Interface 57 | if a.Zone != "" { 58 | ifi, err = net.InterfaceByName(a.Zone) 59 | if err != nil { 60 | return nil, fmt.Errorf("DHCPv4: Listen could not find interface %s: %v", a.Zone, err) 61 | } 62 | l4.Interface = *ifi 63 | } else { 64 | 65 | // When not bound to an interface, we need the information in each 66 | // packet to know which interface it came on 67 | err = l4.SetControlMessage(ipv4.FlagInterface, true) 68 | if err != nil { 69 | return nil, err 70 | } 71 | } 72 | 73 | if a.IP.IsMulticast() { 74 | err = l4.JoinGroup(ifi, a) 75 | if err != nil { 76 | return nil, err 77 | } 78 | } 79 | return &l4, nil 80 | } 81 | 82 | func listen6(a *net.UDPAddr) (*listener6, error) { 83 | l6 := listener6{} 84 | udpconn, err := server6.NewIPv6UDPConn(a.Zone, a) 85 | if err != nil { 86 | return nil, err 87 | } 88 | l6.PacketConn = ipv6.NewPacketConn(udpconn) 89 | var ifi *net.Interface 90 | if a.Zone != "" { 91 | ifi, err = net.InterfaceByName(a.Zone) 92 | if err != nil { 93 | return nil, fmt.Errorf("DHCPv4: Listen could not find interface %s: %v", a.Zone, err) 94 | } 95 | l6.Interface = *ifi 96 | } else { 97 | // When not bound to an interface, we need the information in each 98 | // packet to know which interface it came on 99 | err = l6.SetControlMessage(ipv6.FlagInterface, true) 100 | if err != nil { 101 | return nil, err 102 | } 103 | } 104 | 105 | if a.IP.IsMulticast() { 106 | err = l6.JoinGroup(ifi, a) 107 | if err != nil { 108 | return nil, err 109 | } 110 | } 111 | return &l6, nil 112 | } 113 | 114 | // Start will start the server asynchronously. See `Wait` to wait until 115 | // the execution ends. 116 | func Start(config *config.Config) (*Servers, error) { 117 | handlers4, handlers6, err := plugins.LoadPlugins(config) 118 | if err != nil { 119 | return nil, err 120 | } 121 | srv := Servers{ 122 | errors: make(chan error), 123 | } 124 | 125 | // listen 126 | if config.Server6 != nil { 127 | log.Println("Starting DHCPv6 server") 128 | for _, addr := range config.Server6.Addresses { 129 | var l6 *listener6 130 | l6, err = listen6(&addr) 131 | if err != nil { 132 | goto cleanup 133 | } 134 | l6.handlers = handlers6 135 | srv.listeners = append(srv.listeners, l6) 136 | go func() { 137 | srv.errors <- l6.Serve() 138 | }() 139 | } 140 | } 141 | 142 | if config.Server4 != nil { 143 | log.Println("Starting DHCPv4 server") 144 | for _, addr := range config.Server4.Addresses { 145 | var l4 *listener4 146 | l4, err = listen4(&addr) 147 | if err != nil { 148 | goto cleanup 149 | } 150 | l4.handlers = handlers4 151 | srv.listeners = append(srv.listeners, l4) 152 | go func() { 153 | srv.errors <- l4.Serve() 154 | }() 155 | } 156 | } 157 | 158 | return &srv, nil 159 | 160 | cleanup: 161 | srv.Close() 162 | return nil, err 163 | } 164 | 165 | // Wait waits until the end of the execution of the server. 166 | func (s *Servers) Wait() error { 167 | log.Debug("Waiting") 168 | errs := make([]error, 1, len(s.listeners)) 169 | errs[0] = <-s.errors 170 | s.Close() 171 | // Wait for the other listeners to close 172 | for i := 1; i < len(s.listeners); i++ { 173 | errs = append(errs, <-s.errors) 174 | } 175 | return errors.Join(errs...) 176 | } 177 | 178 | // Close closes all listening connections 179 | func (s *Servers) Close() { 180 | for _, srv := range s.listeners { 181 | if srv != nil { 182 | srv.Close() 183 | } 184 | } 185 | } 186 | --------------------------------------------------------------------------------