add all files from Hong

This commit is contained in:
zhangsz
2025-06-30 09:23:28 +08:00
parent ceb1fe2640
commit 9b7d32fbd9
69 changed files with 7280 additions and 0 deletions

1
CNAME Normal file
View File

@@ -0,0 +1 @@
coredhcp.io

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 coredhcp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-tactile

5
cmds/client/README.md Normal file
View File

@@ -0,0 +1,5 @@
# DHCPv6 debug client
This is a simple dhcpv6 client for use as a debugging tool with coredhcp
***This is not a general-purpose DHCP client. This is only a testing/debugging tool for developing CoreDHCP***

61
cmds/client/main.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package main
/*
* Sample DHCPv6 client to test on the local interface
*/
import (
"flag"
"net"
"github.com/coredhcp/coredhcp/logger"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/client6"
"github.com/insomniacslk/dhcp/iana"
)
var log = logger.GetLogger("main")
func main() {
flag.Parse()
var macString string
if len(flag.Args()) > 0 {
macString = flag.Arg(0)
} else {
macString = "00:11:22:33:44:55"
}
c := client6.NewClient()
c.LocalAddr = &net.UDPAddr{
IP: net.ParseIP("::1"),
Port: 546,
}
c.RemoteAddr = &net.UDPAddr{
IP: net.ParseIP("::1"),
Port: 547,
}
log.Printf("%+v", c)
mac, err := net.ParseMAC(macString)
if err != nil {
log.Fatal(err)
}
duid := dhcpv6.DUIDLLT{
HWType: iana.HWTypeEthernet,
Time: dhcpv6.GetTime(),
LinkLayerAddr: mac,
}
conv, err := c.Exchange("lo", dhcpv6.WithClientID(&duid))
for _, p := range conv {
log.Print(p.Summary())
}
if err != nil {
log.Fatal(err)
}
}

View File

@@ -0,0 +1,40 @@
## CoreDHCP Generator
`coredhcp-generator` is a tool used to build CoreDHCP with the plugins you want.
Why is it even needed? Go is a compiled language with no dynamic loading
support. In order to load a plugin, it has to be compiled in. We are happy to
provide a standard [main.go](/cmds/coredhcp/main.go), and at the same time we
don't want to include plugins that not everyone would use, otherwise the binary
size would grow without control.
You can use `coredhcp-generator` to generate a `main.go` that includes all the
plugins you wish. Just use it as follows:
```
$ ./coredhcp-generator --from core-plugins.txt
2019/11/21 23:32:04 Generating output file '/tmp/coredhcp547019106/coredhcp.go' with 7 plugin(s):
2019/11/21 23:32:04 1) github.com/coredhcp/coredhcp/plugins/file
2019/11/21 23:32:04 2) github.com/coredhcp/coredhcp/plugins/lease_time
2019/11/21 23:32:04 3) github.com/coredhcp/coredhcp/plugins/netmask
2019/11/21 23:32:04 4) github.com/coredhcp/coredhcp/plugins/range
2019/11/21 23:32:04 5) github.com/coredhcp/coredhcp/plugins/router
2019/11/21 23:32:04 6) github.com/coredhcp/coredhcp/plugins/server_id
2019/11/21 23:32:04 7) github.com/coredhcp/coredhcp/plugins/dns
2019/11/21 23:32:04 Generated file '/tmp/coredhcp547019106/coredhcp.go'. You can build it by running 'go build' in the output directory.
```
You can also specify the plugin list on the command line, or mix it with
`--from`:
```
$ ./coredhcp-generator --from core-plugins.txt \
github.com/coredhcp/plugins/redis
```
Notice that it created a file called `coredhcp.go` in a temporary directory. You
can now `go build` that file and have your own custom CoreDHCP.
## Bugs
CoreDHCP uses Go versioned modules. The generated file does not do that yet. We
will add this feature soon.

View File

@@ -0,0 +1,15 @@
github.com/coredhcp/coredhcp/plugins/autoconfigure
github.com/coredhcp/coredhcp/plugins/dns
github.com/coredhcp/coredhcp/plugins/file
github.com/coredhcp/coredhcp/plugins/ipv6only
github.com/coredhcp/coredhcp/plugins/leasetime
github.com/coredhcp/coredhcp/plugins/mtu
github.com/coredhcp/coredhcp/plugins/netmask
github.com/coredhcp/coredhcp/plugins/nbp
github.com/coredhcp/coredhcp/plugins/prefix
github.com/coredhcp/coredhcp/plugins/range
github.com/coredhcp/coredhcp/plugins/router
github.com/coredhcp/coredhcp/plugins/serverid
github.com/coredhcp/coredhcp/plugins/searchdomains
github.com/coredhcp/coredhcp/plugins/sleep
github.com/coredhcp/coredhcp/plugins/staticroute

View File

@@ -0,0 +1,104 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
{{/* This file is the template source. The following comment obviously doesn't apply here */ -}}
// This is a generated file, edits should be made in the corresponding source file
// And this file regenerated using `coredhcp-generator --from core-plugins.txt`
package main
import (
"fmt"
"io"
"os"
"github.com/coredhcp/coredhcp/config"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/server"
"github.com/coredhcp/coredhcp/plugins"
{{- range $plugin := .}}
{{- /* We import all plugins as pl_<pluginname> to avoid conflicts with reserved keywords */}}
{{importname $plugin}} "{{$plugin}}"
{{- end}}
"github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
)
var (
flagLogFile = flag.StringP("logfile", "l", "", "Name of the log file to append to. Default: stdout/stderr only")
flagLogNoStdout = flag.BoolP("nostdout", "N", false, "Disable logging to stdout/stderr")
flagLogLevel = flag.StringP("loglevel", "L", "info", fmt.Sprintf("Log level. One of %v", getLogLevels()))
flagConfig = flag.StringP("conf", "c", "", "Use this configuration file instead of the default location")
flagPlugins = flag.BoolP("plugins", "P", false, "list plugins")
)
var logLevels = map[string]func(*logrus.Logger){
"none": func(l *logrus.Logger) { l.SetOutput(io.Discard) },
"debug": func(l *logrus.Logger) { l.SetLevel(logrus.DebugLevel) },
"info": func(l *logrus.Logger) { l.SetLevel(logrus.InfoLevel) },
"warning": func(l *logrus.Logger) { l.SetLevel(logrus.WarnLevel) },
"error": func(l *logrus.Logger) { l.SetLevel(logrus.ErrorLevel) },
"fatal": func(l *logrus.Logger) { l.SetLevel(logrus.FatalLevel) },
}
func getLogLevels() []string {
var levels []string
for k := range logLevels {
levels = append(levels, k)
}
return levels
}
var desiredPlugins = []*plugins.Plugin{
{{- range $plugin := .}}
&{{importname $plugin}}.Plugin,
{{- end}}
}
func main() {
flag.Parse()
if *flagPlugins {
for _, p := range desiredPlugins {
fmt.Println(p.Name)
}
os.Exit(0)
}
log := logger.GetLogger("main")
fn, ok := logLevels[*flagLogLevel]
if !ok {
log.Fatalf("Invalid log level '%s'. Valid log levels are %v", *flagLogLevel, getLogLevels())
}
fn(log.Logger)
log.Infof("Setting log level to '%s'", *flagLogLevel)
if *flagLogFile != "" {
log.Infof("Logging to file %s", *flagLogFile)
logger.WithFile(log, *flagLogFile)
}
if *flagLogNoStdout {
log.Infof("Disabling logging to stdout/stderr")
logger.WithNoStdOutErr(log)
}
config, err := config.Load(*flagConfig)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// register plugins
for _, plugin := range desiredPlugins {
if err := plugins.RegisterPlugin(plugin); err != nil {
log.Fatalf("Failed to register plugin '%s': %v", plugin.Name, err)
}
}
// start server
srv, err := server.Start(config)
if err != nil {
log.Fatal(err)
}
if err := srv.Wait(); err != nil {
log.Error(err)
}
}

View File

@@ -0,0 +1,144 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package main
import (
"bufio"
"fmt"
"html/template"
"log"
"os"
"path"
"sort"
"strings"
flag "github.com/spf13/pflag"
)
const (
defaultTemplateFile = "coredhcp.go.template"
importBase = "github.com/coredhcp/coredhcp/"
)
var (
flagTemplate = flag.StringP("template", "t", defaultTemplateFile, "Template file name")
flagOutfile = flag.StringP("outfile", "o", "", "Output file path")
flagFromFile = flag.StringP("from", "f", "", "Optional file name to get the plugin list from, one import path per line")
)
var funcMap = template.FuncMap{
"importname": func(importPath string) (string, error) {
parts := strings.Split(importPath, "/")
if len(parts) < 1 {
return "", fmt.Errorf("no components found in import path '%s'", importPath)
}
return "pl_" + parts[len(parts)-1], nil
},
}
func usage() {
fmt.Fprintf(flag.CommandLine.Output(),
"%s [-template tpl] [-outfile out] [-from pluginlist] [plugin [plugin...]]\n",
os.Args[0],
)
flag.PrintDefaults()
fmt.Fprintln(flag.CommandLine.Output(), ` plugin
Plugin name to include, as go import path.
Short names can be used for builtin coredhcp plugins (eg "serverid")`)
}
func main() {
flag.Usage = usage
flag.Parse()
data, err := os.ReadFile(*flagTemplate)
if err != nil {
log.Fatalf("Failed to read template file '%s': %v", *flagTemplate, err)
}
t, err := template.New("coredhcp").Funcs(funcMap).Parse(string(data))
if err != nil {
log.Fatalf("Template parsing failed: %v", err)
}
plugins := make(map[string]bool)
for _, pl := range flag.Args() {
pl := strings.TrimSpace(pl)
if pl == "" {
continue
}
if !strings.ContainsRune(pl, '/') {
// A bare name was specified, not a full import path.
// Coredhcp plugins aren't in the standard library, and it's unlikely someone
// would put them at the base of $GOPATH/src.
// Assume this is one of the builtin plugins. If needed, use the -from option
// which always requires (and uses) exact paths
// XXX: we could also look into github.com/coredhcp/plugins
pl = importBase + pl
}
plugins[pl] = true
}
if *flagFromFile != "" {
// additional plugin names from a text file, one line per plugin import
// path
fd, err := os.Open(*flagFromFile)
if err != nil {
log.Fatalf("Failed to read file '%s': %v", *flagFromFile, err)
}
defer func() {
if err := fd.Close(); err != nil {
log.Printf("Error closing file '%s': %v", *flagFromFile, err)
}
}()
sc := bufio.NewScanner(fd)
for sc.Scan() {
pl := strings.TrimSpace(sc.Text())
if pl == "" {
continue
}
plugins[pl] = true
}
if err := sc.Err(); err != nil {
log.Fatalf("Error reading file '%s': %v", *flagFromFile, err)
}
}
if len(plugins) == 0 {
log.Fatalf("No plugin specified!")
}
outfile := *flagOutfile
if outfile == "" {
tmpdir, err := os.MkdirTemp("", "coredhcp")
if err != nil {
log.Fatalf("Cannot create temporary directory: %v", err)
}
outfile = path.Join(tmpdir, "coredhcp.go")
}
log.Printf("Generating output file '%s' with %d plugin(s):", outfile, len(plugins))
idx := 1
for pl := range plugins {
log.Printf("% 3d) %s", idx, pl)
idx++
}
outFD, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Failed to create output file '%s': %v", outfile, err)
}
defer func() {
if err := outFD.Close(); err != nil {
log.Printf("Error while closing file descriptor for '%s': %v", outfile, err)
}
}()
// WARNING: no escaping of the provided strings is done
pluginList := make([]string, 0, len(plugins))
for pl := range plugins {
pluginList = append(pluginList, pl)
}
sort.Strings(pluginList)
if err := t.Execute(outFD, pluginList); err != nil {
log.Fatalf("Template execution failed: %v", err)
}
log.Printf("Generated file '%s'. You can build it by running 'go build' in the output directory.", outfile)
fmt.Println(path.Dir(outfile))
}

167
cmds/coredhcp/config.yml Normal file
View File

@@ -0,0 +1,167 @@
# CoreDHCP configuration (yaml)
# In this file, lines starting with "## " represent default values,
# while uncommented lines are examples which have no default value
# The base level configuration has two sections, one for each protocol version
# (DHCPv4 and DHCPv6). There is no shared configuration at the moment.
# At a high level, both accept the same structure of configuration
# DHCPv6 configuration
#server6:
# listen is an optional section to specify how the server binds to an
# interface or address.
# If unset, the server will join the link-layer multicast group for all
# dhcp servers and relays on each interface, as well as the site-scoped
# multicast group for all dhcp servers.
# Note that in this default configuration the server will not handle
# unicast datagrams, and is equivalent to:
## listen:
## - "[ff02::1:2]"
## - "[ff05::1:3]"
# In general, listen takes a list of addresses, with the general syntax
# "[address%interface]:port", where each part is optional.
# Omitting the address results in the wildcard address being used
# Omitting the interface skips binding the listener to a specific interface
# and listens on all interfaces instead
# Omitting the port uses the default port for DHCPv6 (547)
#
# For example:
# - "[::]"
# Listen on the wildcard address on all interfaces on the default port.
# Note that no multicast group will be joined, so this will *not* work with
# most clients
#
# - ":44480"
# Listens on the wildcard address on a specific port. This can be used if
# you have a relay setup that can contact this server using unicast
#
# - "[::%eno1]"
# Listens on the wildcard address on one interface. This can be used if you
# want to spawn multiple servers with different configurations for multiple
# interfaces, behind a relay that can use unicast
#
# There are some special considerations for multicast:
# - "[ff02::1:2%eno1]"
# Listens on a link-layer multicast address bound to an interface. Also
# used to spawn multiple servers, but for clients on the same subnet
#
# - "[ff05::1:3%eno1]"
# Joining a multicast group with an interface allows to skip the default
# routing table when responding to clients, which can be useful if
# multicast is not otherwise configured system-wide
#
# - "[ff02::1:2]"
# Using a multicast address without an interface will be auto-expanded, so
# that it listens on all available interfaces
# plugins is a mandatory section, which defines how requests are handled.
# It is a list of maps, matching plugin names to their arguments.
# The order is meaningful, as incoming requests are handled by each plugin
# in turn. There is no default value for a plugin configuration, and a
# plugin that is not mentioned will not be loaded at all
#
# The following contains examples of the most common, builtin plugins.
# External plugins should document their arguments in their own
# documentations or readmes
#plugins:
# server_id is mandatory for RFC-compliant operation.
# - server_id: <DUID format> <LL address>
# The supported DUID formats are LL and LLT
#- server_id: LL 00:de:ad:be:ef:00
# file serves leases defined in a static file, matching link-layer addresses to IPs
# - file: <file name> [autorefresh]
# The file format is one lease per line, "<hw address> <IPv6>"
# When the 'autorefresh' argument is given, the plugin will try to refresh
# the lease mapping during runtime whenever the lease file is updated.
#- file: "leases.txt"
# dns adds information about available DNS resolvers to the responses
# - dns: <resolver IP> <... resolver IPs>
#- dns: 2001:4860:4860::8888 2001:4860:4860::8844
# nbp can add information about the location of a network boot program
# - nbp: <NBP URL>
#- nbp: "http://[2001:db8:a::1]/nbp"
# prefix provides prefix delegation.
# - prefix: <prefix> <allocation size>
# prefix is the prefix pool from which the allocations will be carved
# allocation size is the maximum size for prefixes that will be allocated to clients
# EG for allocating /64 or smaller prefixes within 2001:db8::/48 :
#- prefix: 2001:db8::/48 64
# DHCPv4 configuration
server4:
# listen is an optional section to specify how the server binds to an
# interface or address.
# If unset, the server will listen on the broadcast address on all
# interfaces, equivalent to:
## listen:
## - "0.0.0.0"
# In general, listen takes a list of addresses, with the general syntax
# "address%interface:port", where each part is optional.
# * Omitting the address results in the wildcard address being used
# * Omitting the interface skips binding the listener to a specific interface
# and listens on all interfaces instead
# * Omitting the port uses the default port for DHCPv4 (67)
#
# For example:
# - ":44480" Listens on a specific port.
# - "%eno1" Listens on the wildcard address on one interface.
# - "192.0.2.1%eno1:44480" with all parts
# plugins is a mandatory section, which defines how requests are handled.
# It is a list of maps, matching plugin names to their arguments.
# The order is meaningful, as incoming requests are handled by each plugin
# in turn. There is no default value for a plugin configuration, and a
# plugin that is not mentioned will not be loaded at all
#
# The following contains examples of the most common, builtin plugins.
# External plugins should document their arguments in their own
# documentations or readmes
plugins:
# lease_time sets the default lease time for advertised leases
# - lease_time: <duration>
# The duration can be given in any format understood by go's
# "ParseDuration": https://golang.org/pkg/time/#ParseDuration
- lease_time: 3600s
# server_id advertises a DHCP Server Identifier, to help resolve
# situations where there are multiple DHCP servers on the network
# - server_id: <IP address>
# The IP address should be one address where this server is reachable
- server_id: 10.10.10.1
# dns advertises DNS resolvers usable by the clients on this network
# - dns: <IP address> <...IP addresses>
- dns: 8.8.8.8 8.8.4.4
# router is mandatory, and advertises the address of the default router
# for this network
# - router: <IP address>
- router: 192.168.1.1
# netmask advertises the network mask for the IPs assigned through this
# server
# - netmask: <network mask>
- netmask: 255.255.255.0
# range allocates leases within a range of IPs
# - range: <lease file> <start IP> <end IP> <lease duration>
# * the lease file is an initially empty file where the leases that are
# allocated to clients will be stored across server restarts
# * lease duration can be given in any format understood by go's
# "ParseDuration": https://golang.org/pkg/time/#ParseDuration
- range: leases.txt 10.10.10.100 10.10.10.200 60s
# staticroute advertises additional routes the client should install in
# its routing table as described in RFC3442
# - staticroute: <destination>,<gateway> [<destination>,<gateway> ...]
# where destination should be in CIDR notation and gateway should be
# the IP address of the router through which the destination is reachable
# - staticroute: 10.20.20.0/24,10.10.10.1

View File

@@ -0,0 +1,167 @@
# CoreDHCP configuration (yaml)
# In this file, lines starting with "## " represent default values,
# while uncommented lines are examples which have no default value
# The base level configuration has two sections, one for each protocol version
# (DHCPv4 and DHCPv6). There is no shared configuration at the moment.
# At a high level, both accept the same structure of configuration
# DHCPv6 configuration
server6:
# listen is an optional section to specify how the server binds to an
# interface or address.
# If unset, the server will join the link-layer multicast group for all
# dhcp servers and relays on each interface, as well as the site-scoped
# multicast group for all dhcp servers.
# Note that in this default configuration the server will not handle
# unicast datagrams, and is equivalent to:
## listen:
## - "[ff02::1:2]"
## - "[ff05::1:3]"
# In general, listen takes a list of addresses, with the general syntax
# "[address%interface]:port", where each part is optional.
# Omitting the address results in the wildcard address being used
# Omitting the interface skips binding the listener to a specific interface
# and listens on all interfaces instead
# Omitting the port uses the default port for DHCPv6 (547)
#
# For example:
# - "[::]"
# Listen on the wildcard address on all interfaces on the default port.
# Note that no multicast group will be joined, so this will *not* work with
# most clients
#
# - ":44480"
# Listens on the wildcard address on a specific port. This can be used if
# you have a relay setup that can contact this server using unicast
#
# - "[::%eno1]"
# Listens on the wildcard address on one interface. This can be used if you
# want to spawn multiple servers with different configurations for multiple
# interfaces, behind a relay that can use unicast
#
# There are some special considerations for multicast:
# - "[ff02::1:2%eno1]"
# Listens on a link-layer multicast address bound to an interface. Also
# used to spawn multiple servers, but for clients on the same subnet
#
# - "[ff05::1:3%eno1]"
# Joining a multicast group with an interface allows to skip the default
# routing table when responding to clients, which can be useful if
# multicast is not otherwise configured system-wide
#
# - "[ff02::1:2]"
# Using a multicast address without an interface will be auto-expanded, so
# that it listens on all available interfaces
# plugins is a mandatory section, which defines how requests are handled.
# It is a list of maps, matching plugin names to their arguments.
# The order is meaningful, as incoming requests are handled by each plugin
# in turn. There is no default value for a plugin configuration, and a
# plugin that is not mentioned will not be loaded at all
#
# The following contains examples of the most common, builtin plugins.
# External plugins should document their arguments in their own
# documentations or readmes
plugins:
# server_id is mandatory for RFC-compliant operation.
# - server_id: <DUID format> <LL address>
# The supported DUID formats are LL and LLT
- server_id: LL 00:de:ad:be:ef:00
# file serves leases defined in a static file, matching link-layer addresses to IPs
# - file: <file name> [autorefresh]
# The file format is one lease per line, "<hw address> <IPv6>"
# When the 'autorefresh' argument is given, the plugin will try to refresh
# the lease mapping during runtime whenever the lease file is updated.
- file: "leases.txt"
# dns adds information about available DNS resolvers to the responses
# - dns: <resolver IP> <... resolver IPs>
- dns: 2001:4860:4860::8888 2001:4860:4860::8844
# nbp can add information about the location of a network boot program
# - nbp: <NBP URL>
- nbp: "http://[2001:db8:a::1]/nbp"
# prefix provides prefix delegation.
# - prefix: <prefix> <allocation size>
# prefix is the prefix pool from which the allocations will be carved
# allocation size is the maximum size for prefixes that will be allocated to clients
# EG for allocating /64 or smaller prefixes within 2001:db8::/48 :
- prefix: 2001:db8::/48 64
# DHCPv4 configuration
server4:
# listen is an optional section to specify how the server binds to an
# interface or address.
# If unset, the server will listen on the broadcast address on all
# interfaces, equivalent to:
## listen:
## - "0.0.0.0"
# In general, listen takes a list of addresses, with the general syntax
# "address%interface:port", where each part is optional.
# * Omitting the address results in the wildcard address being used
# * Omitting the interface skips binding the listener to a specific interface
# and listens on all interfaces instead
# * Omitting the port uses the default port for DHCPv4 (67)
#
# For example:
# - ":44480" Listens on a specific port.
# - "%eno1" Listens on the wildcard address on one interface.
# - "192.0.2.1%eno1:44480" with all parts
# plugins is a mandatory section, which defines how requests are handled.
# It is a list of maps, matching plugin names to their arguments.
# The order is meaningful, as incoming requests are handled by each plugin
# in turn. There is no default value for a plugin configuration, and a
# plugin that is not mentioned will not be loaded at all
#
# The following contains examples of the most common, builtin plugins.
# External plugins should document their arguments in their own
# documentations or readmes
plugins:
# lease_time sets the default lease time for advertised leases
# - lease_time: <duration>
# The duration can be given in any format understood by go's
# "ParseDuration": https://golang.org/pkg/time/#ParseDuration
- lease_time: 3600s
# server_id advertises a DHCP Server Identifier, to help resolve
# situations where there are multiple DHCP servers on the network
# - server_id: <IP address>
# The IP address should be one address where this server is reachable
- server_id: 10.10.10.1
# dns advertises DNS resolvers usable by the clients on this network
# - dns: <IP address> <...IP addresses>
- dns: 8.8.8.8 8.8.4.4
# router is mandatory, and advertises the address of the default router
# for this network
# - router: <IP address>
- router: 192.168.1.1
# netmask advertises the network mask for the IPs assigned through this
# server
# - netmask: <network mask>
- netmask: 255.255.255.0
# range allocates leases within a range of IPs
# - range: <lease file> <start IP> <end IP> <lease duration>
# * the lease file is an initially empty file where the leases that are
# allocated to clients will be stored across server restarts
# * lease duration can be given in any format understood by go's
# "ParseDuration": https://golang.org/pkg/time/#ParseDuration
- range: leases.txt 10.10.10.100 10.10.10.200 60s
# staticroute advertises additional routes the client should install in
# its routing table as described in RFC3442
# - staticroute: <destination>,<gateway> [<destination>,<gateway> ...]
# where destination should be in CIDR notation and gateway should be
# the IP address of the router through which the destination is reachable
# - staticroute: 10.20.20.0/24,10.10.10.1

BIN
cmds/coredhcp/coredhcp Executable file

Binary file not shown.

View File

@@ -0,0 +1 @@
00:11:22:33:44:55 2001:2::1

BIN
cmds/coredhcp/leases.txt Normal file

Binary file not shown.

129
cmds/coredhcp/main.go Normal file
View File

@@ -0,0 +1,129 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// This is a generated file, edits should be made in the corresponding source file
// And this file regenerated using `coredhcp-generator --from core-plugins.txt`
package main
import (
"fmt"
"io"
"os"
"github.com/coredhcp/coredhcp/config"
grpcServer "github.com/coredhcp/coredhcp/grpc_server"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/server"
"github.com/coredhcp/coredhcp/plugins"
pl_autoconfigure "github.com/coredhcp/coredhcp/plugins/autoconfigure"
pl_dns "github.com/coredhcp/coredhcp/plugins/dns"
pl_file "github.com/coredhcp/coredhcp/plugins/file"
pl_ipv6only "github.com/coredhcp/coredhcp/plugins/ipv6only"
pl_leasetime "github.com/coredhcp/coredhcp/plugins/leasetime"
pl_mtu "github.com/coredhcp/coredhcp/plugins/mtu"
pl_nbp "github.com/coredhcp/coredhcp/plugins/nbp"
pl_netmask "github.com/coredhcp/coredhcp/plugins/netmask"
pl_prefix "github.com/coredhcp/coredhcp/plugins/prefix"
pl_range "github.com/coredhcp/coredhcp/plugins/range"
pl_router "github.com/coredhcp/coredhcp/plugins/router"
pl_searchdomains "github.com/coredhcp/coredhcp/plugins/searchdomains"
pl_serverid "github.com/coredhcp/coredhcp/plugins/serverid"
pl_sleep "github.com/coredhcp/coredhcp/plugins/sleep"
pl_staticroute "github.com/coredhcp/coredhcp/plugins/staticroute"
"github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
)
var (
flagLogFile = flag.StringP("logfile", "l", "", "Name of the log file to append to. Default: stdout/stderr only")
flagLogNoStdout = flag.BoolP("nostdout", "N", false, "Disable logging to stdout/stderr")
flagLogLevel = flag.StringP("loglevel", "L", "info", fmt.Sprintf("Log level. One of %v", getLogLevels()))
flagConfig = flag.StringP("conf", "c", "", "Use this configuration file instead of the default location")
flagPlugins = flag.BoolP("plugins", "P", false, "list plugins")
)
var logLevels = map[string]func(*logrus.Logger){
"none": func(l *logrus.Logger) { l.SetOutput(io.Discard) },
"debug": func(l *logrus.Logger) { l.SetLevel(logrus.DebugLevel) },
"info": func(l *logrus.Logger) { l.SetLevel(logrus.InfoLevel) },
"warning": func(l *logrus.Logger) { l.SetLevel(logrus.WarnLevel) },
"error": func(l *logrus.Logger) { l.SetLevel(logrus.ErrorLevel) },
"fatal": func(l *logrus.Logger) { l.SetLevel(logrus.FatalLevel) },
}
func getLogLevels() []string {
var levels []string
for k := range logLevels {
levels = append(levels, k)
}
return levels
}
var desiredPlugins = []*plugins.Plugin{
&pl_autoconfigure.Plugin,
&pl_dns.Plugin,
&pl_file.Plugin,
&pl_ipv6only.Plugin,
&pl_leasetime.Plugin,
&pl_mtu.Plugin,
&pl_nbp.Plugin,
&pl_netmask.Plugin,
&pl_prefix.Plugin,
&pl_range.Plugin,
&pl_router.Plugin,
&pl_searchdomains.Plugin,
&pl_serverid.Plugin,
&pl_sleep.Plugin,
&pl_staticroute.Plugin,
}
func main() {
flag.Parse()
if *flagPlugins {
for _, p := range desiredPlugins {
fmt.Println(p.Name)
}
os.Exit(0)
}
log := logger.GetLogger("main")
fn, ok := logLevels[*flagLogLevel]
if !ok {
log.Fatalf("Invalid log level '%s'. Valid log levels are %v", *flagLogLevel, getLogLevels())
}
fn(log.Logger)
log.Infof("Setting log level to '%s'", *flagLogLevel)
if *flagLogFile != "" {
log.Infof("Logging to file %s", *flagLogFile)
logger.WithFile(log, *flagLogFile)
}
if *flagLogNoStdout {
log.Infof("Disabling logging to stdout/stderr")
logger.WithNoStdOutErr(log)
}
config, err := config.Load(*flagConfig)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// register plugins
for _, plugin := range desiredPlugins {
if err := plugins.RegisterPlugin(plugin); err != nil {
log.Fatalf("Failed to register plugin '%s': %v", plugin.Name, err)
}
}
go grpcServer.Run()
// start server
srv, err := server.Start(config)
if err != nil {
log.Fatal(err)
}
if err := srv.Wait(); err != nil {
log.Error(err)
}
}

341
config/config.go Normal file
View File

@@ -0,0 +1,341 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package config
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/coredhcp/coredhcp/logger"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/spf13/cast"
"github.com/spf13/viper"
)
var log = logger.GetLogger("config")
type protocolVersion int
const (
protocolV6 protocolVersion = 6
protocolV4 protocolVersion = 4
)
// Config holds the DHCPv6/v4 server configuration
type Config struct {
v *viper.Viper
Server6 *ServerConfig
Server4 *ServerConfig
}
// New returns a new initialized instance of a Config object
func New() *Config {
return &Config{v: viper.New()}
}
// ServerConfig holds a server configuration that is specific to either the
// DHCPv6 server or the DHCPv4 server.
type ServerConfig struct {
Addresses []net.UDPAddr
Plugins []PluginConfig
}
// PluginConfig holds the configuration of a plugin
type PluginConfig struct {
Name string
Args []string
}
// Load reads a configuration file and returns a Config object, or an error if
// any.
func Load(pathOverride string) (*Config, error) {
log.Print("Loading configuration")
c := New()
c.v.SetConfigType("yml")
if pathOverride != "" {
c.v.SetConfigFile(pathOverride)
} else {
c.v.SetConfigName("config")
c.v.AddConfigPath(".")
c.v.AddConfigPath("$XDG_CONFIG_HOME/coredhcp/")
c.v.AddConfigPath("$HOME/.coredhcp/")
c.v.AddConfigPath("/etc/coredhcp/")
}
if err := c.v.ReadInConfig(); err != nil {
return nil, err
}
if err := c.parseConfig(protocolV6); err != nil {
return nil, err
}
if err := c.parseConfig(protocolV4); err != nil {
return nil, err
}
if c.Server6 == nil && c.Server4 == nil {
return nil, ConfigErrorFromString("need at least one valid config for DHCPv6 or DHCPv4")
}
return c, nil
}
func protoVersionCheck(v protocolVersion) error {
if v != protocolV6 && v != protocolV4 {
return fmt.Errorf("invalid protocol version: %d", v)
}
return nil
}
func parsePlugins(pluginList []interface{}) ([]PluginConfig, error) {
plugins := make([]PluginConfig, 0, len(pluginList))
for idx, val := range pluginList {
conf := cast.ToStringMap(val)
if conf == nil {
return nil, ConfigErrorFromString("dhcpv6: plugin #%d is not a string map", idx)
}
// make sure that only one item is specified, since it's a
// map name -> args
if len(conf) != 1 {
return nil, ConfigErrorFromString("dhcpv6: exactly one plugin per item can be specified")
}
var (
name string
args []string
)
// only one item, as enforced above, so read just that
for k, v := range conf {
name = k
args = strings.Fields(cast.ToString(v))
break
}
plugins = append(plugins, PluginConfig{Name: name, Args: args})
}
return plugins, nil
}
// BUG(Natolumin): listen specifications of the form `[ip6]%iface:port` or
// `[ip6]%iface` are not supported, even though they are the default format of
// the `ss` utility in linux. Use `[ip6%iface]:port` instead
// splitHostPort splits an address of the form ip%zone:port into ip,zone and port.
// It still returns if any of these are unset (unlike net.SplitHostPort which
// returns an error if there is no port)
func splitHostPort(hostport string) (ip string, zone string, port string, err error) {
ip, port, err = net.SplitHostPort(hostport)
if err != nil {
// Either there is no port, or a more serious error.
// Supply a synthetic port to differentiate cases
var altErr error
if ip, _, altErr = net.SplitHostPort(hostport + ":0"); altErr != nil {
// Invalid even with a fake port. Return the original error
return
}
err = nil
}
if i := strings.LastIndexByte(ip, '%'); i >= 0 {
ip, zone = ip[:i], ip[i+1:]
}
return
}
func (c *Config) getListenAddress(addr string, ver protocolVersion) (*net.UDPAddr, error) {
if err := protoVersionCheck(ver); err != nil {
return nil, err
}
ipStr, ifname, portStr, err := splitHostPort(addr)
if err != nil {
return nil, ConfigErrorFromString("dhcpv%d: %v", ver, err)
}
ip := net.ParseIP(ipStr)
if ipStr == "" {
switch ver {
case protocolV4:
ip = net.IPv4zero
case protocolV6:
ip = net.IPv6unspecified
default:
panic("BUG: Unknown protocol version")
}
}
if ip == nil {
return nil, ConfigErrorFromString("dhcpv%d: invalid IP address in `listen` directive: %s", ver, ipStr)
}
if ip4 := ip.To4(); (ver == protocolV6 && ip4 != nil) || (ver == protocolV4 && ip4 == nil) {
return nil, ConfigErrorFromString("dhcpv%d: not a valid IPv%d address in `listen` directive: '%s'", ver, ver, ipStr)
}
var port int
if portStr == "" {
switch ver {
case protocolV4:
port = dhcpv4.ServerPort
case protocolV6:
port = dhcpv6.DefaultServerPort
default:
panic("BUG: Unknown protocol version")
}
} else {
port, err = strconv.Atoi(portStr)
if err != nil {
return nil, ConfigErrorFromString("dhcpv%d: invalid `listen` port '%s'", ver, portStr)
}
}
listener := net.UDPAddr{
IP: ip,
Port: port,
Zone: ifname,
}
return &listener, nil
}
func (c *Config) getPlugins(ver protocolVersion) ([]PluginConfig, error) {
if err := protoVersionCheck(ver); err != nil {
return nil, err
}
pluginList := cast.ToSlice(c.v.Get(fmt.Sprintf("server%d.plugins", ver)))
if pluginList == nil {
return nil, ConfigErrorFromString("dhcpv%d: invalid plugins section, not a list or no plugin specified", ver)
}
return parsePlugins(pluginList)
}
func (c *Config) parseConfig(ver protocolVersion) error {
if err := protoVersionCheck(ver); err != nil {
return err
}
if exists := c.v.Get(fmt.Sprintf("server%d", ver)); exists == nil {
// it is valid to have no server configuration defined
return nil
}
// read plugin configuration
plugins, err := c.getPlugins(ver)
if err != nil {
return err
}
for _, p := range plugins {
log.Printf("DHCPv%d: found plugin `%s` with %d args: %v", ver, p.Name, len(p.Args), p.Args)
}
listeners, err := c.parseListen(ver)
if err != nil {
return err
}
sc := ServerConfig{
Addresses: listeners,
Plugins: plugins,
}
if ver == protocolV6 {
c.Server6 = &sc
} else if ver == protocolV4 {
c.Server4 = &sc
}
return nil
}
// BUG(Natolumin): When listening on link-local multicast addresses without
// binding to a specific interface, new interfaces coming up after the server
// starts will not be taken into account.
func expandLLMulticast(addr *net.UDPAddr) ([]net.UDPAddr, error) {
if !addr.IP.IsLinkLocalMulticast() && !addr.IP.IsInterfaceLocalMulticast() {
return nil, errors.New("Address is not multicast")
}
if addr.Zone != "" {
return nil, errors.New("Address is already zoned")
}
var needFlags = net.FlagMulticast
if addr.IP.To4() != nil {
// We need to be able to send broadcast responses in ipv4
needFlags |= net.FlagBroadcast
}
ifs, err := net.Interfaces()
ret := make([]net.UDPAddr, 0, len(ifs))
if err != nil {
return nil, fmt.Errorf("Could not list network interfaces: %v", err)
}
for _, iface := range ifs {
if (iface.Flags & needFlags) != needFlags {
continue
}
caddr := *addr
caddr.Zone = iface.Name
ret = append(ret, caddr)
}
if len(ret) == 0 {
return nil, errors.New("No suitable interface found for multicast listener")
}
return ret, nil
}
func defaultListen(ver protocolVersion) ([]net.UDPAddr, error) {
switch ver {
case protocolV4:
return []net.UDPAddr{{Port: dhcpv4.ServerPort}}, nil
case protocolV6:
l, err := expandLLMulticast(&net.UDPAddr{IP: dhcpv6.AllDHCPRelayAgentsAndServers, Port: dhcpv6.DefaultServerPort})
if err != nil {
return nil, err
}
l = append(l,
net.UDPAddr{IP: dhcpv6.AllDHCPServers, Port: dhcpv6.DefaultServerPort},
// XXX: Do we want to listen on [::] as default ?
)
return l, nil
}
return nil, errors.New("defaultListen: Incorrect protocol version")
}
func (c *Config) parseListen(ver protocolVersion) ([]net.UDPAddr, error) {
if err := protoVersionCheck(ver); err != nil {
return nil, err
}
listen := c.v.Get(fmt.Sprintf("server%d.listen", ver))
// Provide an emulation of the old keyword "interface" to avoid breaking config files
if iface := c.v.Get(fmt.Sprintf("server%d.interface", ver)); iface != nil && listen != nil {
return nil, ConfigErrorFromString("interface is a deprecated alias for listen, " +
"both cannot be used at the same time. Choose one and remove the other.")
} else if iface != nil {
listen = "%" + cast.ToString(iface)
}
if listen == nil {
return defaultListen(ver)
}
addrs, err := cast.ToStringSliceE(listen)
if err != nil {
addrs = []string{cast.ToString(listen)}
}
listeners := []net.UDPAddr{}
for _, a := range addrs {
l, err := c.getListenAddress(a, ver)
if err != nil {
return nil, err
}
if l.Zone == "" && (l.IP.IsLinkLocalMulticast() || l.IP.IsInterfaceLocalMulticast()) {
// link-local multicast specified without interface gets expanded to listen on all interfaces
expanded, err := expandLLMulticast(l)
if err != nil {
return nil, err
}
listeners = append(listeners, expanded...)
continue
}
listeners = append(listeners, *l)
}
return listeners, nil
}

52
config/config_test.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package config
import "testing"
func TestSplitHostPort(t *testing.T) {
testcases := []struct {
hostport string
ip string
zone string
port string
err bool // Should return an error (ie true for err != nil)
}{
{"0.0.0.0:67", "0.0.0.0", "", "67", false},
{"192.0.2.0", "192.0.2.0", "", "", false},
{"192.0.2.9%eth0", "192.0.2.9", "eth0", "", false},
{"0.0.0.0%eth0:67", "0.0.0.0", "eth0", "67", false},
{"0.0.0.0:20%eth0:67", "0.0.0.0", "eth0", "67", true},
{"2001:db8::1:547", "", "", "547", true}, // [] mandatory for v6
{"[::]:547", "::", "", "547", false},
{"[fe80::1%eth0]", "fe80::1", "eth0", "", false},
{"[fe80::1]:eth1", "fe80::1", "", "eth1", false}, // no validation of ports in this function
{"fe80::1%eth0:547", "fe80::1", "eth0", "547", true}, // [] mandatory for v6 even with %zone
{"fe80::1%eth0", "fe80::1", "eth0", "547", true}, // [] mandatory for v6 even without port
{"[2001:db8::2]47", "fe80::1", "eth0", "547", true}, // garbage after []
{"[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
{":http", "", "", "http", false},
{"%eth0:80", "", "eth0", "80", false}, // janky, but looks valid enough for "[::%eth0]:80" imo
{"%eth0", "", "eth0", "", false}, // janky
{"fe80::1]:80", "fe80::1", "", "80", true}, // unbalanced ]
{"fe80::1%eth0]", "fe80::1", "eth0", "", true}, // unbalanced ], no port
{"", "", "", "", false}, // trivial case, still valid
}
for _, tc := range testcases {
ip, zone, port, err := splitHostPort(tc.hostport)
if tc.err != (err != nil) {
errState := "not "
if tc.err {
errState = ""
}
t.Errorf("Mismatched error state: %s should %serror (got err: %v)\n", tc.hostport, errState, err)
continue
}
if err == nil && (ip != tc.ip || zone != tc.zone || port != tc.port) {
t.Errorf("%s => \"%s\", \"%s\", \"%s\" expected \"%s\",\"%s\",\"%s\"\n", tc.hostport, ip, zone, port, tc.ip, tc.zone, tc.port)
}
}
}

32
config/errors.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package config
import (
"fmt"
)
// ConfigError is an error type returned upon configuration errors.
type ConfigError struct {
err error
}
// ConfigErrorFromString returns a ConfigError from the given error string.
func ConfigErrorFromString(format string, args ...interface{}) *ConfigError {
return &ConfigError{
err: fmt.Errorf(format, args...),
}
}
// ConfigErrorFromError returns a ConfigError from the given error object.
func ConfigErrorFromError(err error) *ConfigError {
return &ConfigError{
err: err,
}
}
func (ce ConfigError) Error() string {
return fmt.Sprintf("error parsing config: %v", ce.err)
}

2
config/static_ip.csv Executable file
View File

@@ -0,0 +1,2 @@
42:9c:aa:1b:87:d5,172.16.11.20
d2:ab:13:8a:e8:15,172.16.11.21
1 42:9c:aa:1b:87:d5 172.16.11.20
2 d2:ab:13:8a:e8:15 172.16.11.21

55
go.mod Normal file
View File

@@ -0,0 +1,55 @@
module github.com/coredhcp/coredhcp
go 1.23.3
require (
github.com/bits-and-blooms/bitset v1.15.0
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb
github.com/fsnotify/fsnotify v1.8.0
github.com/google/gopacket v1.1.19
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475
github.com/mattn/go-sqlite3 v1.14.24
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/vishvananda/netns v0.0.5
golang.org/x/net v0.31.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.1
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.35.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

276
go.sum Normal file
View File

@@ -0,0 +1,276 @@
cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8=
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/bits-and-blooms/bitset v1.15.0 h1:DiCRMscZsGyYePE9AR3sVhKqUXCt5IZvkX5AfAc5xLQ=
github.com/bits-and-blooms/bitset v1.15.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg=
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb/go.mod h1:xzXc1S/L+64uglB3pw54o8kqyM6KFYpTeC9Q6+qZIu8=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4=
go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E=
go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,369 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2-devel
// protoc v3.12.4
// source: dhcp_server.proto
package dhcpServer
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type MacAddr struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Mac string `protobuf:"bytes,1,opt,name=mac,proto3" json:"mac,omitempty"`
}
func (x *MacAddr) Reset() {
*x = MacAddr{}
mi := &file_dhcp_server_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *MacAddr) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MacAddr) ProtoMessage() {}
func (x *MacAddr) ProtoReflect() protoreflect.Message {
mi := &file_dhcp_server_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MacAddr.ProtoReflect.Descriptor instead.
func (*MacAddr) Descriptor() ([]byte, []int) {
return file_dhcp_server_proto_rawDescGZIP(), []int{0}
}
func (x *MacAddr) GetMac() string {
if x != nil {
return x.Mac
}
return ""
}
// The response message containing the staInfo.
type StaDhcpInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"`
}
func (x *StaDhcpInfo) Reset() {
*x = StaDhcpInfo{}
mi := &file_dhcp_server_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StaDhcpInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StaDhcpInfo) ProtoMessage() {}
func (x *StaDhcpInfo) ProtoReflect() protoreflect.Message {
mi := &file_dhcp_server_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StaDhcpInfo.ProtoReflect.Descriptor instead.
func (*StaDhcpInfo) Descriptor() ([]byte, []int) {
return file_dhcp_server_proto_rawDescGZIP(), []int{1}
}
func (x *StaDhcpInfo) GetIp() string {
if x != nil {
return x.Ip
}
return ""
}
func (x *StaDhcpInfo) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
type EmptyRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *EmptyRequest) Reset() {
*x = EmptyRequest{}
mi := &file_dhcp_server_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EmptyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EmptyRequest) ProtoMessage() {}
func (x *EmptyRequest) ProtoReflect() protoreflect.Message {
mi := &file_dhcp_server_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EmptyRequest.ProtoReflect.Descriptor instead.
func (*EmptyRequest) Descriptor() ([]byte, []int) {
return file_dhcp_server_proto_rawDescGZIP(), []int{2}
}
// The response message containing the dhcpInfo.
type DhcpInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UeInfo []*UeInfo `protobuf:"bytes,1,rep,name=ueInfo,proto3" json:"ueInfo,omitempty"`
}
func (x *DhcpInfo) Reset() {
*x = DhcpInfo{}
mi := &file_dhcp_server_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DhcpInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DhcpInfo) ProtoMessage() {}
func (x *DhcpInfo) ProtoReflect() protoreflect.Message {
mi := &file_dhcp_server_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DhcpInfo.ProtoReflect.Descriptor instead.
func (*DhcpInfo) Descriptor() ([]byte, []int) {
return file_dhcp_server_proto_rawDescGZIP(), []int{3}
}
func (x *DhcpInfo) GetUeInfo() []*UeInfo {
if x != nil {
return x.UeInfo
}
return nil
}
type UeInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
Mac string `protobuf:"bytes,2,opt,name=mac,proto3" json:"mac,omitempty"`
Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"`
StartTime string `protobuf:"bytes,4,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime string `protobuf:"bytes,5,opt,name=endTime,proto3" json:"endTime,omitempty"`
}
func (x *UeInfo) Reset() {
*x = UeInfo{}
mi := &file_dhcp_server_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UeInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UeInfo) ProtoMessage() {}
func (x *UeInfo) ProtoReflect() protoreflect.Message {
mi := &file_dhcp_server_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UeInfo.ProtoReflect.Descriptor instead.
func (*UeInfo) Descriptor() ([]byte, []int) {
return file_dhcp_server_proto_rawDescGZIP(), []int{4}
}
func (x *UeInfo) GetIp() string {
if x != nil {
return x.Ip
}
return ""
}
func (x *UeInfo) GetMac() string {
if x != nil {
return x.Mac
}
return ""
}
func (x *UeInfo) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
func (x *UeInfo) GetStartTime() string {
if x != nil {
return x.StartTime
}
return ""
}
func (x *UeInfo) GetEndTime() string {
if x != nil {
return x.EndTime
}
return ""
}
var File_dhcp_server_proto protoreflect.FileDescriptor
var file_dhcp_server_proto_rawDesc = []byte{
0x0a, 0x11, 0x64, 0x68, 0x63, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x1b, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x12, 0x10,
0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63,
0x22, 0x39, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x44, 0x68, 0x63, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12,
0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x0e, 0x0a, 0x0c, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2b, 0x0a, 0x08, 0x44,
0x68, 0x63, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1f, 0x0a, 0x06, 0x75, 0x65, 0x49, 0x6e, 0x66,
0x6f, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x55, 0x65, 0x49, 0x6e, 0x66, 0x6f,
0x52, 0x06, 0x75, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x7e, 0x0a, 0x06, 0x55, 0x65, 0x49, 0x6e,
0x66, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x03, 0x6d, 0x61, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18,
0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x32, 0x30, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
0x12, 0x08, 0x2e, 0x4d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x1a, 0x0c, 0x2e, 0x53, 0x74, 0x61,
0x44, 0x68, 0x63, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x00, 0x32, 0x34, 0x0a, 0x0b, 0x44, 0x68,
0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x47, 0x65, 0x74,
0x44, 0x68, 0x63, 0x70, 0x12, 0x0d, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x44, 0x68, 0x63, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x00,
0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2f, 0x64, 0x68, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_dhcp_server_proto_rawDescOnce sync.Once
file_dhcp_server_proto_rawDescData = file_dhcp_server_proto_rawDesc
)
func file_dhcp_server_proto_rawDescGZIP() []byte {
file_dhcp_server_proto_rawDescOnce.Do(func() {
file_dhcp_server_proto_rawDescData = protoimpl.X.CompressGZIP(file_dhcp_server_proto_rawDescData)
})
return file_dhcp_server_proto_rawDescData
}
var file_dhcp_server_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_dhcp_server_proto_goTypes = []any{
(*MacAddr)(nil), // 0: MacAddr
(*StaDhcpInfo)(nil), // 1: StaDhcpInfo
(*EmptyRequest)(nil), // 2: EmptyRequest
(*DhcpInfo)(nil), // 3: DhcpInfo
(*UeInfo)(nil), // 4: UeInfo
}
var file_dhcp_server_proto_depIdxs = []int32{
4, // 0: DhcpInfo.ueInfo:type_name -> UeInfo
0, // 1: StaService.GetSta:input_type -> MacAddr
2, // 2: DhcpService.GetDhcp:input_type -> EmptyRequest
1, // 3: StaService.GetSta:output_type -> StaDhcpInfo
3, // 4: DhcpService.GetDhcp:output_type -> DhcpInfo
3, // [3:5] is the sub-list for method output_type
1, // [1:3] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_dhcp_server_proto_init() }
func file_dhcp_server_proto_init() {
if File_dhcp_server_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_dhcp_server_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 2,
},
GoTypes: file_dhcp_server_proto_goTypes,
DependencyIndexes: file_dhcp_server_proto_depIdxs,
MessageInfos: file_dhcp_server_proto_msgTypes,
}.Build()
File_dhcp_server_proto = out.File
file_dhcp_server_proto_rawDesc = nil
file_dhcp_server_proto_goTypes = nil
file_dhcp_server_proto_depIdxs = nil
}

View File

@@ -0,0 +1,231 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v3.12.4
// source: dhcp_server.proto
package dhcpServer
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
StaService_GetSta_FullMethodName = "/StaService/GetSta"
)
// StaServiceClient is the client API for StaService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// The get sta service definition.
type StaServiceClient interface {
GetSta(ctx context.Context, in *MacAddr, opts ...grpc.CallOption) (*StaDhcpInfo, error)
}
type staServiceClient struct {
cc grpc.ClientConnInterface
}
func NewStaServiceClient(cc grpc.ClientConnInterface) StaServiceClient {
return &staServiceClient{cc}
}
func (c *staServiceClient) GetSta(ctx context.Context, in *MacAddr, opts ...grpc.CallOption) (*StaDhcpInfo, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StaDhcpInfo)
err := c.cc.Invoke(ctx, StaService_GetSta_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// StaServiceServer is the server API for StaService service.
// All implementations must embed UnimplementedStaServiceServer
// for forward compatibility.
//
// The get sta service definition.
type StaServiceServer interface {
GetSta(context.Context, *MacAddr) (*StaDhcpInfo, error)
mustEmbedUnimplementedStaServiceServer()
}
// UnimplementedStaServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedStaServiceServer struct{}
func (UnimplementedStaServiceServer) GetSta(context.Context, *MacAddr) (*StaDhcpInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSta not implemented")
}
func (UnimplementedStaServiceServer) mustEmbedUnimplementedStaServiceServer() {}
func (UnimplementedStaServiceServer) testEmbeddedByValue() {}
// UnsafeStaServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to StaServiceServer will
// result in compilation errors.
type UnsafeStaServiceServer interface {
mustEmbedUnimplementedStaServiceServer()
}
func RegisterStaServiceServer(s grpc.ServiceRegistrar, srv StaServiceServer) {
// If the following call panics, it indicates UnimplementedStaServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&StaService_ServiceDesc, srv)
}
func _StaService_GetSta_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MacAddr)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StaServiceServer).GetSta(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StaService_GetSta_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StaServiceServer).GetSta(ctx, req.(*MacAddr))
}
return interceptor(ctx, in, info, handler)
}
// StaService_ServiceDesc is the grpc.ServiceDesc for StaService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var StaService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "StaService",
HandlerType: (*StaServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetSta",
Handler: _StaService_GetSta_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "dhcp_server.proto",
}
const (
DhcpService_GetDhcp_FullMethodName = "/DhcpService/GetDhcp"
)
// DhcpServiceClient is the client API for DhcpService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// The get dhcp service definition.
type DhcpServiceClient interface {
GetDhcp(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*DhcpInfo, error)
}
type dhcpServiceClient struct {
cc grpc.ClientConnInterface
}
func NewDhcpServiceClient(cc grpc.ClientConnInterface) DhcpServiceClient {
return &dhcpServiceClient{cc}
}
func (c *dhcpServiceClient) GetDhcp(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*DhcpInfo, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DhcpInfo)
err := c.cc.Invoke(ctx, DhcpService_GetDhcp_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// DhcpServiceServer is the server API for DhcpService service.
// All implementations must embed UnimplementedDhcpServiceServer
// for forward compatibility.
//
// The get dhcp service definition.
type DhcpServiceServer interface {
GetDhcp(context.Context, *EmptyRequest) (*DhcpInfo, error)
mustEmbedUnimplementedDhcpServiceServer()
}
// UnimplementedDhcpServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedDhcpServiceServer struct{}
func (UnimplementedDhcpServiceServer) GetDhcp(context.Context, *EmptyRequest) (*DhcpInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDhcp not implemented")
}
func (UnimplementedDhcpServiceServer) mustEmbedUnimplementedDhcpServiceServer() {}
func (UnimplementedDhcpServiceServer) testEmbeddedByValue() {}
// UnsafeDhcpServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DhcpServiceServer will
// result in compilation errors.
type UnsafeDhcpServiceServer interface {
mustEmbedUnimplementedDhcpServiceServer()
}
func RegisterDhcpServiceServer(s grpc.ServiceRegistrar, srv DhcpServiceServer) {
// If the following call panics, it indicates UnimplementedDhcpServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&DhcpService_ServiceDesc, srv)
}
func _DhcpService_GetDhcp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EmptyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DhcpServiceServer).GetDhcp(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DhcpService_GetDhcp_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DhcpServiceServer).GetDhcp(ctx, req.(*EmptyRequest))
}
return interceptor(ctx, in, info, handler)
}
// DhcpService_ServiceDesc is the grpc.ServiceDesc for DhcpService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var DhcpService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "DhcpService",
HandlerType: (*DhcpServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetDhcp",
Handler: _DhcpService_GetDhcp_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "dhcp_server.proto",
}

View File

@@ -0,0 +1,54 @@
package grpcServer
import (
"context"
"net"
"google.golang.org/grpc"
"github.com/coredhcp/coredhcp/grpc_server/dhcpServer"
"github.com/coredhcp/coredhcp/logger"
rangeplugin "github.com/coredhcp/coredhcp/plugins/range"
)
var log = logger.GetLogger("grpc")
type staServer struct {
dhcpServer.UnimplementedStaServiceServer
}
func (s *staServer) GetSta(ctx context.Context, req *dhcpServer.MacAddr) (*dhcpServer.StaDhcpInfo, error) {
record := rangeplugin.GetRecord(req.Mac)
if record != nil {
return &dhcpServer.StaDhcpInfo{
Ip: record.IP.String(),
Hostname: record.Hostname,
}, nil
} else {
return nil, nil
}
}
type dhcpService struct {
dhcpServer.UnimplementedDhcpServiceServer
}
func (d *dhcpService) GetDhcp(ctx context.Context, req *dhcpServer.EmptyRequest) (dhcpInfo *dhcpServer.DhcpInfo, err error) {
return rangeplugin.GetDhcpInfo()
}
func Run() {
lis, err := net.Listen("tcp", "127.0.0.1:50051")
if err != nil {
log.Errorf("failed to listen: %v", err)
}
s := grpc.NewServer()
dhcpServer.RegisterStaServiceServer(s, &staServer{})
dhcpServer.RegisterDhcpServiceServer(s, &dhcpService{})
if err := s.Serve(lis); err != nil {
log.Errorf("failed to serve: %v", err)
}
}

23
handler/handler.go Normal file
View File

@@ -0,0 +1,23 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package handler
import (
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
// Handler6 is a function that is called on a given DHCPv6 packet.
// It returns a DHCPv6 packet and a boolean.
// If the boolean is true, this will be the last handler to be called.
// The two input packets are the original request, and a response packet.
// The response packet may or may not be modified by the function, and
// the result will be returned by the handler.
// If the returned boolean is true, the returned packet may be nil or
// invalid, in which case no response will be sent.
type Handler6 func(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool)
// Handler4 behaves like Handler6, but for DHCPv4 packets.
type Handler4 func(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool)

View File

@@ -0,0 +1 @@
de:ad:be:ef:00:00 2001:db8::10:1

129
integ/server6/server6.go Normal file
View File

@@ -0,0 +1,129 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
//go:build integration
// +build integration
package main
import (
"fmt"
"log"
"net"
"runtime"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/client6"
"github.com/insomniacslk/dhcp/iana"
"github.com/vishvananda/netns"
"github.com/coredhcp/coredhcp/config"
"github.com/coredhcp/coredhcp/plugins"
"github.com/coredhcp/coredhcp/server"
// Plugins
"github.com/coredhcp/coredhcp/plugins/file"
"github.com/coredhcp/coredhcp/plugins/serverid"
)
var serverConfig = config.Config{
Server6: &config.ServerConfig{
Addresses: []net.UDPAddr{
{
IP: net.ParseIP("ff02::1:2"),
Port: dhcpv6.DefaultServerPort,
Zone: "cdhcp_srv",
},
},
Plugins: []config.PluginConfig{
{Name: "server_id", Args: []string{"LL", "11:22:33:44:55:66"}},
{Name: "file", Args: []string{"./leases-dhcpv6-test.txt"}},
},
},
}
// This function *must* be run in its own routine
// For now this assumes ns are created outside.
// TODO: dynamically create NS and interfaces directly in the test program
func runServer(readyCh chan<- struct{}, nsName string, desiredPlugins []*plugins.Plugin) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ns, err := netns.GetFromName(nsName)
if err != nil {
log.Panicf("Netns `%s` not set up: %v", nsName, err)
}
if err := netns.Set(ns); err != nil {
log.Panicf("Failed to switch to netns `%s`: %v", nsName, err)
}
// register plugins
for _, pl := range desiredPlugins {
if err := plugins.RegisterPlugin(pl); err != nil {
log.Panicf("Failed to register plugin `%s`: %v", pl.Name, err)
}
}
// start DHCP server
srv, err := server.Start(&serverConfig)
if err != nil {
log.Panicf("Server could not start: %v", err)
}
readyCh <- struct{}{}
if err := srv.Wait(); err != nil {
log.Panicf("Server errored during run: %v", err)
}
}
// runInNs will execute the provided cmd in the namespace nsName.
// It returns the error status of the cmd. Errors in NS management will panic
func runClient6(nsName, iface string, modifiers ...dhcpv6.Modifier) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
backupNS, err := netns.Get()
if err != nil {
panic("Could not save handle to original NS")
}
ns, err := netns.GetFromName(nsName)
if err != nil {
panic("netns not set up")
}
if err := netns.Set(ns); err != nil {
panic(fmt.Sprintf("Couldn't switch to test NS: %v", err))
}
client := client6.NewClient()
_, cErr := client.Exchange(iface, modifiers...)
if netns.Set(backupNS) != nil {
panic("couldn't switch back to original NS")
}
return cErr
}
// Create a server and run a DORA exchange with it
func main() {
readyCh := make(chan struct{}, 1)
go runServer(readyCh,
"coredhcp-direct-upper",
[]*plugins.Plugin{
&serverid.Plugin, &file.Plugin,
},
)
// wait for server to be ready before sending DHCP request
<-readyCh
mac, err := net.ParseMAC("de:ad:be:ef:00:00")
if err != nil {
panic(err)
}
err = runClient6(
"coredhcp-direct-lower", "cdhcp_cli",
dhcpv6.WithClientID(&dhcpv6.DUIDLL{
HWType: iana.HWTypeEthernet,
LinkLayerAddr: mac,
}),
)
if err != nil {
panic(err)
}
}

46
logger/logger.go Normal file
View File

@@ -0,0 +1,46 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package logger
import (
"io"
"sync"
log_prefixed "github.com/chappjc/logrus-prefix"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
)
var (
globalLogger *logrus.Logger
getLoggerMutex sync.Mutex
)
// GetLogger returns a configured logger instance
func GetLogger(prefix string) *logrus.Entry {
if prefix == "" {
prefix = "<no prefix>"
}
if globalLogger == nil {
getLoggerMutex.Lock()
defer getLoggerMutex.Unlock()
logger := logrus.New()
logger.SetFormatter(&log_prefixed.TextFormatter{
FullTimestamp: true,
})
globalLogger = logger
}
return globalLogger.WithField("prefix", prefix)
}
// WithFile logs to the specified file in addition to the existing output.
func WithFile(log *logrus.Entry, logfile string) {
log.Logger.AddHook(lfshook.NewHook(logfile, &logrus.TextFormatter{}))
}
// WithNoStdOutErr disables logging to stdout/stderr.
func WithNoStdOutErr(log *logrus.Entry) {
log.Logger.SetOutput(io.Discard)
}

View File

@@ -0,0 +1,48 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// Package allocators provides the interface and the algorithm(s) for allocation of ipv6
// prefixes of various sizes within a larger prefix.
// There are many many parallels with memory allocation.
package allocators
import (
"errors"
"fmt"
"net"
)
// Allocator is the interface to the address allocator. It only finds and
// allocates blocks and is not concerned with lease-specific questions like
// expiration (ie garbage collection needs to be handled separately)
type Allocator interface {
// Allocate finds a suitable prefix of the given size and returns it.
//
// hint is a prefix, which the client desires especially, and that the
// allocator MAY try to return; the allocator SHOULD try to return a prefix of
// the same size as the given hint prefix. The allocator MUST NOT return an
// error if a prefix was successfully assigned, even if the prefix has nothing
// in common with the hinted prefix
Allocate(hint net.IPNet) (net.IPNet, error)
// Free returns the prefix containing the given network to the pool
//
// Free may return a DoubleFreeError if the prefix being returned was not
// previously allocated
Free(net.IPNet) error
}
// ErrDoubleFree is an error type returned by Allocator.Free() when a
// non-allocated block is passed
type ErrDoubleFree struct {
Loc net.IPNet
}
// String returns a human-readable error message for a DoubleFree error
func (err *ErrDoubleFree) Error() string {
return fmt.Sprint("Attempted to free unallocated block at ", err.Loc.String())
}
// ErrNoAddrAvail is returned when we can't allocate an IP because there's no unallocated space left
var ErrNoAddrAvail = errors.New("no address available to allocate")

View File

@@ -0,0 +1,135 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// This allocator only returns prefixes of a single size
// This is much simpler to implement (reduces the problem to an equivalent of
// single ip allocations), probably makes sense in cases where the available
// range is much larger than the expected number of clients. Also is what KEA
// does so at least it's not worse than that
package bitmap
import (
"errors"
"fmt"
"net"
"strconv"
"sync"
"github.com/bits-and-blooms/bitset"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins/allocators"
)
var log = logger.GetLogger("plugins/allocators/bitmap")
// Allocator is a prefix allocator allocating in chunks of a fixed size
// regardless of the size requested by the client.
// It consumes an amount of memory proportional to the total amount of available prefixes
type Allocator struct {
containing net.IPNet
page int
bitmap *bitset.BitSet
l sync.Mutex
}
// prefix must verify: containing.Mask.Size < prefix.Mask.Size < page
func (a *Allocator) toIndex(base net.IP) (uint, error) {
value, err := allocators.Offset(base, a.containing.IP, a.page)
if err != nil {
return 0, fmt.Errorf("Cannot compute prefix index: %w", err)
}
return uint(value), nil
}
func (a *Allocator) toPrefix(idx uint) (net.IP, error) {
return allocators.AddPrefixes(a.containing.IP, uint64(idx), uint64(a.page))
}
// Allocate reserves a maxsize-sized block and returns a block of size
// min(maxsize, hint.size)
func (a *Allocator) Allocate(hint net.IPNet) (ret net.IPNet, err error) {
// Ensure size is max(maxsize, hint.size)
reqSize, hintErr := hint.Mask.Size()
if reqSize < a.page || hintErr != 128 {
reqSize = a.page
}
ret.Mask = net.CIDRMask(reqSize, 128)
// Try to allocate the requested prefix
a.l.Lock()
defer a.l.Unlock()
if hint.IP.To16() != nil && a.containing.Contains(hint.IP) {
idx, hintErr := a.toIndex(hint.IP)
if hintErr == nil && !a.bitmap.Test(idx) {
a.bitmap.Set(idx)
ret.IP, err = a.toPrefix(idx)
return
}
}
// Find a free prefix
next, ok := a.bitmap.NextClear(0)
if !ok {
err = allocators.ErrNoAddrAvail
return
}
a.bitmap.Set(next)
ret.IP, err = a.toPrefix(next)
if err != nil {
// This violates the assumption that every index in the bitmap maps back to a valid prefix
err = fmt.Errorf("BUG: could not get prefix from allocation: %w", err)
a.bitmap.Clear(next)
}
return
}
// Free returns the given prefix to the available pool if it was taken.
func (a *Allocator) Free(prefix net.IPNet) error {
idx, err := a.toIndex(prefix.IP.Mask(prefix.Mask))
if err != nil {
return fmt.Errorf("Could not find prefix in pool: %w", err)
}
a.l.Lock()
defer a.l.Unlock()
if !a.bitmap.Test(idx) {
return &allocators.ErrDoubleFree{Loc: prefix}
}
a.bitmap.Clear(idx)
return nil
}
// NewBitmapAllocator creates a new allocator, allocating /`size` prefixes
// carved out of the given `pool` prefix
func NewBitmapAllocator(pool net.IPNet, size int) (*Allocator, error) {
poolSize, _ := pool.Mask.Size()
allocOrder := size - poolSize
if allocOrder < 0 {
return nil, errors.New("The size of allocated prefixes cannot be larger than the pool they're allocated from")
} else if allocOrder >= strconv.IntSize {
return nil, fmt.Errorf("A pool with more than 2^%d items is not representable", size-poolSize)
} else if allocOrder >= 32 {
log.Warningln("Using a pool of more than 2^32 elements may result in large memory consumption")
}
if !(1<<uint(allocOrder) <= bitset.Cap()) {
return nil, errors.New("Can't fit this pool using the bitmap allocator")
}
alloc := Allocator{
containing: pool,
page: size,
bitmap: bitset.New(1 << uint(allocOrder)),
}
return &alloc, nil
}

View File

@@ -0,0 +1,122 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package bitmap
// This allocator handles IPv4 assignments with a similar logic to the base bitmap, but a simpler
// implementation due to the ability to just use uint32 for IPv4 addresses
import (
"encoding/binary"
"errors"
"fmt"
"net"
"sync"
"github.com/bits-and-blooms/bitset"
"github.com/coredhcp/coredhcp/plugins/allocators"
)
var (
errNotInRange = errors.New("IPv4 address outside of allowed range")
errInvalidIP = errors.New("invalid IPv4 address passed as input")
)
// IPv4Allocator allocates IPv4 addresses, tracking utilization with a bitmap
type IPv4Allocator struct {
start uint32
end uint32
// This bitset implementation isn't goroutine-safe, we protect it with a mutex for now
// until we can swap for another concurrent implementation
bitmap *bitset.BitSet
l sync.Mutex
}
func (a *IPv4Allocator) toIP(offset uint32) net.IP {
if offset > a.end-a.start {
panic("BUG: offset out of bounds")
}
r := make(net.IP, net.IPv4len)
binary.BigEndian.PutUint32(r, a.start+offset)
return r
}
func (a *IPv4Allocator) toOffset(ip net.IP) (uint, error) {
if ip.To4() == nil {
return 0, errInvalidIP
}
intIP := binary.BigEndian.Uint32(ip.To4())
if intIP < a.start || intIP > a.end {
return 0, errNotInRange
}
return uint(intIP - a.start), nil
}
// Allocate reserves an IP for a client
func (a *IPv4Allocator) Allocate(hint net.IPNet) (n net.IPNet, err error) {
n.Mask = net.CIDRMask(32, 32)
// This is just a hint, ignore any error with it
hintOffset, _ := a.toOffset(hint.IP)
a.l.Lock()
defer a.l.Unlock()
var next uint
// First try the exact match
if !a.bitmap.Test(hintOffset) {
next = hintOffset
} else {
// Then any available address
avail, ok := a.bitmap.NextClear(0)
if !ok {
return n, allocators.ErrNoAddrAvail
}
next = avail
}
a.bitmap.Set(next)
n.IP = a.toIP(uint32(next))
return
}
// Free releases the given IP
func (a *IPv4Allocator) Free(n net.IPNet) error {
offset, err := a.toOffset(n.IP)
if err != nil {
return errNotInRange
}
a.l.Lock()
defer a.l.Unlock()
if !a.bitmap.Test(uint(offset)) {
return &allocators.ErrDoubleFree{Loc: n}
}
a.bitmap.Clear(offset)
return nil
}
// NewIPv4Allocator creates a new allocator suitable for giving out IPv4 addresses
func NewIPv4Allocator(start, end net.IP) (*IPv4Allocator, error) {
if start.To4() == nil || end.To4() == nil {
return nil, fmt.Errorf("invalid IPv4 addresses given to create the allocator: [%s,%s]", start, end)
}
alloc := IPv4Allocator{
start: binary.BigEndian.Uint32(start.To4()),
end: binary.BigEndian.Uint32(end.To4()),
}
if alloc.start > alloc.end {
return nil, errors.New("no IPs in the given range to allocate")
}
alloc.bitmap = bitset.New(uint(alloc.end - alloc.start + 1))
return &alloc, nil
}

View File

@@ -0,0 +1,63 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package bitmap
import (
"net"
"testing"
)
func getv4Allocator() *IPv4Allocator {
alloc, err := NewIPv4Allocator(net.IPv4(192, 0, 2, 0), net.IPv4(192, 0, 2, 255))
if err != nil {
panic(err)
}
return alloc
}
func Test4Alloc(t *testing.T) {
alloc := getv4Allocator()
net1, err := alloc.Allocate(net.IPNet{})
if err != nil {
t.Fatal(err)
}
net2, err := alloc.Allocate(net.IPNet{})
if err != nil {
t.Fatal(err)
}
if net1.IP.Equal(net2.IP) {
t.Fatal("That address was already allocated")
}
err = alloc.Free(net1)
if err != nil {
t.Fatal(err)
}
err = alloc.Free(net1)
if err == nil {
t.Fatal("Expected DoubleFree error")
}
}
func Test4OutOfPool(t *testing.T) {
alloc := getv4Allocator()
hint := net.IPv4(198, 51, 100, 5)
res, err := alloc.Allocate(net.IPNet{IP: hint, Mask: net.CIDRMask(32, 32)})
if err != nil {
t.Fatalf("Failed to allocate with invalid hint: %v", err)
}
_, prefix, _ := net.ParseCIDR("192.0.2.0/24")
if !prefix.Contains(res.IP) {
t.Fatal("Obtained prefix outside of range: ", res)
}
if prefLen, totalLen := res.Mask.Size(); prefLen != 32 || totalLen != 32 {
t.Fatalf("Prefixes have wrong size %d/%d", prefLen, totalLen)
}
}

View File

@@ -0,0 +1,137 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package bitmap
import (
"math"
"math/rand"
"net"
"testing"
"github.com/bits-and-blooms/bitset"
)
func getAllocator(bits int) *Allocator {
_, prefix, err := net.ParseCIDR("2001:db8::/56")
if err != nil {
panic(err)
}
alloc, err := NewBitmapAllocator(*prefix, 56+bits)
if err != nil {
panic(err)
}
return alloc
}
func TestAlloc(t *testing.T) {
alloc := getAllocator(8)
net, err := alloc.Allocate(net.IPNet{})
if err != nil {
t.Fatal(err)
}
err = alloc.Free(net)
if err != nil {
t.Fatal(err)
}
err = alloc.Free(net)
if err == nil {
t.Fatal("Expected DoubleFree error")
}
}
func TestExhaust(t *testing.T) {
_, prefix, _ := net.ParseCIDR("2001:db8::/62")
alloc, _ := NewBitmapAllocator(*prefix, 64)
allocd := []net.IPNet{}
for i := 0; i < 4; i++ {
net, err := alloc.Allocate(net.IPNet{Mask: net.CIDRMask(64, 128)})
if err != nil {
t.Fatalf("Error before exhaustion: %v", err)
}
allocd = append(allocd, net)
}
_, err := alloc.Allocate(net.IPNet{})
if err == nil {
t.Fatalf("Successfully allocated more prefixes than there are in the pool")
}
err = alloc.Free(allocd[1])
if err != nil {
t.Fatalf("Could not free: %v", err)
}
net, err := alloc.Allocate(allocd[1])
if err != nil {
t.Fatalf("Could not reallocate after free: %v", err)
}
if !net.IP.Equal(allocd[1].IP) || net.Mask.String() != allocd[1].Mask.String() {
t.Fatalf("Did not obtain the right network after free: got %v, expected %v", net, allocd[1])
}
}
func TestOutOfPool(t *testing.T) {
alloc := getAllocator(8)
_, prefix, _ := net.ParseCIDR("fe80:abcd::/48")
res, err := alloc.Allocate(*prefix)
if err != nil {
t.Fatalf("Failed to allocate with invalid hint: %v", err)
}
if !alloc.containing.Contains(res.IP) {
t.Fatal("Obtained prefix outside of range: ", res)
}
if prefLen, totalLen := res.Mask.Size(); prefLen != 64 || totalLen != 128 {
t.Fatalf("Prefixes have wrong size %d/%d", prefLen, totalLen)
}
}
func prefixSizeForAllocs(allocs int) int {
return int(math.Ceil(math.Log2(float64(allocs))))
}
// Benchmark parallel Allocate, when the bitmap is mostly empty and we're allocating few values
// compared to the available allocations
func BenchmarkParallelAllocInitiallyEmpty(b *testing.B) {
// Run with -race to debug concurrency issues
alloc := getAllocator(prefixSizeForAllocs(b.N) + 2) // Use max 25% of the bitmap (initially empty)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if net, err := alloc.Allocate(net.IPNet{}); err != nil {
b.Logf("Could not allocate (got %v and an error): %v", net, err)
b.Fail()
}
}
})
}
func BenchmarkParallelAllocPartiallyFilled(b *testing.B) {
// We'll make a bitmap with 2x the number of allocs we want to make.
// Then randomly fill it to about 50% utilization
alloc := getAllocator(prefixSizeForAllocs(b.N) + 1)
// Build a replacement bitmap that we'll put in the allocator, with approx. 50% of values filled
newbmap := make([]uint64, alloc.bitmap.Len())
for i := uint(0); i < alloc.bitmap.Len(); i++ {
newbmap[i] = rand.Uint64()
}
alloc.bitmap = bitset.From(newbmap)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if net, err := alloc.Allocate(net.IPNet{}); err != nil {
b.Logf("Could not allocate (got %v and an error): %v", net, err)
b.Fail()
}
}
})
}

View File

@@ -0,0 +1,126 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// Provides functions to add/subtract ipv6 addresses, for use in offset
// calculations in allocators
package allocators
import (
"bytes"
"encoding/binary"
"errors"
"math/bits"
"net"
)
// ErrOverflow is returned when arithmetic operations on IPs carry bits
// over/under the 0th or 128th bit respectively
var ErrOverflow = errors.New("Operation overflows")
// Offset returns the absolute distance between addresses `a` and `b` in units
// of /`prefixLength` subnets.
// Both addresses will have a /`prefixLength` mask applied to them, any
// differences of less than that will be discarded
// If the distance is larger than 2^64 units of /`prefixLength` an error is returned
//
// This function is used in allocators to index bitmaps by an offset from the
// first ip of the range
func Offset(a, b net.IP, prefixLength int) (uint64, error) {
if prefixLength > 128 || prefixLength < 0 {
return 0, errors.New("prefix out of range")
}
reverse := bytes.Compare(a, b)
if reverse == 0 {
return 0, nil
} else if reverse < 0 {
a, b = b, a
}
// take an example of [a:b:c:d:e:f:g:h] [1:2:3:4:5:6:7:8]
// 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
// native integers for computation
ah, bh := binary.BigEndian.Uint64(a[:8]), binary.BigEndian.Uint64(b[:8])
if prefixLength <= 64 {
// [(a:b:c):d|e:f:g:h] - [(1:2:3):4|5:6:7:8]
// Only the high bits matter, so the distance always fits within 64 bits.
// We shift to remove anything to the right of the cut
// [(a:b:c):d] => [0:a:b:c]
return (ah - bh) >> (64 - uint(prefixLength)), nil
}
// General case where both high and low bits matter
al, bl := binary.BigEndian.Uint64(a[8:]), binary.BigEndian.Uint64(b[8:])
distanceLow, borrow := bits.Sub64(al, bl, 0)
// This is the distance between the high bits. depending on the prefix unit, we
// will shift this distance left or right
distanceHigh, _ := bits.Sub64(ah, bh, borrow) // [a:b:c:d] - [1:2:3:4]
// [a:b:c:(d|e:f:g):h] - [1:2:3:(4|5:6:7):8]
// we cut in the low bits (eg. between the parentheses)
// To ensure we stay within 64 bits, we need to ensure [a:b:c:d] - [1:2:3:4] = [0:0:0:d-4]
// so that we don't overflow when adding to the low bits
if distanceHigh >= (1 << (128 - uint(prefixLength))) {
return 0, ErrOverflow
}
// Schema of the carry and shifts:
// [a:b:c:(d]
// [e:f:g):h]
// <---------------> prefixLen
// <-> 128 - prefixLen (cut right)
// <-----> prefixLen - 64 (cut left)
//
// [a:b:c:(d] => [d:0:0:0]
distanceHigh <<= uint(prefixLength) - 64
// [e:f:g):h] => [0:e:f:g]
distanceLow >>= 128 - uint(prefixLength)
// [d:0:0:0] + [0:e:f:g] = (d:e:f:g)
return distanceHigh + distanceLow, nil
}
// AddPrefixes returns the `n`th /`unit` subnet after the `ip` base subnet. It
// is the converse operation of Offset(), used to retrieve a prefix from the
// index within the allocator table
func AddPrefixes(ip net.IP, n, unit uint64) (net.IP, error) {
if unit == 0 && n != 0 {
return net.IP{}, ErrOverflow
} else if n == 0 {
return ip, nil
}
if len(ip) != 16 {
// We don't actually care if they're true v6 or v4-mapped,
// but they need to be 128-bit to handle as 64-bit ints
return net.IP{}, errors.New("AddPrefixes needs 128-bit IPs")
}
// Compute as pairs of uint64 for easier operations
// This could all be 1 function call if go had 128-bit integers
iph, ipl := binary.BigEndian.Uint64(ip[:8]), binary.BigEndian.Uint64(ip[8:])
// Compute `n` /`unit` subnets as uint64 pair
var offh, offl uint64
if unit <= 64 {
offh = n << (64 - unit)
} else {
offh, offl = bits.Mul64(n, 1<<(128-unit))
}
// Now add the 2, check for overflow
ipl, carry := bits.Add64(offl, ipl, 0)
iph, carry = bits.Add64(offh, iph, carry)
if carry != 0 {
return net.IP{}, ErrOverflow
}
// Finally convert back to net.IP
ret := make(net.IP, net.IPv6len)
binary.BigEndian.PutUint64(ret[:8], iph)
binary.BigEndian.PutUint64(ret[8:], ipl)
return ret, nil
}

View File

@@ -0,0 +1,77 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package allocators
import (
"fmt"
"net"
"testing"
"math/rand"
)
func ExampleOffset() {
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 0))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 16))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 32))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 48))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 64))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 73))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 80))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 96))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 112))
fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 128))
// Output:
// 0 <nil>
// 0 <nil>
// 0 <nil>
// 254 <nil>
// 16667973 <nil>
// 8534002176 <nil>
// 1092352278528 <nil>
// 71588398925611008 <nil>
// 0 Operation overflows
// 0 Operation overflows
}
func ExampleAddPrefixes() {
fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 64))
fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0x1, 128))
fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 32))
fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0x1, 16))
fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 65))
// Error cases
fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 8))
fmt.Println(AddPrefixes(net.IP{10, 0, 0, 1}, 64, 32))
// Output:
// 2001:db8:0:ff:: <nil>
// 2001:db8::1 <nil>
// 2001:eb7:: <nil>
// 2002:db8:: <nil>
// 2001:db8:0:7f:8000:: <nil>
// <nil> Operation overflows
// <nil> AddPrefixes needs 128-bit IPs
}
// Offset is used as a hash function, so it needs to be reasonably fast
func BenchmarkOffset(b *testing.B) {
// Need predictable randomness for benchmark reproducibility
rng := rand.New(rand.NewSource(0))
addresses := make([]byte, b.N*net.IPv6len*2)
_, err := rng.Read(addresses)
if err != nil {
b.Fatalf("Could not generate random addresses: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// The arrays will be in cache, so this should amortize to measure mostly just the offset
// computation itself
_, _ = Offset(
addresses[i*2*net.IPv6len:(i*2+1)*net.IPv6len],
addresses[(i*2+1)*net.IPv6len:(i+1)*2*net.IPv6len],
(i*4)%128,
)
}
}

View File

@@ -0,0 +1,86 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package autoconfigure
// This plugin implements RFC2563:
// 1. If the client has been allocated an IP address, do nothing
// 2. If the client has not been allocated an IP address
// (yiaddr=0.0.0.0), then:
// 2a. If the client has requested the "AutoConfigure" option,
// then add the defined value to the response
// 2b. Otherwise, terminate processing and send no reply
//
// This plugin should be used at the end of the plugin chain,
// after any IP address allocation has taken place.
//
// The optional argument is the string "DoNotAutoConfigure" or
// "AutoConfigure" (or "0" or "1" respectively). The default
// is DoNotAutoConfigure.
import (
"errors"
"fmt"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/sirupsen/logrus"
)
var log = logger.GetLogger("plugins/autoconfigure")
var autoconfigure dhcpv4.AutoConfiguration
var Plugin = plugins.Plugin{
Name: "autoconfigure",
Setup4: setup4,
}
var argMap = map[string]dhcpv4.AutoConfiguration{
"0": dhcpv4.AutoConfiguration(0),
"1": dhcpv4.AutoConfiguration(1),
"DoNotAutoConfigure": dhcpv4.DoNotAutoConfigure,
"AutoConfigure": dhcpv4.AutoConfigure,
}
func setup4(args ...string) (handler.Handler4, error) {
if len(args) > 0 {
var ok bool
autoconfigure, ok = argMap[args[0]]
if !ok {
return nil, fmt.Errorf("unexpected value '%v' for autoconfigure argument", args[0])
}
}
if len(args) > 1 {
return nil, errors.New("too many arguments")
}
return Handler4, nil
}
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
if resp.MessageType() != dhcpv4.MessageTypeOffer || !resp.YourIPAddr.IsUnspecified() {
return resp, false
}
ac, ok := req.AutoConfigure()
if ok {
resp.UpdateOption(dhcpv4.OptAutoConfigure(autoconfigure))
log.WithFields(logrus.Fields{
"mac": req.ClientHWAddr.String(),
"autoconfigure": fmt.Sprintf("%v", ac),
}).Debugf("Responded with autoconfigure %v", autoconfigure)
return resp, false
}
log.WithFields(logrus.Fields{
"mac": req.ClientHWAddr.String(),
"autoconfigure": "nil",
}).Debugf("Client does not support autoconfigure")
// RFC2563 2.3: if no address is chosen for the host [...]
// If the DHCPDISCOVER does not contain the Auto-Configure option,
// it is not answered.
return nil, true
}

View File

@@ -0,0 +1,118 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package autoconfigure
import (
"bytes"
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv4"
)
func TestOptionRequested0(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
req.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionAutoConfigure, []byte{1}))
stub, err := dhcpv4.NewReplyFromRequest(req,
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
)
if err != nil {
t.Fatal(err)
}
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
opt := resp.Options.Get(dhcpv4.OptionAutoConfigure)
if opt == nil {
t.Fatal("plugin did not return the Auto-Configure option")
}
if !bytes.Equal(opt, []byte{0}) {
t.Errorf("plugin gave wrong option response: %v", opt)
}
}
func TestOptionRequested1(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
req.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionAutoConfigure, []byte{1}))
stub, err := dhcpv4.NewReplyFromRequest(req,
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
)
if err != nil {
t.Fatal(err)
}
autoconfigure = 1
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
opt := resp.Options.Get(dhcpv4.OptionAutoConfigure)
if opt == nil {
t.Fatal("plugin did not return the Auto-Configure option")
}
if !bytes.Equal(opt, []byte{1}) {
t.Errorf("plugin gave wrong option response: %v", opt)
}
}
func TestNotRequestedAssignedIP(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
stub, err := dhcpv4.NewReplyFromRequest(req,
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
)
if err != nil {
t.Fatal(err)
}
stub.YourIPAddr = net.ParseIP("192.0.2.100")
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
if resp.Options.Get(dhcpv4.OptionAutoConfigure) != nil {
t.Error("plugin responsed with AutoConfigure option")
}
}
func TestNotRequestedNoIP(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
stub, err := dhcpv4.NewReplyFromRequest(req,
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
)
if err != nil {
t.Fatal(err)
}
resp, stop := Handler4(req, stub)
if resp != nil {
t.Error("plugin returned a message")
}
if !stop {
t.Error("plugin did not interrupt processing")
}
}

83
plugins/dns/plugin.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package dns
import (
"errors"
"net"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
var log = logger.GetLogger("plugins/dns")
// Plugin wraps the DNS plugin information.
var Plugin = plugins.Plugin{
Name: "dns",
Setup6: setup6,
Setup4: setup4,
}
var (
dnsServers6 []net.IP
dnsServers4 []net.IP
)
func setup6(args ...string) (handler.Handler6, error) {
if len(args) < 1 {
return nil, errors.New("need at least one DNS server")
}
for _, arg := range args {
server := net.ParseIP(arg)
if server.To16() == nil {
return Handler6, errors.New("expected an DNS server address, got: " + arg)
}
dnsServers6 = append(dnsServers6, server)
}
log.Infof("loaded %d DNS servers.", len(dnsServers6))
return Handler6, nil
}
func setup4(args ...string) (handler.Handler4, error) {
log.Printf("loaded plugin for DHCPv4.")
if len(args) < 1 {
return nil, errors.New("need at least one DNS server")
}
for _, arg := range args {
DNSServer := net.ParseIP(arg)
if DNSServer.To4() == nil {
return Handler4, errors.New("expected an DNS server address, got: " + arg)
}
dnsServers4 = append(dnsServers4, DNSServer)
}
log.Infof("loaded %d DNS servers.", len(dnsServers4))
return Handler4, nil
}
// Handler6 handles DHCPv6 packets for the dns plugin
func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
decap, err := req.GetInnerMessage()
if err != nil {
log.Errorf("Could not decapsulate relayed message, aborting: %v", err)
return nil, true
}
if decap.IsOptionRequested(dhcpv6.OptionDNSRecursiveNameServer) {
resp.UpdateOption(dhcpv6.OptDNS(dnsServers6...))
}
return resp, false
}
//Handler4 handles DHCPv4 packets for the dns plugin
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
if req.IsOptionRequested(dhcpv4.OptionDomainNameServer) {
resp.Options.Update(dhcpv4.OptDNS(dnsServers4...))
}
return resp, false
}

149
plugins/dns/plugin_test.go Normal file
View File

@@ -0,0 +1,149 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package dns
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
func TestAddServer6(t *testing.T) {
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
req.MessageType = dhcpv6.MessageTypeRequest
req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionDNSRecursiveNameServer))
stub, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
stub.MessageType = dhcpv6.MessageTypeReply
dnsServers6 = []net.IP{
net.ParseIP("2001:db8::1"),
net.ParseIP("2001:db8::3"),
}
resp, stop := Handler6(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
opts := resp.GetOption(dhcpv6.OptionDNSRecursiveNameServer)
if len(opts) != 1 {
t.Fatalf("Expected 1 RDNSS option, got %d: %v", len(opts), opts)
}
foundServers := resp.(*dhcpv6.Message).Options.DNS()
// XXX: is enforcing the order relevant here ?
for i, srv := range foundServers {
if !srv.Equal(dnsServers6[i]) {
t.Errorf("Found server %s, expected %s", srv, dnsServers6[i])
}
}
if len(foundServers) != len(dnsServers6) {
t.Errorf("Found %d servers, expected %d", len(foundServers), len(dnsServers6))
}
}
func TestNotRequested6(t *testing.T) {
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
req.MessageType = dhcpv6.MessageTypeRequest
req.AddOption(dhcpv6.OptRequestedOption())
stub, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
stub.MessageType = dhcpv6.MessageTypeReply
dnsServers6 = []net.IP{
net.ParseIP("2001:db8::1"),
}
resp, stop := Handler6(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
opts := resp.GetOption(dhcpv6.OptionDNSRecursiveNameServer)
if len(opts) != 0 {
t.Errorf("RDNSS options were added when not requested: %v", opts)
}
}
func TestAddServer4(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
stub, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
t.Fatal(err)
}
dnsServers4 = []net.IP{
net.ParseIP("192.0.2.1"),
net.ParseIP("192.0.2.3"),
}
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
servers := resp.DNS()
for i, srv := range servers {
if !srv.Equal(dnsServers4[i]) {
t.Errorf("Found server %s, expected %s", srv, dnsServers4[i])
}
}
if len(servers) != len(dnsServers4) {
t.Errorf("Found %d servers, expected %d", len(servers), len(dnsServers4))
}
}
func TestNotRequested4(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
stub, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
t.Fatal(err)
}
dnsServers4 = []net.IP{
net.ParseIP("192.0.2.1"),
}
req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress))
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
servers := dhcpv4.GetIPs(dhcpv4.OptionDomainNameServer, resp.Options)
if len(servers) != 0 {
t.Errorf("Found %d DNS servers when explicitly not requested", len(servers))
}
}

118
plugins/example/plugin.go Normal file
View File

@@ -0,0 +1,118 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package example
// This is an example plugin that inspects a packet and prints it out. The code
// is commented in a way that should walk you through the implementation of your
// own plugins.
// Feedback is welcome!
import (
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
// We use a customizable logger, as part of the `logger` package. You can use
// `logger.GetLogger()` to get a singleton instance of the logger. Then just use
// it with the `logrus` interface (https://github.com/sirupsen/logrus). More
// information in the docstring of the logger package.
var log = logger.GetLogger("plugins/example")
// Plugin wraps the information necessary to register a plugin.
// In the main package, you need to export a `plugins.Plugin` object called
// `Plugin`, so it can be registered into the plugin registry.
// Just import your plugin, and fill the structure with plugin name and setup
// functions:
//
// import (
// "github.com/coredhcp/coredhcp/plugins"
// "github.com/coredhcp/coredhcp/plugins/example"
// )
//
// var Plugin = plugins.Plugin{
// Name: "example",
// Setup6: setup6,
// Setup4: setup4,
// }
//
// Name is simply the name used to register the plugin. It must be unique to
// other registered plugins, or the operation will fail. In other words, don't
// declare plugins with colliding names.
//
// Setup6 and Setup4 are the setup functions for DHCPv6 and DHCPv4 traffic
// handlers. They conform to the `plugins.SetupFunc6` and `plugins.SetupFunc4`
// interfaces, so they must return a `plugins.Handler6` and a `plugins.Handler4`
// respectively.
// A `nil` setup function means that that protocol won't be handled by this
// plugin.
//
// Note that importing the plugin is not enough to use it: you have to
// explicitly specify the intention to use it in the `config.yml` file, in the
// plugins section. For example:
//
// server6:
// listen: '[::]547'
// - example:
// - server_id: LL aa:bb:cc:dd:ee:ff
// - file: "leases.txt"
//
var Plugin = plugins.Plugin{
Name: "example",
Setup6: setup6,
Setup4: setup4,
}
// setup6 is the setup function to initialize the handler for DHCPv6
// traffic. This function implements the `plugin.SetupFunc6` interface.
// This function returns a `handler.Handler6` function, and an error if any.
// In this example we do very little in the setup function, and just return the
// `exampleHandler6` function. Such function will be called for every DHCPv6
// packet that the server receives. Remember that a handler may not be called
// for each packet, if the handler chain is interrupted before reaching it.
func setup6(args ...string) (handler.Handler6, error) {
log.Printf("loaded plugin for DHCPv6.")
return exampleHandler6, nil
}
// setup4 behaves like setupExample6, but for DHCPv4 packets. It
// implements the `plugin.SetupFunc4` interface.
func setup4(args ...string) (handler.Handler4, error) {
log.Printf("loaded plugin for DHCPv4.")
return exampleHandler4, nil
}
// exampleHandler6 handles DHCPv6 packets for the example plugin. It implements
// the `handler.Handler6` interface. The input arguments are the request packet
// that the server received from a client, and the response packet that has been
// computed so far. This function returns the response packet to be sent back to
// the client, and a boolean.
// The response can be either the same response packet received as input, a
// modified response packet, or nil. If nil, the server will not reply to the
// client, basically dropping the request.
// The returned boolean indicates to the server whether the chain of plugins
// should continue or not. If `true`, the server will stop at this plugin, and
// respond to the client (or drop the response, if nil). If `false`, the server
// will call the next plugin in the chan, using the returned response packet as
// input for the next plugin.
func exampleHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
log.Printf("received DHCPv6 packet: %s", req.Summary())
// return the unmodified response, and false. This means that the next
// plugin in the chain will be called, and the unmodified response packet
// will be used as its input.
return resp, false
}
// exampleHandler4 behaves like exampleHandler6, but for DHCPv4 packets. It
// implements the `handler.Handler4` interface.
func exampleHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
log.Printf("received DHCPv4 packet: %s", req.Summary())
// return the unmodified response, and false. This means that the next
// plugin in the chain will be called, and the unmodified response packet
// will be used as its input.
return resp, false
}

282
plugins/file/plugin.go Normal file
View File

@@ -0,0 +1,282 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// Package file enables static mapping of MAC <--> IP addresses.
// The mapping is stored in a text file, where each mapping is described by one line containing
// two fields separated by spaces: MAC address, and IP address. For example:
//
// $ cat file_leases.txt
// 00:11:22:33:44:55 10.0.0.1
// 01:23:45:67:89:01 10.0.10.10
//
// To specify the plugin configuration in the server6/server4 sections of the config file, just
// pass the leases file name as plugin argument, e.g.:
//
// $ cat config.yml
//
// server6:
// ...
// plugins:
// - file: "file_leases.txt" [autorefresh]
// ...
//
// If the file path is not absolute, it is relative to the cwd where coredhcp is run.
//
// Optionally, when the 'autorefresh' argument is given, the plugin will try to refresh
// the lease mapping during runtime whenever the lease file is updated.
package file
import (
"bytes"
"errors"
"fmt"
"net"
"os"
"strings"
"sync"
"time"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/fsnotify/fsnotify"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
const (
autoRefreshArg = "autorefresh"
)
var log = logger.GetLogger("plugins/file")
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "file",
Setup6: setup6,
Setup4: setup4,
}
var recLock sync.RWMutex
// StaticRecords holds a MAC -> IP address mapping
var StaticRecords map[string]net.IP
// DHCPv6Records and DHCPv4Records are mappings between MAC addresses in
// form of a string, to network configurations.
var (
DHCPv6Records map[string]net.IP
DHCPv4Records map[string]net.IP
)
// LoadDHCPv4Records loads the DHCPv4Records global map with records stored on
// the specified file. The records have to be one per line, a mac address and an
// IPv4 address.
func LoadDHCPv4Records(filename string) (map[string]net.IP, error) {
log.Infof("reading leases from %s", filename)
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
records := make(map[string]net.IP)
for _, lineBytes := range bytes.Split(data, []byte{'\n'}) {
line := string(lineBytes)
if len(line) == 0 {
continue
}
if strings.HasPrefix(line, "#") {
continue
}
tokens := strings.Fields(line)
if len(tokens) != 2 {
return nil, fmt.Errorf("malformed line, want 2 fields, got %d: %s", len(tokens), line)
}
hwaddr, err := net.ParseMAC(tokens[0])
if err != nil {
return nil, fmt.Errorf("malformed hardware address: %s", tokens[0])
}
ipaddr := net.ParseIP(tokens[1])
if ipaddr.To4() == nil {
return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr)
}
records[hwaddr.String()] = ipaddr
}
return records, nil
}
// LoadDHCPv6Records loads the DHCPv6Records global map with records stored on
// the specified file. The records have to be one per line, a mac address and an
// IPv6 address.
func LoadDHCPv6Records(filename string) (map[string]net.IP, error) {
log.Infof("reading leases from %s", filename)
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
records := make(map[string]net.IP)
for _, lineBytes := range bytes.Split(data, []byte{'\n'}) {
line := string(lineBytes)
if len(line) == 0 {
continue
}
if strings.HasPrefix(line, "#") {
continue
}
tokens := strings.Fields(line)
if len(tokens) != 2 {
return nil, fmt.Errorf("malformed line, want 2 fields, got %d: %s", len(tokens), line)
}
hwaddr, err := net.ParseMAC(tokens[0])
if err != nil {
return nil, fmt.Errorf("malformed hardware address: %s", tokens[0])
}
ipaddr := net.ParseIP(tokens[1])
if ipaddr.To16() == nil || ipaddr.To4() != nil {
return nil, fmt.Errorf("expected an IPv6 address, got: %v", ipaddr)
}
records[hwaddr.String()] = ipaddr
}
return records, nil
}
// Handler6 handles DHCPv6 packets for the file plugin
func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
m, err := req.GetInnerMessage()
if err != nil {
log.Errorf("BUG: could not decapsulate: %v", err)
return nil, true
}
if m.Options.OneIANA() == nil {
log.Debug("No address requested")
return resp, false
}
mac, err := dhcpv6.ExtractMAC(req)
if err != nil {
log.Warningf("Could not find client MAC, passing")
return resp, false
}
log.Debugf("looking up an IP address for MAC %s", mac.String())
recLock.RLock()
defer recLock.RUnlock()
ipaddr, ok := StaticRecords[mac.String()]
if !ok {
log.Warningf("MAC address %s is unknown", mac.String())
return resp, false
}
log.Debugf("found IP address %s for MAC %s", ipaddr, mac.String())
resp.AddOption(&dhcpv6.OptIANA{
IaId: m.Options.OneIANA().IaId,
Options: dhcpv6.IdentityOptions{Options: []dhcpv6.Option{
&dhcpv6.OptIAAddress{
IPv6Addr: ipaddr,
PreferredLifetime: 3600 * time.Second,
ValidLifetime: 3600 * time.Second,
},
}},
})
return resp, false
}
// Handler4 handles DHCPv4 packets for the file plugin
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
recLock.RLock()
defer recLock.RUnlock()
ipaddr, ok := StaticRecords[req.ClientHWAddr.String()]
if !ok {
log.Warningf("MAC address %s is unknown", req.ClientHWAddr.String())
return resp, false
}
resp.YourIPAddr = ipaddr
log.Debugf("found IP address %s for MAC %s", ipaddr, req.ClientHWAddr.String())
return resp, true
}
func setup6(args ...string) (handler.Handler6, error) {
h6, _, err := setupFile(true, args...)
return h6, err
}
func setup4(args ...string) (handler.Handler4, error) {
_, h4, err := setupFile(false, args...)
return h4, err
}
func setupFile(v6 bool, args ...string) (handler.Handler6, handler.Handler4, error) {
var err error
if len(args) < 1 {
return nil, nil, errors.New("need a file name")
}
filename := args[0]
if filename == "" {
return nil, nil, errors.New("got empty file name")
}
// load initial database from lease file
if err = loadFromFile(v6, filename); err != nil {
return nil, nil, err
}
// when the 'autorefresh' argument was passed, watch the lease file for
// changes and reload the lease mapping on any event
if len(args) > 1 && args[1] == autoRefreshArg {
// creates a new file watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, nil, fmt.Errorf("failed to create watcher: %w", err)
}
// have file watcher watch over lease file
if err = watcher.Add(filename); err != nil {
return nil, nil, fmt.Errorf("failed to watch %s: %w", filename, err)
}
// very simple watcher on the lease file to trigger a refresh on any event
// on the file
go func() {
for range watcher.Events {
err := loadFromFile(v6, filename)
if err != nil {
log.Warningf("failed to refresh from %s: %s", filename, err)
continue
}
log.Infof("updated to %d leases from %s", len(StaticRecords), filename)
}
}()
}
log.Infof("loaded %d leases from %s", len(StaticRecords), filename)
return Handler6, Handler4, nil
}
func loadFromFile(v6 bool, filename string) error {
var err error
var records map[string]net.IP
var protver int
if v6 {
protver = 6
records, err = LoadDHCPv6Records(filename)
} else {
protver = 4
records, err = LoadDHCPv4Records(filename)
}
if err != nil {
return fmt.Errorf("failed to load DHCPv%d records: %w", protver, err)
}
recLock.Lock()
defer recLock.Unlock()
StaticRecords = records
return nil
}

368
plugins/file/plugin_test.go Normal file
View File

@@ -0,0 +1,368 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package file
import (
"net"
"os"
"testing"
"time"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadDHCPv4Records(t *testing.T) {
t.Run("valid leases", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// fill temp file with valid lease lines and some comments
_, err = tmp.WriteString("00:11:22:33:44:55 192.0.2.100\n")
require.NoError(t, err)
_, err = tmp.WriteString("11:22:33:44:55:66 192.0.2.101\n")
require.NoError(t, err)
_, err = tmp.WriteString("# this is a comment\n")
require.NoError(t, err)
records, err := LoadDHCPv4Records(tmp.Name())
if !assert.NoError(t, err) {
return
}
if assert.Equal(t, 2, len(records)) {
if assert.Contains(t, records, "00:11:22:33:44:55") {
assert.Equal(t, net.ParseIP("192.0.2.100"), records["00:11:22:33:44:55"])
}
if assert.Contains(t, records, "11:22:33:44:55:66") {
assert.Equal(t, net.ParseIP("192.0.2.101"), records["11:22:33:44:55:66"])
}
}
})
t.Run("missing field", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with too few fields
_, err = tmp.WriteString("foo\n")
require.NoError(t, err)
_, err = LoadDHCPv4Records(tmp.Name())
assert.Error(t, err)
})
t.Run("invalid MAC", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with invalid MAC address to trigger an error
_, err = tmp.WriteString("abcd 192.0.2.102\n")
require.NoError(t, err)
_, err = LoadDHCPv4Records(tmp.Name())
assert.Error(t, err)
})
t.Run("invalid IP address", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with invalid MAC address to trigger an error
_, err = tmp.WriteString("22:33:44:55:66:77 bcde\n")
require.NoError(t, err)
_, err = LoadDHCPv4Records(tmp.Name())
assert.Error(t, err)
})
t.Run("lease with IPv6 address", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with IPv6 address instead to trigger an error
_, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n")
require.NoError(t, err)
_, err = LoadDHCPv4Records(tmp.Name())
assert.Error(t, err)
})
}
func TestLoadDHCPv6Records(t *testing.T) {
t.Run("valid leases", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// fill temp file with valid lease lines and some comments
_, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n")
require.NoError(t, err)
_, err = tmp.WriteString("11:22:33:44:55:66 2001:db8::10:2\n")
require.NoError(t, err)
_, err = tmp.WriteString("# this is a comment\n")
require.NoError(t, err)
records, err := LoadDHCPv6Records(tmp.Name())
if !assert.NoError(t, err) {
return
}
if assert.Equal(t, 2, len(records)) {
if assert.Contains(t, records, "00:11:22:33:44:55") {
assert.Equal(t, net.ParseIP("2001:db8::10:1"), records["00:11:22:33:44:55"])
}
if assert.Contains(t, records, "11:22:33:44:55:66") {
assert.Equal(t, net.ParseIP("2001:db8::10:2"), records["11:22:33:44:55:66"])
}
}
})
t.Run("missing field", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with too few fields
_, err = tmp.WriteString("foo\n")
require.NoError(t, err)
_, err = LoadDHCPv6Records(tmp.Name())
assert.Error(t, err)
})
t.Run("invalid MAC", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with invalid MAC address to trigger an error
_, err = tmp.WriteString("abcd 2001:db8::10:3\n")
require.NoError(t, err)
_, err = LoadDHCPv6Records(tmp.Name())
assert.Error(t, err)
})
t.Run("invalid IP address", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with invalid MAC address to trigger an error
_, err = tmp.WriteString("22:33:44:55:66:77 bcde\n")
require.NoError(t, err)
_, err = LoadDHCPv6Records(tmp.Name())
assert.Error(t, err)
})
t.Run("lease with IPv4 address", func(t *testing.T) {
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
// add line with IPv4 address instead to trigger an error
_, err = tmp.WriteString("00:11:22:33:44:55 192.0.2.100\n")
require.NoError(t, err)
_, err = LoadDHCPv6Records(tmp.Name())
assert.Error(t, err)
})
}
func TestHandler4(t *testing.T) {
t.Run("unknown MAC", func(t *testing.T) {
// prepare DHCPv4 request
mac := "00:11:22:33:44:55"
claddr, _ := net.ParseMAC(mac)
req := &dhcpv4.DHCPv4{
ClientHWAddr: claddr,
}
resp := &dhcpv4.DHCPv4{}
assert.Nil(t, resp.ClientIPAddr)
// if we handle this DHCP request, nothing should change since the lease is
// unknown
result, stop := Handler4(req, resp)
assert.Same(t, result, resp)
assert.False(t, stop)
assert.Nil(t, result.YourIPAddr)
})
t.Run("known MAC", func(t *testing.T) {
// prepare DHCPv4 request
mac := "00:11:22:33:44:55"
claddr, _ := net.ParseMAC(mac)
req := &dhcpv4.DHCPv4{
ClientHWAddr: claddr,
}
resp := &dhcpv4.DHCPv4{}
assert.Nil(t, resp.ClientIPAddr)
// add lease for the MAC in the lease map
clIPAddr := net.ParseIP("192.0.2.100")
StaticRecords = map[string]net.IP{
mac: clIPAddr,
}
// if we handle this DHCP request, the YourIPAddr field should be set
// in the result
result, stop := Handler4(req, resp)
assert.Same(t, result, resp)
assert.True(t, stop)
assert.Equal(t, clIPAddr, result.YourIPAddr)
// cleanup
StaticRecords = make(map[string]net.IP)
})
}
func TestHandler6(t *testing.T) {
t.Run("unknown MAC", func(t *testing.T) {
// prepare DHCPv6 request
mac := "11:22:33:44:55:66"
claddr, _ := net.ParseMAC(mac)
req, err := dhcpv6.NewSolicit(claddr)
require.NoError(t, err)
resp, err := dhcpv6.NewAdvertiseFromSolicit(req)
require.NoError(t, err)
assert.Equal(t, 0, len(resp.GetOption(dhcpv6.OptionIANA)))
// if we handle this DHCP request, nothing should change since the lease is
// unknown
result, stop := Handler6(req, resp)
assert.False(t, stop)
assert.Equal(t, 0, len(result.GetOption(dhcpv6.OptionIANA)))
})
t.Run("known MAC", func(t *testing.T) {
// prepare DHCPv6 request
mac := "11:22:33:44:55:66"
claddr, _ := net.ParseMAC(mac)
req, err := dhcpv6.NewSolicit(claddr)
require.NoError(t, err)
resp, err := dhcpv6.NewAdvertiseFromSolicit(req)
require.NoError(t, err)
assert.Equal(t, 0, len(resp.GetOption(dhcpv6.OptionIANA)))
// add lease for the MAC in the lease map
clIPAddr := net.ParseIP("2001:db8::10:1")
StaticRecords = map[string]net.IP{
mac: clIPAddr,
}
// if we handle this DHCP request, there should be a specific IANA option
// set in the resulting response
result, stop := Handler6(req, resp)
assert.False(t, stop)
if assert.Equal(t, 1, len(result.GetOption(dhcpv6.OptionIANA))) {
opt := result.GetOneOption(dhcpv6.OptionIANA)
assert.Contains(t, opt.String(), "IP=2001:db8::10:1")
}
// cleanup
StaticRecords = make(map[string]net.IP)
})
}
func TestSetupFile(t *testing.T) {
// too few arguments
_, _, err := setupFile(false)
assert.Error(t, err)
// empty file name
_, _, err = setupFile(false, "")
assert.Error(t, err)
// trigger error in LoadDHCPv*Records
_, _, err = setupFile(false, "/foo/bar")
assert.Error(t, err)
_, _, err = setupFile(true, "/foo/bar")
assert.Error(t, err)
// setup temp leases file
tmp, err := os.CreateTemp("", "test_plugin_file")
require.NoError(t, err)
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}()
t.Run("typical case", func(t *testing.T) {
_, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n")
require.NoError(t, err)
_, err = tmp.WriteString("11:22:33:44:55:66 2001:db8::10:2\n")
require.NoError(t, err)
assert.Equal(t, 0, len(StaticRecords))
// leases should show up in StaticRecords
_, _, err = setupFile(true, tmp.Name())
if assert.NoError(t, err) {
assert.Equal(t, 2, len(StaticRecords))
}
})
t.Run("autorefresh enabled", func(t *testing.T) {
_, _, err = setupFile(true, tmp.Name(), autoRefreshArg)
if assert.NoError(t, err) {
assert.Equal(t, 2, len(StaticRecords))
}
// we add more leases to the file
// this should trigger an event to refresh the leases database
// without calling setupFile again
_, err = tmp.WriteString("22:33:44:55:66:77 2001:db8::10:3\n")
require.NoError(t, err)
// since the event is processed asynchronously, give it a little time
time.Sleep(time.Millisecond * 100)
// an additional record should show up in the database
// but we should respect the locking first
recLock.RLock()
defer recLock.RUnlock()
assert.Equal(t, 3, len(StaticRecords))
})
}

View File

@@ -0,0 +1,64 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package ipv6only
// This plugin implements RFC8925: if the client has requested the
// IPv6-Only Preferred option, then add the option response and then
// terminate processing immediately.
//
// This module should be invoked *before* any IP address
// allocation has been done, so that the yiaddr is 0.0.0.0 and
// no pool addresses are consumed for compatible clients.
//
// The optional argument is the V6ONLY_WAIT configuration variable,
// described in RFC8925 section 3.2.
import (
"errors"
"time"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/sirupsen/logrus"
)
var log = logger.GetLogger("plugins/ipv6only")
var v6only_wait time.Duration
var Plugin = plugins.Plugin{
Name: "ipv6only",
Setup4: setup4,
}
func setup4(args ...string) (handler.Handler4, error) {
if len(args) > 0 {
dur, err := time.ParseDuration(args[0])
if err != nil {
log.Errorf("invalid duration: %v", args[0])
return nil, errors.New("ipv6only failed to initialize")
}
v6only_wait = dur
}
if len(args) > 1 {
return nil, errors.New("too many arguments")
}
return Handler4, nil
}
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
v6pref := req.IsOptionRequested(dhcpv4.OptionIPv6OnlyPreferred)
log.WithFields(logrus.Fields{
"mac": req.ClientHWAddr.String(),
"ipv6only": v6pref,
}).Debug("ipv6only status")
if v6pref {
resp.UpdateOption(dhcpv4.OptIPv6OnlyPreferred(v6only_wait))
return resp, true
}
return resp, false
}

View File

@@ -0,0 +1,65 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package ipv6only
import (
"bytes"
"net"
"testing"
"time"
"github.com/insomniacslk/dhcp/dhcpv4"
)
func TestOptionRequested(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress, dhcpv4.OptionIPv6OnlyPreferred))
stub, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
t.Fatal(err)
}
v6only_wait = 0x1234 * time.Second
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if !stop {
t.Error("plugin did not interrupt processing")
}
opt := resp.Options.Get(dhcpv4.OptionIPv6OnlyPreferred)
if opt == nil {
t.Fatal("plugin did not return the IPv6-Only Preferred option")
}
if !bytes.Equal(opt, []byte{0x00, 0x00, 0x12, 0x34}) {
t.Errorf("plugin gave wrong option response: %v", opt)
}
}
func TestNotRequested(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
stub, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
t.Fatal(err)
}
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
if resp.Options.Get(dhcpv4.OptionIPv6OnlyPreferred) != nil {
t.Error("Found IPv6-Only Preferred option when not requested")
}
}

View File

@@ -0,0 +1,57 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package leasetime
import (
"errors"
"time"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
)
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "lease_time",
// currently not supported for DHCPv6
Setup6: nil,
Setup4: setup4,
}
var (
log = logger.GetLogger("plugins/lease_time")
v4LeaseTime time.Duration
)
// Handler4 handles DHCPv4 packets for the lease_time plugin.
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
if req.OpCode != dhcpv4.OpcodeBootRequest {
return resp, false
}
// Set lease time unless it has already been set
if !resp.Options.Has(dhcpv4.OptionIPAddressLeaseTime) {
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(v4LeaseTime))
}
return resp, false
}
func setup4(args ...string) (handler.Handler4, error) {
log.Print("loading `lease_time` plugin for DHCPv4")
if len(args) < 1 {
log.Error("No default lease time provided")
return nil, errors.New("lease_time failed to initialize")
}
leaseTime, err := time.ParseDuration(args[0])
if err != nil {
log.Errorf("invalid duration: %v", args[0])
return nil, errors.New("lease_time failed to initialize")
}
v4LeaseTime = leaseTime
return Handler4, nil
}

50
plugins/mtu/plugin.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package mtu
import (
"errors"
"fmt"
"strconv"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
)
var log = logger.GetLogger("plugins/mtu")
// Plugin wraps the MTU plugin information.
var Plugin = plugins.Plugin{
Name: "mtu",
Setup4: setup4,
// No Setup6 since DHCPv6 does not have MTU-related options
}
var (
mtu int
)
func setup4(args ...string) (handler.Handler4, error) {
if len(args) != 1 {
return nil, errors.New("need one mtu value")
}
var err error
if mtu, err = strconv.Atoi(args[0]); err != nil {
return nil, fmt.Errorf("invalid mtu: %v", args[0])
}
log.Infof("loaded mtu %d.", mtu)
return Handler4, nil
}
// Handler4 handles DHCPv4 packets for the mtu plugin
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
if req.IsOptionRequested(dhcpv4.OptionInterfaceMTU) {
resp.Options.Update(dhcpv4.Option{Code: dhcpv4.OptionInterfaceMTU, Value: dhcpv4.Uint16(mtu)})
}
return resp, false
}

View File

@@ -0,0 +1,66 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package mtu
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv4"
)
func TestAddServer4(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, dhcpv4.WithRequestedOptions(dhcpv4.OptionInterfaceMTU))
if err != nil {
t.Fatal(err)
}
stub, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
t.Fatal(err)
}
mtu = 1500
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
rMTU, err := dhcpv4.GetUint16(dhcpv4.OptionInterfaceMTU, resp.Options)
if err != nil {
t.Errorf("Failed to retrieve mtu from response")
}
if mtu != int(rMTU) {
t.Errorf("Found %d mtu, expected %d", rMTU, mtu)
}
}
func TestNotRequested4(t *testing.T) {
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
stub, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
t.Fatal(err)
}
mtu = 1500
req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress))
resp, stop := Handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
if mtu, err := dhcpv4.GetUint16(dhcpv4.OptionInterfaceMTU, resp.Options); err == nil {
t.Errorf("Retrieve mtu %d in response, expected none", mtu)
}
}

142
plugins/nbp/nbp.go Normal file
View File

@@ -0,0 +1,142 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// Package nbp implements handling of an NBP (Network Boot Program) using an
// URL, e.g. http://[fe80::abcd:efff:fe12:3456]/my-nbp or tftp://10.0.0.1/my-nbp .
// The NBP information is only added if it is requested by the client.
//
// Note that for DHCPv4, unless the URL is prefixed with a "http", "https" or
// "ftp" scheme, the URL will be split into TFTP server name (option 66)
// and Bootfile name (option 67), so the scheme will be stripped out, and it
// will be treated as a TFTP URL. Anything other than host name and file path
// will be ignored (no port, no query string, etc).
//
// For DHCPv6 OPT_BOOTFILE_URL (option 59) is used, and the value is passed
// unmodified. If the query string is specified and contains a "param" key,
// its value is also passed as OPT_BOOTFILE_PARAM (option 60), so it will be
// duplicated between option 59 and 60.
//
// Example usage:
//
// server6:
// - plugins:
// - nbp: http://[2001:db8:a::1]/nbp
//
// server4:
// - plugins:
// - nbp: tftp://10.0.0.254/nbp
//
package nbp
import (
"fmt"
"net/url"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
var log = logger.GetLogger("plugins/nbp")
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "nbp",
Setup6: setup6,
Setup4: setup4,
}
var (
opt59, opt60 dhcpv6.Option
opt66, opt67 *dhcpv4.Option
)
func parseArgs(args ...string) (*url.URL, error) {
if len(args) != 1 {
return nil, fmt.Errorf("Exactly one argument must be passed to NBP plugin, got %d", len(args))
}
return url.Parse(args[0])
}
func setup6(args ...string) (handler.Handler6, error) {
u, err := parseArgs(args...)
if err != nil {
return nil, err
}
opt59 = dhcpv6.OptBootFileURL(u.String())
params := u.Query().Get("params")
if params != "" {
opt60 = &dhcpv6.OptionGeneric{
OptionCode: dhcpv6.OptionBootfileParam,
OptionData: []byte(params),
}
}
log.Printf("loaded NBP plugin for DHCPv6.")
return nbpHandler6, nil
}
func setup4(args ...string) (handler.Handler4, error) {
u, err := parseArgs(args...)
if err != nil {
return nil, err
}
var otsn, obfn dhcpv4.Option
switch u.Scheme {
case "http", "https", "ftp":
obfn = dhcpv4.OptBootFileName(u.String())
default:
otsn = dhcpv4.OptTFTPServerName(u.Host)
obfn = dhcpv4.OptBootFileName(u.Path)
opt66 = &otsn
}
opt67 = &obfn
log.Printf("loaded NBP plugin for DHCPv4.")
return nbpHandler4, nil
}
func nbpHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
if opt59 == nil {
// nothing to do
return resp, true
}
decap, err := req.GetInnerMessage()
if err != nil {
log.Errorf("Could not decapsulate request: %v", err)
// drop the request, this is probably a critical error in the packet.
return nil, true
}
for _, code := range decap.Options.RequestedOptions() {
if code == dhcpv6.OptionBootfileURL {
// bootfile URL is requested
resp.AddOption(opt59)
} else if code == dhcpv6.OptionBootfileParam {
// optionally add opt60, bootfile params, if requested
if opt60 != nil {
resp.AddOption(opt60)
}
}
}
log.Debugf("Added NBP %s to request", opt59)
return resp, true
}
func nbpHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
if opt67 == nil {
// nothing to do
return resp, true
}
if req.IsOptionRequested(dhcpv4.OptionTFTPServerName) && opt66 != nil {
resp.Options.Update(*opt66)
log.Debugf("Added NBP %s / %s to request", opt66, opt67)
}
if req.IsOptionRequested(dhcpv4.OptionBootfileName) {
resp.Options.Update(*opt67)
log.Debugf("Added NBP %s to request", opt67)
}
return resp, true
}

62
plugins/netmask/plugin.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package netmask
import (
"encoding/binary"
"errors"
"net"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var log = logger.GetLogger("plugins/netmask")
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "netmask",
Setup4: setup4,
}
var (
netmask net.IPMask
)
func setup4(args ...string) (handler.Handler4, error) {
log.Printf("loaded plugin for DHCPv4.")
if len(args) != 1 {
return nil, errors.New("need at least one netmask IP address")
}
netmaskIP := net.ParseIP(args[0])
if netmaskIP.IsUnspecified() {
return nil, errors.New("netmask is not valid, got: " + args[0])
}
netmaskIP = netmaskIP.To4()
if netmaskIP == nil {
return nil, errors.New("expected an netmask address, got: " + args[0])
}
netmask = net.IPv4Mask(netmaskIP[0], netmaskIP[1], netmaskIP[2], netmaskIP[3])
if !checkValidNetmask(netmask) {
return nil, errors.New("netmask is not valid, got: " + args[0])
}
log.Printf("loaded client netmask")
return Handler4, nil
}
//Handler4 handles DHCPv4 packets for the netmask plugin
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
resp.Options.Update(dhcpv4.OptSubnetMask(netmask))
return resp, false
}
func checkValidNetmask(netmask net.IPMask) bool {
netmaskInt := binary.BigEndian.Uint32(netmask)
x := ^netmaskInt
y := x + 1
return (y & x) == 0
}

View File

@@ -0,0 +1,65 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package netmask
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/stretchr/testify/assert"
)
func TestCheckValidNetmask(t *testing.T) {
assert.True(t, checkValidNetmask(net.IPv4Mask(255, 255, 255, 0)))
assert.True(t, checkValidNetmask(net.IPv4Mask(255, 255, 0, 0)))
assert.True(t, checkValidNetmask(net.IPv4Mask(255, 0, 0, 0)))
assert.True(t, checkValidNetmask(net.IPv4Mask(0, 0, 0, 0)))
assert.False(t, checkValidNetmask(net.IPv4Mask(0, 255, 255, 255)))
assert.False(t, checkValidNetmask(net.IPv4Mask(0, 0, 255, 255)))
assert.False(t, checkValidNetmask(net.IPv4Mask(0, 0, 0, 255)))
}
func TestHandler4(t *testing.T) {
// set plugin netmask
netmask = net.IPv4Mask(255, 255, 255, 0)
// prepare DHCPv4 request
req := &dhcpv4.DHCPv4{}
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.Options{},
}
// if we handle this DHCP request, the netmask should be one of the options
// of the result
result, stop := Handler4(req, resp)
assert.Same(t, result, resp)
assert.False(t, stop)
assert.EqualValues(t, netmask, resp.Options.Get(dhcpv4.OptionSubnetMask))
}
func TestSetup4(t *testing.T) {
// valid configuration
_, err := setup4("255.255.255.0")
assert.NoError(t, err)
assert.EqualValues(t, netmask, net.IPv4Mask(255, 255, 255, 0))
// no configuration
_, err = setup4()
assert.Error(t, err)
// unspecified netmask
_, err = setup4("0.0.0.0")
assert.Error(t, err)
// ipv6 prefix
_, err = setup4("ff02::/64")
assert.Error(t, err)
// invalid netmask
_, err = setup4("0.0.0.255")
assert.Error(t, err)
}

114
plugins/plugin.go Normal file
View File

@@ -0,0 +1,114 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package plugins
import (
"errors"
"github.com/coredhcp/coredhcp/config"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
)
var log = logger.GetLogger("plugins")
// Plugin represents a plugin object.
// Setup6 and Setup4 are the setup functions for DHCPv6 and DHCPv4 handlers
// respectively. Both setup functions can be nil.
type Plugin struct {
Name string
Setup6 SetupFunc6
Setup4 SetupFunc4
}
// RegisteredPlugins maps a plugin name to a Plugin instance.
var RegisteredPlugins = make(map[string]*Plugin)
// SetupFunc6 defines a plugin setup function for DHCPv6
type SetupFunc6 func(args ...string) (handler.Handler6, error)
// SetupFunc4 defines a plugin setup function for DHCPv6
type SetupFunc4 func(args ...string) (handler.Handler4, error)
// RegisterPlugin registers a plugin.
func RegisterPlugin(plugin *Plugin) error {
if plugin == nil {
return errors.New("cannot register nil plugin")
}
log.Printf("Registering plugin '%s'", plugin.Name)
if _, ok := RegisteredPlugins[plugin.Name]; ok {
// TODO this highlights that asking the plugins to register themselves
// is not the right approach. Need to register them in the main program.
log.Panicf("Plugin '%s' is already registered", plugin.Name)
}
RegisteredPlugins[plugin.Name] = plugin
return nil
}
// LoadPlugins reads a Config object and loads the plugins as specified in the
// `plugins` section, in order. For a plugin to be available, it must have been
// previously registered with plugins.RegisterPlugin. This is normally done at
// plugin import time.
// This function returns the list of loaded v6 plugins, the list of loaded v4
// plugins, and an error if any.
func LoadPlugins(conf *config.Config) ([]handler.Handler4, []handler.Handler6, error) {
log.Print("Loading plugins...")
handlers4 := make([]handler.Handler4, 0)
handlers6 := make([]handler.Handler6, 0)
if conf.Server6 == nil && conf.Server4 == nil {
return nil, nil, errors.New("no configuration found for either DHCPv6 or DHCPv4")
}
// now load the plugins. We need to call its setup function with
// the arguments extracted above. The setup function is mapped in
// plugins.RegisteredPlugins .
// Load DHCPv6 plugins.
if conf.Server6 != nil {
for _, pluginConf := range conf.Server6.Plugins {
if plugin, ok := RegisteredPlugins[pluginConf.Name]; ok {
log.Printf("DHCPv6: loading plugin `%s`", pluginConf.Name)
if plugin.Setup6 == nil {
log.Warningf("DHCPv6: plugin `%s` has no setup function for DHCPv6", pluginConf.Name)
continue
}
h6, err := plugin.Setup6(pluginConf.Args...)
if err != nil {
return nil, nil, err
} else if h6 == nil {
return nil, nil, config.ConfigErrorFromString("no DHCPv6 handler for plugin %s", pluginConf.Name)
}
handlers6 = append(handlers6, h6)
} else {
return nil, nil, config.ConfigErrorFromString("DHCPv6: unknown plugin `%s`", pluginConf.Name)
}
}
}
// Load DHCPv4 plugins. Yes, duplicated code, there's not really much that
// can be deduplicated here.
if conf.Server4 != nil {
for _, pluginConf := range conf.Server4.Plugins {
if plugin, ok := RegisteredPlugins[pluginConf.Name]; ok {
log.Printf("DHCPv4: loading plugin `%s`", pluginConf.Name)
if plugin.Setup4 == nil {
log.Warningf("DHCPv4: plugin `%s` has no setup function for DHCPv4", pluginConf.Name)
continue
}
h4, err := plugin.Setup4(pluginConf.Args...)
if err != nil {
return nil, nil, err
} else if h4 == nil {
return nil, nil, config.ConfigErrorFromString("no DHCPv4 handler for plugin %s", pluginConf.Name)
}
handlers4 = append(handlers4, h4)
} else {
return nil, nil, config.ConfigErrorFromString("DHCPv4: unknown plugin `%s`", pluginConf.Name)
}
}
}
return handlers4, handlers6, nil
}

271
plugins/prefix/plugin.go Normal file
View File

@@ -0,0 +1,271 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// Package prefix implements a plugin offering prefixes to clients requesting them
// This plugin attributes prefixes to clients requesting them with IA_PREFIX requests.
//
// Arguments for the plugin configuration are as follows, in this order:
// - prefix: The base prefix from which assigned prefixes are carved
// - max: maximum size of the prefix delegated to clients. When a client requests a larger prefix
// than this, this is the size of the offered prefix
package prefix
// FIXME: various settings will be hardcoded (default size, minimum size, lease times) pending a
// better configuration system
import (
"bytes"
"errors"
"fmt"
"net"
"strconv"
"sync"
"time"
"github.com/bits-and-blooms/bitset"
"github.com/insomniacslk/dhcp/dhcpv6"
dhcpIana "github.com/insomniacslk/dhcp/iana"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/coredhcp/coredhcp/plugins/allocators"
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
)
var log = logger.GetLogger("plugins/prefix")
// Plugin registers the prefix. Prefix delegation only exists for DHCPv6
var Plugin = plugins.Plugin{
Name: "prefix",
Setup6: setupPrefix,
}
const leaseDuration = 3600 * time.Second
func setupPrefix(args ...string) (handler.Handler6, error) {
// - prefix: 2001:db8::/48 64
if len(args) < 2 {
return nil, errors.New("Need both a subnet and an allocation max size")
}
_, prefix, err := net.ParseCIDR(args[0])
if err != nil {
return nil, fmt.Errorf("Invalid pool subnet: %v", err)
}
allocSize, err := strconv.Atoi(args[1])
if err != nil || allocSize > 128 || allocSize < 0 {
return nil, fmt.Errorf("Invalid prefix length: %v", err)
}
// TODO: select allocators based on heuristics or user configuration
alloc, err := bitmap.NewBitmapAllocator(*prefix, allocSize)
if err != nil {
return nil, fmt.Errorf("Could not initialize prefix allocator: %v", err)
}
return (&Handler{
Records: make(map[string][]lease),
allocator: alloc,
}).Handle, nil
}
type lease struct {
Prefix net.IPNet
Expire time.Time
}
// Handler holds state of allocations for the plugin
type Handler struct {
// Mutex here is the simplest implementation fit for purpose.
// We can revisit for perf when we move lease management to separate plugins
sync.Mutex
// Records has a string'd []byte as key, because []byte can't be a key itself
// Since it's not valid utf-8 we can't use any other string function though
Records map[string][]lease
allocator allocators.Allocator
}
// samePrefix returns true if both prefixes are defined and equal
// The empty prefix is equal to nothing, not even itself
func samePrefix(a, b *net.IPNet) bool {
if a == nil || b == nil {
return false
}
return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask)
}
// recordKey computes the key for the Records array from the client ID
func recordKey(d dhcpv6.DUID) string {
return string(d.ToBytes())
}
// Handle processes DHCPv6 packets for the prefix plugin for a given allocator/leaseset
func (h *Handler) Handle(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
msg, err := req.GetInnerMessage()
if err != nil {
log.Error(err)
return nil, true
}
client := msg.Options.ClientID()
if client == nil {
log.Error("Invalid packet received, no clientID")
return nil, true
}
// Each request IA_PD requires an IA_PD response
for _, iapd := range msg.Options.IAPD() {
if err != nil {
log.Errorf("Malformed IAPD received: %v", err)
resp.AddOption(&dhcpv6.OptStatusCode{StatusCode: dhcpIana.StatusMalformedQuery})
return resp, true
}
iapdResp := &dhcpv6.OptIAPD{
IaId: iapd.IaId,
}
// First figure out what prefixes the client wants
hints := iapd.Options.Prefixes()
if len(hints) == 0 {
// If there are no IAPrefix hints, this is still a valid IA_PD request (just
// unspecified) and we must attempt to allocate a prefix; so we include an empty hint
// which is equivalent to no hint
hints = []*dhcpv6.OptIAPrefix{{Prefix: &net.IPNet{}}}
}
// Bitmap to track which requests are already satisfied or not
satisfied := bitset.New(uint(len(hints)))
// A possible simple optimization here would be to be able to lock single map values
// individually instead of the whole map, since we lock for some amount of time
h.Lock()
knownLeases := h.Records[recordKey(client)]
// Bitmap to track which leases are already given in this exchange
givenOut := bitset.New(uint(len(knownLeases)))
// This is, for now, a set of heuristics, to reconcile the requests (prefix hints asked
// by the clients) with what's on offer (existing leases for this client, plus new blocks)
// Try to find leases that exactly match a hint, and extend them to satisfy the request
// This is the safest heuristic, if the lease matches exactly we know we aren't missing
// assigning it to a better candidate request
for hintIdx, h := range hints {
for leaseIdx := range knownLeases {
if samePrefix(h.Prefix, &knownLeases[leaseIdx].Prefix) {
expire := time.Now().Add(leaseDuration)
if knownLeases[leaseIdx].Expire.Before(expire) {
knownLeases[leaseIdx].Expire = expire
}
satisfied.Set(uint(hintIdx))
givenOut.Set(uint(leaseIdx))
addPrefix(iapdResp, knownLeases[leaseIdx])
}
}
}
// Then handle the empty hints, by giving out any remaining lease we
// have already assigned to this client
for hintIdx, h := range hints {
if satisfied.Test(uint(hintIdx)) ||
(h.Prefix != nil && !h.Prefix.IP.Equal(net.IPv6zero)) {
continue
}
for leaseIdx, l := range knownLeases {
if givenOut.Test(uint(leaseIdx)) {
continue
}
// If a length was requested, only give out prefixes of that length
// This is a bad heuristic depending on the allocator behavior, to be improved
if hintPrefixLen, _ := h.Prefix.Mask.Size(); hintPrefixLen != 0 {
leasePrefixLen, _ := l.Prefix.Mask.Size()
if hintPrefixLen != leasePrefixLen {
continue
}
}
expire := time.Now().Add(leaseDuration)
if knownLeases[leaseIdx].Expire.Before(expire) {
knownLeases[leaseIdx].Expire = expire
}
satisfied.Set(uint(hintIdx))
givenOut.Set(uint(leaseIdx))
addPrefix(iapdResp, knownLeases[leaseIdx])
}
}
// Now remains requests with a hint that we can't trivially satisfy, and possibly expired
// leases that haven't been explicitly requested again.
// A possible improvement here would be to try to widen existing leases, to satisfy wider
// requests that contain an existing leases; and to try to break down existing leases into
// smaller allocations, to satisfy requests for a subnet of an existing lease
// We probably don't need such complex behavior (the vast majority of requests will come
// with an empty, or length-only hint)
// Assign a new lease to satisfy the request
var newLeases []lease
for i, prefix := range hints {
if satisfied.Test(uint(i)) {
continue
}
if prefix.Prefix == nil {
// XXX: replace usage of dhcp.OptIAPrefix with a better struct in this inner
// function to avoid repeated nullpointer checks
prefix.Prefix = &net.IPNet{}
}
allocated, err := h.allocator.Allocate(*prefix.Prefix)
if err != nil {
log.Debugf("Nothing allocated for hinted prefix %s", prefix)
continue
}
l := lease{
Expire: time.Now().Add(leaseDuration),
Prefix: allocated,
}
addPrefix(iapdResp, l)
newLeases = append(knownLeases, l)
log.Debugf("Allocated %s to %s (IAID: %x)", &allocated, client, iapd.IaId)
}
if newLeases != nil {
h.Records[recordKey(client)] = newLeases
}
h.Unlock()
if len(iapdResp.Options.Options) == 0 {
log.Debugf("No valid prefix to return for IAID %x", iapd.IaId)
iapdResp.Options.Add(&dhcpv6.OptStatusCode{
StatusCode: dhcpIana.StatusNoPrefixAvail,
})
}
resp.AddOption(iapdResp)
}
return resp, false
}
func addPrefix(resp *dhcpv6.OptIAPD, l lease) {
lifetime := time.Until(l.Expire)
resp.Options.Add(&dhcpv6.OptIAPrefix{
PreferredLifetime: lifetime,
ValidLifetime: lifetime,
Prefix: dup(&l.Prefix),
})
}
func dup(src *net.IPNet) (dst *net.IPNet) {
dst = &net.IPNet{
IP: make(net.IP, net.IPv6len),
Mask: make(net.IPMask, net.IPv6len),
}
copy(dst.IP, src.IP)
copy(dst.Mask, src.Mask)
return dst
}

View File

@@ -0,0 +1,93 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package prefix
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv6"
dhcpIana "github.com/insomniacslk/dhcp/iana"
)
func TestRoundTrip(t *testing.T) {
reqIAID := [4]uint8{0x12, 0x34, 0x56, 0x78}
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
req.AddOption(dhcpv6.OptClientID(&dhcpv6.DUIDLL{
HWType: dhcpIana.HWTypeEthernet,
LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
}))
req.AddOption(&dhcpv6.OptIAPD{
IaId: reqIAID,
T1: 0,
T2: 0,
})
resp, err := dhcpv6.NewAdvertiseFromSolicit(req)
if err != nil {
t.Fatal(err)
}
handler, err := setupPrefix("2001:db8::/48", "64")
if err != nil {
t.Fatal(err)
}
result, final := handler(req, resp)
if final {
t.Log("Handler declared final")
}
t.Logf("%#v", result)
// Sanity checks on the response
success := result.GetOption(dhcpv6.OptionStatusCode)
var mo dhcpv6.MessageOptions
if len(success) > 1 {
t.Fatal("Got multiple StatusCode options")
} else if len(success) == 0 { // Everything OK
} else if err := mo.FromBytes(success[0].ToBytes()); err != nil || mo.Status().StatusCode != dhcpIana.StatusSuccess {
t.Fatalf("Did not get a (implicit or explicit) success status code: %v", success)
}
var iapd *dhcpv6.OptIAPD
{
// Check for IA_PD
iapds := result.(*dhcpv6.Message).Options.IAPD()
if len(iapds) != 1 {
t.Fatal("Malformed response, expected exactly 1 IAPD")
}
iapd = iapds[0]
}
if iapd.IaId != reqIAID {
t.Fatalf("IAID doesn't match: request %x, response: %x", iapd.IaId, reqIAID)
}
// Check the status code
if status := result.(*dhcpv6.Message).Options.Status(); status != nil && status.StatusCode != dhcpIana.StatusSuccess {
t.Fatalf("Did not get a (implicit or explicit) success status code: %v", success)
}
t.Logf("%#v", iapd)
// Check IAPrefix within IAPD
if len(iapd.Options.Prefixes()) != 1 {
t.Fatalf("Response did not contain exactly one prefix in the IA_PD option (found %s)",
iapd.Options.Prefixes())
}
}
func TestDup(t *testing.T) {
_, prefix, err := net.ParseCIDR("2001:db8::/48")
if err != nil {
panic("bad cidr")
}
dupPrefix := dup(prefix)
if !samePrefix(dupPrefix, prefix) {
t.Fatalf("dup doesn't work: got %v expected %v", dupPrefix, prefix)
}
}

193
plugins/range/plugin.go Normal file
View File

@@ -0,0 +1,193 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package rangeplugin
import (
"database/sql"
"encoding/binary"
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/coredhcp/coredhcp/grpc_server/dhcpServer"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/coredhcp/coredhcp/plugins/allocators"
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var log = logger.GetLogger("plugins/range")
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "range",
Setup4: setupRange,
}
//Record holds an IP lease record
type Record struct {
IP net.IP
Static bool
Expires int
Hostname string
}
// PluginState is the data held by an instance of the range plugin
type PluginState struct {
// Rough lock for the whole plugin, we'll get better performance once we use leasestorage
sync.Mutex
// Recordsv4 holds a MAC -> IP address and lease time mapping
Recordsv4 map[string]*Record
LeaseTime time.Duration
leasedb *sql.DB
allocator allocators.Allocator
}
var p PluginState
// Handler4 handles DHCPv4 packets for the range plugin
func (p *PluginState) Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
p.Lock()
defer p.Unlock()
record, ok := p.Recordsv4[req.ClientHWAddr.String()]
hostname := req.HostName()
if !ok {
// Allocating new address since there isn't one allocated
log.Printf("MAC address %s is new, leasing new IPv4 address", req.ClientHWAddr.String())
var netIp net.IP
var isStatic bool
if addr := GetStaticIp(req.ClientHWAddr.String()); addr != "" {
netIp = net.ParseIP(addr)
isStatic = true
} else {
ip, err := p.allocator.Allocate(net.IPNet{})
if err != nil {
log.Errorf("Could not allocate IP for MAC %s: %v", req.ClientHWAddr.String(), err)
return nil, true
}
netIp = ip.IP.To4()
}
rec := Record{
IP: netIp,
Static: isStatic,
Expires: int(time.Now().Add(p.LeaseTime).Unix()),
Hostname: hostname,
}
err := p.saveIPAddress(req.ClientHWAddr, &rec)
if err != nil {
log.Errorf("SaveIPAddress for MAC %s failed: %v", req.ClientHWAddr.String(), err)
}
p.Recordsv4[req.ClientHWAddr.String()] = &rec
record = &rec
} else {
if addr := GetStaticIp(req.ClientHWAddr.String()); addr != "" {
record.IP = net.ParseIP(addr)
record.Static = true
}
// Ensure we extend the existing lease at least past when the one we're giving expires
expiry := time.Unix(int64(record.Expires), 0)
if expiry.Before(time.Now().Add(p.LeaseTime)) {
record.Expires = int(time.Now().Add(p.LeaseTime).Round(time.Second).Unix())
record.Hostname = hostname
err := p.saveIPAddress(req.ClientHWAddr, record)
if err != nil {
log.Errorf("Could not persist lease for MAC %s: %v", req.ClientHWAddr.String(), err)
}
}
}
resp.YourIPAddr = record.IP
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second)))
log.Printf("found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String())
return resp, false
}
func GetRecord(clientHWAddr string) *Record {
p.Lock()
defer p.Unlock()
return p.Recordsv4[clientHWAddr]
}
func GetDhcpInfo() (*dhcpServer.DhcpInfo, error) {
p.Lock()
defer p.Unlock()
var dhcpInfo dhcpServer.DhcpInfo
for mac, record := range p.Recordsv4 {
if record.Static { continue }
endTime := time.Unix(int64(record.Expires), 0)
startTime := endTime.Add(-p.LeaseTime)
dhcpInfo.UeInfo = append(dhcpInfo.UeInfo, &dhcpServer.UeInfo{
Ip: record.IP.String(),
Mac: mac,
Hostname: record.Hostname,
StartTime: startTime.Format(time.DateTime),
EndTime: endTime.Format(time.DateTime),
})
}
return &dhcpInfo, nil
}
func setupRange(args ...string) (handler.Handler4, error) {
var err error
if len(args) < 4 {
return nil, fmt.Errorf("invalid number of arguments, want: 4 (file name, start IP, end IP, lease time), got: %d", len(args))
}
filename := args[0]
if filename == "" {
return nil, errors.New("file name cannot be empty")
}
ipRangeStart := net.ParseIP(args[1])
if ipRangeStart.To4() == nil {
return nil, fmt.Errorf("invalid IPv4 address: %v", args[1])
}
ipRangeEnd := net.ParseIP(args[2])
if ipRangeEnd.To4() == nil {
return nil, fmt.Errorf("invalid IPv4 address: %v", args[2])
}
if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) {
return nil, errors.New("start of IP range has to be lower than the end of an IP range")
}
p.allocator, err = bitmap.NewIPv4Allocator(ipRangeStart, ipRangeEnd)
if err != nil {
return nil, fmt.Errorf("could not create an allocator: %w", err)
}
p.LeaseTime, err = time.ParseDuration(args[3])
if err != nil {
return nil, fmt.Errorf("invalid lease duration: %v", args[3])
}
if err := p.registerBackingDB(filename); err != nil {
return nil, fmt.Errorf("could not setup lease storage: %w", err)
}
p.Recordsv4, err = loadRecords(p.leasedb)
if err != nil {
return nil, fmt.Errorf("could not load records from file: %v", err)
}
log.Printf("Loaded %d DHCPv4 leases from %s", len(p.Recordsv4), filename)
for _, v := range p.Recordsv4 {
if v.Static {
continue
}
ip, err := p.allocator.Allocate(net.IPNet{IP: v.IP})
if err != nil {
return nil, fmt.Errorf("failed to re-allocate leased ip %v: %v", v.IP.String(), err)
}
if ip.IP.String() != v.IP.String() {
return nil, fmt.Errorf("allocator did not re-allocate requested leased ip %v: %v", v.IP.String(), ip.String())
}
}
importStaticIpFile()
return p.Handler4, nil
}

169
plugins/range/plugin.go.new Normal file
View File

@@ -0,0 +1,169 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package rangeplugin
import (
"database/sql"
"encoding/binary"
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/coredhcp/coredhcp/plugins/allocators"
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var log = logger.GetLogger("plugins/range")
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "range",
Setup4: setupRange,
}
//Record holds an IP lease record
type Record struct {
IP net.IP
expires int
hostname string
}
// PluginState is the data held by an instance of the range plugin
type PluginState struct {
// Rough lock for the whole plugin, we'll get better performance once we use leasestorage
sync.Mutex
// Recordsv4 holds a MAC -> IP address and lease time mapping
Recordsv4 map[string]*Record
LeaseTime time.Duration
leasedb *sql.DB
allocator allocators.Allocator
}
var p PluginState
// Handler4 handles DHCPv4 packets for the range plugin
func (p *PluginState) Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
p.Lock()
defer p.Unlock()
if ip := GetStaticIp(req.ClientHWAddr.String()); ip != "" {
resp.YourIPAddr = net.ParseIP(ip)
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second)))
log.Printf("found static IP address %s for MAC %s", ip, req.ClientHWAddr.String())
return resp, false
}
record, ok := p.Recordsv4[req.ClientHWAddr.String()]
hostname := req.HostName()
if !ok {
// Allocating new address since there isn't one allocated
log.Printf("MAC address %s is new, leasing new IPv4 address", req.ClientHWAddr.String())
ip, err := p.allocator.Allocate(net.IPNet{})
if err != nil {
log.Errorf("Could not allocate IP for MAC %s: %v", req.ClientHWAddr.String(), err)
return nil, true
}
rec := Record{
IP: ip.IP.To4(),
expires: int(time.Now().Add(p.LeaseTime).Unix()),
hostname: hostname,
}
err = p.saveIPAddress(req.ClientHWAddr, &rec)
if err != nil {
log.Errorf("SaveIPAddress for MAC %s failed: %v", req.ClientHWAddr.String(), err)
}
p.Recordsv4[req.ClientHWAddr.String()] = &rec
record = &rec
} else {
// Ensure we extend the existing lease at least past when the one we're giving expires
expiry := time.Unix(int64(record.expires), 0)
if expiry.Before(time.Now().Add(p.LeaseTime)) {
record.expires = int(time.Now().Add(p.LeaseTime).Round(time.Second).Unix())
record.hostname = hostname
err := p.saveIPAddress(req.ClientHWAddr, record)
if err != nil {
log.Errorf("Could not persist lease for MAC %s: %v", req.ClientHWAddr.String(), err)
}
}
}
resp.YourIPAddr = record.IP
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second)))
log.Printf("found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String())
return resp, false
}
func GetRecord(clientHWAddr string) (string, string) {
p.Lock()
defer p.Unlock()
record := p.Recordsv4[clientHWAddr]
if record != nil {
return record.IP.String(), record.hostname
} else {
return "-", "-"
}
}
func setupRange(args ...string) (handler.Handler4, error) {
var err error
if len(args) < 4 {
return nil, fmt.Errorf("invalid number of arguments, want: 4 (file name, start IP, end IP, lease time), got: %d", len(args))
}
filename := args[0]
if filename == "" {
return nil, errors.New("file name cannot be empty")
}
ipRangeStart := net.ParseIP(args[1])
if ipRangeStart.To4() == nil {
return nil, fmt.Errorf("invalid IPv4 address: %v", args[1])
}
ipRangeEnd := net.ParseIP(args[2])
if ipRangeEnd.To4() == nil {
return nil, fmt.Errorf("invalid IPv4 address: %v", args[2])
}
if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) {
return nil, errors.New("start of IP range has to be lower than the end of an IP range")
}
p.allocator, err = bitmap.NewIPv4Allocator(ipRangeStart, ipRangeEnd)
if err != nil {
return nil, fmt.Errorf("could not create an allocator: %w", err)
}
p.LeaseTime, err = time.ParseDuration(args[3])
if err != nil {
return nil, fmt.Errorf("invalid lease duration: %v", args[3])
}
if err := p.registerBackingDB(filename); err != nil {
return nil, fmt.Errorf("could not setup lease storage: %w", err)
}
p.Recordsv4, err = loadRecords(p.leasedb)
if err != nil {
return nil, fmt.Errorf("could not load records from file: %v", err)
}
log.Printf("Loaded %d DHCPv4 leases from %s", len(p.Recordsv4), filename)
for _, v := range p.Recordsv4 {
ip, err := p.allocator.Allocate(net.IPNet{IP: v.IP})
if err != nil {
return nil, fmt.Errorf("failed to re-allocate leased ip %v: %v", v.IP.String(), err)
}
if ip.IP.String() != v.IP.String() {
return nil, fmt.Errorf("allocator did not re-allocate requested leased ip %v: %v", v.IP.String(), ip.String())
}
}
importStaticIpFile()
return p.Handler4, nil
}

View File

@@ -0,0 +1,50 @@
package rangeplugin
import (
"encoding/csv"
"fmt"
"io"
"os"
)
const staticIpFile string = "./static_ip.csv"
var staticIpPool map[string]string // mac as key
var macPool map[string]string // ip as key
func importStaticIpFile() {
fs, err := os.Open(staticIpFile)
if err != nil {
//fmt.Println(err)
return
}
defer fs.Close()
staticIpPool = make(map[string]string)
macPool = make(map[string]string)
r := csv.NewReader(fs)
for {
row, err := r.Read()
if err != nil && err != io.EOF {
fmt.Printf("Can not read, err is %+v", err)
}
if err == io.EOF {
break
}
//fmt.Println(row)
if len(row) > 1 {
staticIpPool[row[0]] = row[1]
macPool[row[1]] = row[0]
}
}
}
func GetStaticIp(mac string) string {
return staticIpPool[mac]
}
func IsStaticIp(ip string) bool {
_, ok := macPool[ip]
return ok
}

93
plugins/range/storage.go Normal file
View File

@@ -0,0 +1,93 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package rangeplugin
import (
"database/sql"
"errors"
"fmt"
"net"
_ "github.com/mattn/go-sqlite3"
)
func loadDB(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", path))
if err != nil {
return nil, fmt.Errorf("failed to open database (%T): %w", err, err)
}
if _, err := db.Exec("create table if not exists leases4 (mac string not null, ip string not null, static bool, expiry int, hostname string not null, primary key (mac, ip))"); err != nil {
return nil, fmt.Errorf("table creation failed: %w", err)
}
return db, nil
}
// loadRecords loads the DHCPv6/v4 Records global map with records stored on
// the specified file. The records have to be one per line, a mac address and an
// IP address.
func loadRecords(db *sql.DB) (map[string]*Record, error) {
rows, err := db.Query("select mac, ip, static, expiry, hostname from leases4")
if err != nil {
return nil, fmt.Errorf("failed to query leases database: %w", err)
}
defer rows.Close()
var (
mac, ip, hostname string
static bool
expiry int
records = make(map[string]*Record)
)
for rows.Next() {
if err := rows.Scan(&mac, &ip, &static, &expiry, &hostname); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
hwaddr, err := net.ParseMAC(mac)
if err != nil {
return nil, fmt.Errorf("malformed hardware address: %s", mac)
}
ipaddr := net.ParseIP(ip)
if ipaddr.To4() == nil {
return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr)
}
records[hwaddr.String()] = &Record{IP: ipaddr, Static: static, Expires: expiry, Hostname: hostname}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed lease database row scanning: %w", err)
}
return records, nil
}
// saveIPAddress writes out a lease to storage
func (p *PluginState) saveIPAddress(mac net.HardwareAddr, record *Record) error {
stmt, err := p.leasedb.Prepare(`insert or replace into leases4(mac, ip, static, expiry, hostname) values (?, ?, ?, ?, ?)`)
if err != nil {
return fmt.Errorf("statement preparation failed: %w", err)
}
defer stmt.Close()
if _, err := stmt.Exec(
mac.String(),
record.IP.String(),
record.Static,
record.Expires,
record.Hostname,
); err != nil {
return fmt.Errorf("record insert/update failed: %w", err)
}
return nil
}
// registerBackingDB installs a database connection string as the backing store for leases
func (p *PluginState) registerBackingDB(filename string) error {
if p.leasedb != nil {
return errors.New("cannot swap out a lease database while running")
}
// We never close this, but that's ok because plugins are never stopped/unregistered
newLeaseDB, err := loadDB(filename)
if err != nil {
return fmt.Errorf("failed to open lease database %s: %w", filename, err)
}
p.leasedb = newLeaseDB
return nil
}

View File

@@ -0,0 +1,99 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package rangeplugin
import (
"database/sql"
"fmt"
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func testDBSetup() (*sql.DB, error) {
db, err := loadDB(":memory:")
if err != nil {
return nil, err
}
for _, record := range records {
stmt, err := db.Prepare("insert into leases4(mac, ip, expiry, hostname) values (?, ?, ?, ?)")
if err != nil {
return nil, fmt.Errorf("failed to prepare insert statement: %w", err)
}
defer stmt.Close()
if _, err := stmt.Exec(record.mac, record.ip.IP.String(), record.ip.expires, record.ip.hostname); err != nil {
return nil, fmt.Errorf("failed to insert record into test db: %w", err)
}
}
return db, nil
}
var expire = int(time.Date(2000, 01, 01, 00, 00, 00, 00, time.UTC).Unix())
var records = []struct {
mac string
ip *Record
}{
{"02:00:00:00:00:00", &Record{IP: net.IPv4(10, 0, 0, 0), expires: expire, hostname: "zero"}},
{"02:00:00:00:00:01", &Record{IP: net.IPv4(10, 0, 0, 1), expires: expire, hostname: "one"}},
{"02:00:00:00:00:02", &Record{IP: net.IPv4(10, 0, 0, 2), expires: expire, hostname: "two"}},
{"02:00:00:00:00:03", &Record{IP: net.IPv4(10, 0, 0, 3), expires: expire, hostname: "three"}},
{"02:00:00:00:00:04", &Record{IP: net.IPv4(10, 0, 0, 4), expires: expire, hostname: "four"}},
{"02:00:00:00:00:05", &Record{IP: net.IPv4(10, 0, 0, 5), expires: expire, hostname: "five"}},
}
func TestLoadRecords(t *testing.T) {
db, err := testDBSetup()
if err != nil {
t.Fatalf("Failed to set up test DB: %v", err)
}
parsedRec, err := loadRecords(db)
if err != nil {
t.Fatalf("Failed to load records from file: %v", err)
}
mapRec := make(map[string]*Record)
for _, rec := range records {
var (
ip, mac, hostname string
expiry int
)
if err := db.QueryRow("select mac, ip, expiry, hostname from leases4 where mac = ?", rec.mac).Scan(&mac, &ip, &expiry, &hostname); err != nil {
t.Fatalf("record not found for mac=%s: %v", rec.mac, err)
}
mapRec[mac] = &Record{IP: net.ParseIP(ip), expires: expiry, hostname: hostname}
}
assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB")
}
func TestWriteRecords(t *testing.T) {
pl := PluginState{}
if err := pl.registerBackingDB(":memory:"); err != nil {
t.Fatalf("Could not setup file")
}
mapRec := make(map[string]*Record)
for _, rec := range records {
hwaddr, err := net.ParseMAC(rec.mac)
if err != nil {
// bug in testdata
panic(err)
}
if err := pl.saveIPAddress(hwaddr, rec.ip); err != nil {
t.Errorf("Failed to save ip for %s: %v", hwaddr, err)
}
mapRec[hwaddr.String()] = &Record{IP: rec.ip.IP, expires: rec.ip.expires, hostname: rec.ip.hostname}
}
parsedRec, err := loadRecords(pl.leasedb)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB")
}

49
plugins/router/plugin.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package router
import (
"errors"
"net"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var log = logger.GetLogger("plugins/router")
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "router",
Setup4: setup4,
}
var (
routers []net.IP
)
func setup4(args ...string) (handler.Handler4, error) {
log.Printf("Loaded plugin for DHCPv4.")
if len(args) < 1 {
return nil, errors.New("need at least one router IP address")
}
for _, arg := range args {
router := net.ParseIP(arg)
if router.To4() == nil {
return Handler4, errors.New("expected an router IP address, got: " + arg)
}
routers = append(routers, router)
}
log.Infof("loaded %d router IP addresses.", len(routers))
return Handler4, nil
}
//Handler4 handles DHCPv4 packets for the router plugin
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
resp.Options.Update(dhcpv4.OptRouter(routers...))
return resp, false
}

View File

@@ -0,0 +1,77 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package searchdomains
// This is an searchdomains plugin that adds default DNS search domains.
import (
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/rfc1035label"
)
var log = logger.GetLogger("plugins/searchdomains")
// Plugin wraps the default DNS search domain options.
// Note that importing the plugin is not enough to use it: you have to
// explicitly specify the intention to use it in the `config.yml` file, in the
// plugins section. For searchdomains:
//
// server6:
// listen: '[::]547'
// - searchdomains: domain.a domain.b
// - server_id: LL aa:bb:cc:dd:ee:ff
// - file: "leases.txt"
//
var Plugin = plugins.Plugin{
Name: "searchdomains",
Setup6: setup6,
Setup4: setup4,
}
// These are the DNS search domains that are set by the plugin.
// Note that DHCPv4 and DHCPv6 options are totally independent.
// If you need the same settings for both, you'll need to configure
// this plugin once for the v4 and once for the v6 server.
var v4SearchList []string
var v6SearchList []string
// copySlice creates a new copy of a string slice in memory.
// This helps to ensure that downstream plugins can't corrupt
// this plugin's configuration
func copySlice(original []string) []string {
copied := make([]string, len(original))
copy(copied, original)
return copied
}
func setup6(args ...string) (handler.Handler6, error) {
v6SearchList = args
log.Printf("Registered domain search list (DHCPv6) %s", v6SearchList)
return domainSearchListHandler6, nil
}
func setup4(args ...string) (handler.Handler4, error) {
v4SearchList = args
log.Printf("Registered domain search list (DHCPv4) %s", v4SearchList)
return domainSearchListHandler4, nil
}
func domainSearchListHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
resp.UpdateOption(dhcpv6.OptDomainSearchList(&rfc1035label.Labels{
Labels: copySlice(v6SearchList),
}))
return resp, false
}
func domainSearchListHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
resp.UpdateOption(dhcpv4.OptDomainSearch(&rfc1035label.Labels{
Labels: copySlice(v4SearchList),
}))
return resp, false
}

View File

@@ -0,0 +1,96 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package searchdomains
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/stretchr/testify/assert"
)
func TestAddDomains6(t *testing.T) {
assert := assert.New(t)
// Search domains we will expect the DHCP server to assign
searchDomains := []string{"domain.a", "domain.b"}
// Init plugin
handler6, err := Plugin.Setup6(searchDomains...)
if err != nil {
t.Fatal(err)
}
// Fake request
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
req.MessageType = dhcpv6.MessageTypeRequest
req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionDNSRecursiveNameServer))
// Fake response input
stub, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
stub.MessageType = dhcpv6.MessageTypeReply
// Call plugin
resp, stop := handler6(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
searchLabels := resp.(*dhcpv6.Message).Options.DomainSearchList().Labels
assert.Equal(searchDomains, searchLabels)
}
func TestAddDomains4(t *testing.T) {
assert := assert.New(t)
// Search domains we will expect the DHCP server to assign
// NOTE: these domains should be different from the v6 test domains;
// this tests that we haven't accidentally set the v6 domains in the
// v4 plugin handler or vice versa.
searchDomains := []string{"domain.b", "domain.c"}
// Init plugin
handler4, err := Plugin.Setup4(searchDomains...)
if err != nil {
t.Fatal(err)
}
// Fake request
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
if err != nil {
t.Fatal(err)
}
// Fake response input
stub, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
t.Fatal(err)
}
// Call plugin
resp, stop := handler4(req, stub)
if resp == nil {
t.Fatal("plugin did not return a message")
}
if stop {
t.Error("plugin interrupted processing")
}
searchLabels := resp.DomainSearch().Labels
assert.Equal(searchDomains, searchLabels)
}

155
plugins/serverid/plugin.go Normal file
View File

@@ -0,0 +1,155 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package serverid
import (
"errors"
"net"
"strings"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/iana"
)
var log = logger.GetLogger("plugins/server_id")
// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "server_id",
Setup6: setup6,
Setup4: setup4,
}
// v6ServerID is the DUID of the v6 server
var (
v6ServerID dhcpv6.DUID
v4ServerID net.IP
)
// Handler6 handles DHCPv6 packets for the server_id plugin.
func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
if v6ServerID == nil {
log.Fatal("BUG: Plugin is running uninitialized!")
return nil, true
}
msg, err := req.GetInnerMessage()
if err != nil {
// BUG: this should already have failed in the main handler. Abort
log.Error(err)
return nil, true
}
if sid := msg.Options.ServerID(); sid != nil {
// RFC8415 §16.{2,5,7}
// These message types MUST be discarded if they contain *any* ServerID option
if msg.MessageType == dhcpv6.MessageTypeSolicit ||
msg.MessageType == dhcpv6.MessageTypeConfirm ||
msg.MessageType == dhcpv6.MessageTypeRebind {
return nil, true
}
// Approximately all others MUST be discarded if the ServerID doesn't match
if !sid.Equal(v6ServerID) {
log.Infof("requested server ID does not match this server's ID. Got %v, want %v", sid, v6ServerID)
return nil, true
}
} else if msg.MessageType == dhcpv6.MessageTypeRequest ||
msg.MessageType == dhcpv6.MessageTypeRenew ||
msg.MessageType == dhcpv6.MessageTypeDecline ||
msg.MessageType == dhcpv6.MessageTypeRelease {
// RFC8415 §16.{6,8,10,11}
// These message types MUST be discarded if they *don't* contain a ServerID option
return nil, true
}
dhcpv6.WithServerID(v6ServerID)(resp)
return resp, false
}
// Handler4 handles DHCPv4 packets for the server_id plugin.
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
if v4ServerID == nil {
log.Fatal("BUG: Plugin is running uninitialized!")
return nil, true
}
if req.OpCode != dhcpv4.OpcodeBootRequest {
log.Warningf("not a BootRequest, ignoring")
return resp, false
}
if req.ServerIPAddr != nil &&
!req.ServerIPAddr.Equal(net.IPv4zero) &&
!req.ServerIPAddr.Equal(v4ServerID) {
// This request is not for us, drop it.
log.Infof("requested server ID does not match this server's ID. Got %v, want %v", req.ServerIPAddr, v4ServerID)
return nil, true
}
resp.ServerIPAddr = make(net.IP, net.IPv4len)
copy(resp.ServerIPAddr[:], v4ServerID)
resp.UpdateOption(dhcpv4.OptServerIdentifier(v4ServerID))
return resp, false
}
func setup4(args ...string) (handler.Handler4, error) {
log.Printf("loading `server_id` plugin for DHCPv4 with args: %v", args)
if len(args) < 1 {
return nil, errors.New("need an argument")
}
serverID := net.ParseIP(args[0])
if serverID == nil {
return nil, errors.New("invalid or empty IP address")
}
if serverID.To4() == nil {
return nil, errors.New("not a valid IPv4 address")
}
v4ServerID = serverID.To4()
return Handler4, nil
}
func setup6(args ...string) (handler.Handler6, error) {
log.Printf("loading `server_id` plugin for DHCPv6 with args: %v", args)
if len(args) < 2 {
return nil, errors.New("need a DUID type and value")
}
duidType := args[0]
if duidType == "" {
return nil, errors.New("got empty DUID type")
}
duidValue := args[1]
if duidValue == "" {
return nil, errors.New("got empty DUID value")
}
duidType = strings.ToLower(duidType)
hwaddr, err := net.ParseMAC(duidValue)
if err != nil {
return nil, err
}
switch duidType {
case "ll", "duid-ll", "duid_ll":
v6ServerID = &dhcpv6.DUIDLL{
// sorry, only ethernet for now
HWType: iana.HWTypeEthernet,
LinkLayerAddr: hwaddr,
}
case "llt", "duid-llt", "duid_llt":
v6ServerID = &dhcpv6.DUIDLLT{
// sorry, zero-time for now
Time: 0,
// sorry, only ethernet for now
HWType: iana.HWTypeEthernet,
LinkLayerAddr: hwaddr,
}
case "en", "uuid":
return nil, errors.New("EN/UUID DUID type not supported yet")
default:
return nil, errors.New("Opaque DUID type not supported yet")
}
log.Printf("using %s %s", duidType, duidValue)
return Handler6, nil
}

View File

@@ -0,0 +1,127 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package serverid
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv6"
)
func makeTestDUID(uuid string) dhcpv6.DUID {
var uuidb [16]byte
copy(uuidb[:], uuid)
return &dhcpv6.DUIDUUID{
UUID: uuidb,
}
}
func TestRejectBadServerIDV6(t *testing.T) {
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
v6ServerID = makeTestDUID("0000000000000000")
req.MessageType = dhcpv6.MessageTypeRenew
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
dhcpv6.WithServerID(makeTestDUID("0000000000000001"))(req)
stub, err := dhcpv6.NewReplyFromMessage(req)
if err != nil {
t.Fatal(err)
}
resp, stop := Handler6(req, stub)
if resp != nil {
t.Error("server_id is sending a response message to a request with mismatched ServerID")
}
if !stop {
t.Error("server_id did not interrupt processing on a request with mismatched ServerID")
}
}
func TestRejectUnexpectedServerIDV6(t *testing.T) {
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
v6ServerID = makeTestDUID("0000000000000000")
req.MessageType = dhcpv6.MessageTypeSolicit
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
dhcpv6.WithServerID(makeTestDUID("0000000000000000"))(req)
stub, err := dhcpv6.NewAdvertiseFromSolicit(req)
if err != nil {
t.Fatal(err)
}
resp, stop := Handler6(req, stub)
if resp != nil {
t.Error("server_id is sending a response message to a solicit with a ServerID")
}
if !stop {
t.Error("server_id did not interrupt processing on a solicit with a ServerID")
}
}
func TestAddServerIDV6(t *testing.T) {
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
v6ServerID = makeTestDUID("0000000000000000")
req.MessageType = dhcpv6.MessageTypeRebind
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
stub, err := dhcpv6.NewReplyFromMessage(req)
if err != nil {
t.Fatal(err)
}
resp, _ := Handler6(req, stub)
if resp == nil {
t.Fatal("plugin did not return an answer")
}
if opt := resp.(*dhcpv6.Message).Options.ServerID(); opt == nil {
t.Fatal("plugin did not add a ServerID option")
} else if !opt.Equal(v6ServerID) {
t.Fatalf("Got unexpected DUID: expected %v, got %v", v6ServerID, opt)
}
}
func TestRejectInnerMessageServerID(t *testing.T) {
req, err := dhcpv6.NewMessage()
if err != nil {
t.Fatal(err)
}
v6ServerID = makeTestDUID("0000000000000000")
req.MessageType = dhcpv6.MessageTypeSolicit
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
dhcpv6.WithServerID(makeTestDUID("0000000000000000"))(req)
stub, err := dhcpv6.NewAdvertiseFromSolicit(req)
if err != nil {
t.Fatal(err)
}
relayedRequest, err := dhcpv6.EncapsulateRelay(req, dhcpv6.MessageTypeRelayForward, net.IPv6loopback, net.IPv6loopback)
if err != nil {
t.Fatal(err)
}
resp, stop := Handler6(relayedRequest, stub)
if resp != nil {
t.Error("server_id is sending a response message to a relayed solicit with a ServerID")
}
if !stop {
t.Error("server_id did not interrupt processing on a relayed solicit with a ServerID")
}
}

89
plugins/sleep/plugin.go Normal file
View File

@@ -0,0 +1,89 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package sleep
// This plugin introduces a delay in the DHCP response.
import (
"fmt"
"time"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
var (
pluginName = "sleep"
log = logger.GetLogger("plugins/" + pluginName)
)
// Example configuration of the `sleep` plugin:
//
// server4:
// plugins:
// - sleep 300ms
// - file: "leases4.txt"
//
// server6:
// plugins:
// - sleep 1s
// - file: "leases6.txt"
//
// For the duration format, see the documentation of `time.ParseDuration`,
// https://golang.org/pkg/time/#ParseDuration .
// Plugin contains the `sleep` plugin data.
var Plugin = plugins.Plugin{
Name: pluginName,
Setup6: setup6,
Setup4: setup4,
}
func setup6(args ...string) (handler.Handler6, error) {
if len(args) != 1 {
return nil, fmt.Errorf("want exactly one argument, got %d", len(args))
}
delay, err := time.ParseDuration(args[0])
if err != nil {
return nil, fmt.Errorf("failed to parse duration: %w", err)
}
log.Printf("loaded plugin for DHCPv6.")
return makeSleepHandler6(delay), nil
}
func setup4(args ...string) (handler.Handler4, error) {
if len(args) != 1 {
return nil, fmt.Errorf("want exactly one argument, got %d", len(args))
}
delay, err := time.ParseDuration(args[0])
if err != nil {
return nil, fmt.Errorf("failed to parse duration: %w", err)
}
log.Printf("loaded plugin for DHCPv4.")
return makeSleepHandler4(delay), nil
}
func makeSleepHandler6(delay time.Duration) handler.Handler6 {
return func(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
log.Printf("introducing delay of %s in response", delay)
// return the unmodified response, and instruct coredhcp to continue to
// the next plugin.
time.Sleep(delay)
return resp, false
}
}
func makeSleepHandler4(delay time.Duration) handler.Handler4 {
return func(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
log.Printf("introducing delay of %s in response", delay)
// return the unmodified response, and instruct coredhcp to continue to
// the next plugin.
time.Sleep(delay)
return resp, false
}
}

View File

@@ -0,0 +1,73 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package staticroute
import (
"errors"
"net"
"strings"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var log = logger.GetLogger("plugins/staticroute")
// Plugin wraps the information necessary to register a plugin.
var Plugin = plugins.Plugin{
Name: "staticroute",
Setup4: setup4,
}
var routes dhcpv4.Routes
func setup4(args ...string) (handler.Handler4, error) {
log.Printf("loaded plugin for DHCPv4.")
routes = make(dhcpv4.Routes, 0)
if len(args) < 1 {
return nil, errors.New("need at least one static route")
}
var err error
for _, arg := range args {
fields := strings.Split(arg, ",")
if len(fields) != 2 {
return Handler4, errors.New("expected a destination/gateway pair, got: " + arg)
}
route := &dhcpv4.Route{}
_, route.Dest, err = net.ParseCIDR(fields[0])
if err != nil {
return Handler4, errors.New("expected a destination subnet, got: " + fields[0])
}
route.Router = net.ParseIP(fields[1])
if route.Router == nil {
return Handler4, errors.New("expected a gateway address, got: " + fields[1])
}
routes = append(routes, route)
log.Debugf("adding static route %s", route)
}
log.Printf("loaded %d static routes.", len(routes))
return Handler4, nil
}
// Handler4 handles DHCPv4 packets for the static routes plugin
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
if len(routes) > 0 {
resp.Options.Update(dhcpv4.Option{
Code: dhcpv4.OptionCode(dhcpv4.OptionClasslessStaticRoute),
Value: routes,
})
}
return resp, false
}

View File

@@ -0,0 +1,60 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package staticroute
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetup4(t *testing.T) {
assert.Empty(t, routes)
var err error
// no args
_, err = setup4()
if assert.Error(t, err) {
assert.Equal(t, "need at least one static route", err.Error())
}
// invalid arg
_, err = setup4("foo")
if assert.Error(t, err) {
assert.Equal(t, "expected a destination/gateway pair, got: foo", err.Error())
}
// invalid destination
_, err = setup4("foo,")
if assert.Error(t, err) {
assert.Equal(t, "expected a destination subnet, got: foo", err.Error())
}
// invalid gateway
_, err = setup4("10.0.0.0/8,foo")
if assert.Error(t, err) {
assert.Equal(t, "expected a gateway address, got: foo", err.Error())
}
// valid route
_, err = setup4("10.0.0.0/8,192.168.1.1")
if assert.NoError(t, err) {
if assert.Equal(t, 1, len(routes)) {
assert.Equal(t, "10.0.0.0/8", routes[0].Dest.String())
assert.Equal(t, "192.168.1.1", routes[0].Router.String())
}
}
// multiple valid routes
_, err = setup4("10.0.0.0/8,192.168.1.1", "192.168.2.0/24,192.168.1.100")
if assert.NoError(t, err) {
if assert.Equal(t, 2, len(routes)) {
assert.Equal(t, "10.0.0.0/8", routes[0].Dest.String())
assert.Equal(t, "192.168.1.1", routes[0].Router.String())
assert.Equal(t, "192.168.2.0/24", routes[1].Dest.String())
assert.Equal(t, "192.168.1.100", routes[1].Router.String())
}
}
}

241
server/handle.go Normal file
View File

@@ -0,0 +1,241 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package server
import (
"errors"
"fmt"
"net"
"sync"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
)
// HandleMsg6 runs for every received DHCPv6 packet. It will run every
// registered handler in sequence, and reply with the resulting response.
// It will not reply if the resulting response is `nil`.
func (l *listener6) HandleMsg6(buf []byte, oob *ipv6.ControlMessage, peer *net.UDPAddr) {
d, err := dhcpv6.FromBytes(buf)
bufpool.Put(&buf)
if err != nil {
log.Printf("Error parsing DHCPv6 request: %v", err)
return
}
// decapsulate the relay message
msg, err := d.GetInnerMessage()
if err != nil {
log.Warningf("DHCPv6: cannot get inner message: %v", err)
return
}
// Create a suitable basic response packet
var resp dhcpv6.DHCPv6
switch msg.Type() {
case dhcpv6.MessageTypeSolicit:
if msg.GetOneOption(dhcpv6.OptionRapidCommit) != nil {
resp, err = dhcpv6.NewReplyFromMessage(msg)
} else {
resp, err = dhcpv6.NewAdvertiseFromSolicit(msg)
}
case dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeConfirm, dhcpv6.MessageTypeRenew,
dhcpv6.MessageTypeRebind, dhcpv6.MessageTypeRelease, dhcpv6.MessageTypeInformationRequest:
resp, err = dhcpv6.NewReplyFromMessage(msg)
default:
err = fmt.Errorf("MainHandler6: message type %d not supported", msg.Type())
}
if err != nil {
log.Printf("MainHandler6: NewReplyFromDHCPv6Message failed: %v", err)
return
}
var stop bool
for _, handler := range l.handlers {
resp, stop = handler(d, resp)
if stop {
break
}
}
if resp == nil {
log.Print("MainHandler6: dropping request because response is nil")
return
}
// if the request was relayed, re-encapsulate the response
if d.IsRelay() {
if rmsg, ok := resp.(*dhcpv6.Message); !ok {
log.Warningf("DHCPv6: response is a relayed message, not reencapsulating")
} else {
tmp, err := dhcpv6.NewRelayReplFromRelayForw(d.(*dhcpv6.RelayMessage), rmsg)
if err != nil {
log.Warningf("DHCPv6: cannot create relay-repl from relay-forw: %v", err)
return
}
resp = tmp
}
}
var woob *ipv6.ControlMessage
if peer.IP.IsLinkLocalUnicast() {
// LL need to be directed to the correct interface. Globally reachable
// addresses should use the default route, in case of asymetric routing.
switch {
case l.Interface.Index != 0:
woob = &ipv6.ControlMessage{IfIndex: l.Interface.Index}
case oob != nil && oob.IfIndex != 0:
woob = &ipv6.ControlMessage{IfIndex: oob.IfIndex}
default:
log.Errorf("HandleMsg6: Did not receive interface information")
}
}
if _, err := l.WriteTo(resp.ToBytes(), woob, peer); err != nil {
log.Printf("MainHandler6: conn.Write to %v failed: %v", peer, err)
}
}
func (l *listener4) HandleMsg4(buf []byte, oob *ipv4.ControlMessage, _peer net.Addr) {
var (
resp, tmp *dhcpv4.DHCPv4
err error
stop bool
)
req, err := dhcpv4.FromBytes(buf)
bufpool.Put(&buf)
if err != nil {
log.Printf("Error parsing DHCPv4 request: %v", err)
return
}
if req.OpCode != dhcpv4.OpcodeBootRequest {
log.Printf("MainHandler4: unsupported opcode %d. Only BootRequest (%d) is supported", req.OpCode, dhcpv4.OpcodeBootRequest)
return
}
tmp, err = dhcpv4.NewReplyFromRequest(req)
if err != nil {
log.Printf("MainHandler4: failed to build reply: %v", err)
return
}
switch mt := req.MessageType(); mt {
case dhcpv4.MessageTypeDiscover:
tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
case dhcpv4.MessageTypeRequest:
tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
default:
log.Printf("plugins/server: Unhandled message type: %v", mt)
return
}
resp = tmp
for _, handler := range l.handlers {
resp, stop = handler(req, resp)
if stop {
break
}
}
if resp != nil {
useEthernet := false
var peer *net.UDPAddr
if !req.GatewayIPAddr.IsUnspecified() {
// TODO: make RFC8357 compliant
peer = &net.UDPAddr{IP: req.GatewayIPAddr, Port: dhcpv4.ServerPort}
} else if resp.MessageType() == dhcpv4.MessageTypeNak {
peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort}
} else if !req.ClientIPAddr.IsUnspecified() {
peer = &net.UDPAddr{IP: req.ClientIPAddr, Port: dhcpv4.ClientPort}
} else if req.IsBroadcast() {
peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort}
} else {
//sends a layer2 frame so that we can define the destination MAC address
peer = &net.UDPAddr{IP: resp.YourIPAddr, Port: dhcpv4.ClientPort}
useEthernet = true
}
var woob *ipv4.ControlMessage
if peer.IP.Equal(net.IPv4bcast) || peer.IP.IsLinkLocalUnicast() || useEthernet {
// Direct broadcasts, link-local and layer2 unicasts to the interface the request was
// received on. Other packets should use the normal routing table in
// case of asymetric routing
switch {
case l.Interface.Index != 0:
woob = &ipv4.ControlMessage{IfIndex: l.Interface.Index}
case oob != nil && oob.IfIndex != 0:
woob = &ipv4.ControlMessage{IfIndex: oob.IfIndex}
default:
log.Errorf("HandleMsg4: Did not receive interface information")
}
}
if useEthernet {
intf, err := net.InterfaceByIndex(woob.IfIndex)
if err != nil {
log.Errorf("MainHandler4: Can not get Interface for index %d %v", woob.IfIndex, err)
return
}
err = sendEthernet(*intf, resp)
if err != nil {
log.Errorf("MainHandler4: Cannot send Ethernet packet: %v", err)
}
} else {
if _, err := l.WriteTo(resp.ToBytes(), woob, peer); err != nil {
log.Errorf("MainHandler4: conn.Write to %v failed: %v", peer, err)
}
}
} else {
log.Print("MainHandler4: dropping request because response is nil")
}
}
// XXX: performance-wise, Pool may or may not be good (see https://github.com/golang/go/issues/23199)
// Interface is good for what we want. Maybe "just" trust the GC and we'll be fine ?
var bufpool = sync.Pool{New: func() interface{} { r := make([]byte, MaxDatagram); return &r }}
// MaxDatagram is the maximum length of message that can be received.
const MaxDatagram = 1 << 16
// XXX: investigate using RecvMsgs to batch messages and reduce syscalls
// Serve6 handles datagrams received on conn and passes them to the pluginchain
func (l *listener6) Serve() error {
log.Printf("Listen %s", l.LocalAddr())
for {
b := *bufpool.Get().(*[]byte)
b = b[:MaxDatagram] //Reslice to max capacity in case the buffer in pool was resliced smaller
n, oob, peer, err := l.ReadFrom(b)
if errors.Is(err, net.ErrClosed) {
// Server is quitting
return nil
} else if err != nil {
log.Printf("Error reading from connection: %v", err)
return err
}
go l.HandleMsg6(b[:n], oob, peer.(*net.UDPAddr))
}
}
// Serve6 handles datagrams received on conn and passes them to the pluginchain
func (l *listener4) Serve() error {
log.Printf("Listen %s", l.LocalAddr())
for {
b := *bufpool.Get().(*[]byte)
b = b[:MaxDatagram] //Reslice to max capacity in case the buffer in pool was resliced smaller
n, oob, peer, err := l.ReadFrom(b)
if errors.Is(err, net.ErrClosed) {
// Server is quitting
return nil
} else if err != nil {
log.Printf("Error reading from connection: %v", err)
return err
}
go l.HandleMsg4(b[:n], oob, peer.(*net.UDPAddr))
}
}

96
server/sendEthernet.go Normal file
View File

@@ -0,0 +1,96 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// +build linux
package server
import (
"fmt"
"net"
"syscall"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
)
//this function sends an unicast to the hardware address defined in resp.ClientHWAddr,
//the layer3 destination address is still the broadcast address;
//iface: the interface where the DHCP message should be sent;
//resp: DHCPv4 struct, which should be sent;
func sendEthernet(iface net.Interface, resp *dhcpv4.DHCPv4) error {
eth := layers.Ethernet{
EthernetType: layers.EthernetTypeIPv4,
SrcMAC: iface.HardwareAddr,
DstMAC: resp.ClientHWAddr,
}
ip := layers.IPv4{
Version: 4,
TTL: 64,
SrcIP: resp.ServerIPAddr,
DstIP: resp.YourIPAddr,
Protocol: layers.IPProtocolUDP,
Flags: layers.IPv4DontFragment,
}
udp := layers.UDP{
SrcPort: dhcpv4.ServerPort,
DstPort: dhcpv4.ClientPort,
}
err := udp.SetNetworkLayerForChecksum(&ip)
if err != nil {
return fmt.Errorf("Send Ethernet: Couldn't set network layer: %v", err)
}
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
// Decode a packet
packet := gopacket.NewPacket(resp.ToBytes(), layers.LayerTypeDHCPv4, gopacket.NoCopy)
dhcpLayer := packet.Layer(layers.LayerTypeDHCPv4)
dhcp, ok := dhcpLayer.(gopacket.SerializableLayer)
if !ok {
return fmt.Errorf("Layer %s is not serializable", dhcpLayer.LayerType().String())
}
err = gopacket.SerializeLayers(buf, opts, &eth, &ip, &udp, dhcp)
if err != nil {
return fmt.Errorf("Cannot serialize layer: %v", err)
}
data := buf.Bytes()
fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, 0)
if err != nil {
return fmt.Errorf("Send Ethernet: Cannot open socket: %v", err)
}
defer func() {
err = syscall.Close(fd)
if err != nil {
log.Errorf("Send Ethernet: Cannot close socket: %v", err)
}
}()
err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
if err != nil {
log.Errorf("Send Ethernet: Cannot set option for socket: %v", err)
}
var hwAddr [8]byte
copy(hwAddr[0:6], resp.ClientHWAddr[0:6])
ethAddr := syscall.SockaddrLinklayer{
Protocol: 0,
Ifindex: iface.Index,
Halen: 6,
Addr: hwAddr, //not used
}
err = syscall.Sendto(fd, data, 0, &ethAddr)
if err != nil {
return fmt.Errorf("Cannot send frame via socket: %v", err)
}
return nil
}

185
server/serve.go Normal file
View File

@@ -0,0 +1,185 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package server
import (
"errors"
"fmt"
"io"
"net"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"github.com/coredhcp/coredhcp/config"
"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/insomniacslk/dhcp/dhcpv6/server6"
)
var log = logger.GetLogger("server")
type listener6 struct {
*ipv6.PacketConn
net.Interface
handlers []handler.Handler6
}
type listener4 struct {
*ipv4.PacketConn
net.Interface
handlers []handler.Handler4
}
type listener interface {
io.Closer
}
// Servers contains state for a running server (with possibly multiple interfaces/listeners)
type Servers struct {
listeners []listener
errors chan error
}
func listen4(a *net.UDPAddr) (*listener4, error) {
var err error
l4 := listener4{}
udpConn, err := server4.NewIPv4UDPConn(a.Zone, a)
if err != nil {
return nil, err
}
l4.PacketConn = ipv4.NewPacketConn(udpConn)
var ifi *net.Interface
if a.Zone != "" {
ifi, err = net.InterfaceByName(a.Zone)
if err != nil {
return nil, fmt.Errorf("DHCPv4: Listen could not find interface %s: %v", a.Zone, err)
}
l4.Interface = *ifi
} else {
// When not bound to an interface, we need the information in each
// packet to know which interface it came on
err = l4.SetControlMessage(ipv4.FlagInterface, true)
if err != nil {
return nil, err
}
}
if a.IP.IsMulticast() {
err = l4.JoinGroup(ifi, a)
if err != nil {
return nil, err
}
}
return &l4, nil
}
func listen6(a *net.UDPAddr) (*listener6, error) {
l6 := listener6{}
udpconn, err := server6.NewIPv6UDPConn(a.Zone, a)
if err != nil {
return nil, err
}
l6.PacketConn = ipv6.NewPacketConn(udpconn)
var ifi *net.Interface
if a.Zone != "" {
ifi, err = net.InterfaceByName(a.Zone)
if err != nil {
return nil, fmt.Errorf("DHCPv4: Listen could not find interface %s: %v", a.Zone, err)
}
l6.Interface = *ifi
} else {
// When not bound to an interface, we need the information in each
// packet to know which interface it came on
err = l6.SetControlMessage(ipv6.FlagInterface, true)
if err != nil {
return nil, err
}
}
if a.IP.IsMulticast() {
err = l6.JoinGroup(ifi, a)
if err != nil {
return nil, err
}
}
return &l6, nil
}
// Start will start the server asynchronously. See `Wait` to wait until
// the execution ends.
func Start(config *config.Config) (*Servers, error) {
handlers4, handlers6, err := plugins.LoadPlugins(config)
if err != nil {
return nil, err
}
srv := Servers{
errors: make(chan error),
}
// listen
if config.Server6 != nil {
log.Println("Starting DHCPv6 server")
for _, addr := range config.Server6.Addresses {
var l6 *listener6
l6, err = listen6(&addr)
if err != nil {
goto cleanup
}
l6.handlers = handlers6
srv.listeners = append(srv.listeners, l6)
go func() {
srv.errors <- l6.Serve()
}()
}
}
if config.Server4 != nil {
log.Println("Starting DHCPv4 server")
for _, addr := range config.Server4.Addresses {
var l4 *listener4
l4, err = listen4(&addr)
if err != nil {
goto cleanup
}
l4.handlers = handlers4
srv.listeners = append(srv.listeners, l4)
go func() {
srv.errors <- l4.Serve()
}()
}
}
return &srv, nil
cleanup:
srv.Close()
return nil, err
}
// Wait waits until the end of the execution of the server.
func (s *Servers) Wait() error {
log.Debug("Waiting")
errs := make([]error, 1, len(s.listeners))
errs[0] = <-s.errors
s.Close()
// Wait for the other listeners to close
for i := 1; i < len(s.listeners); i++ {
errs = append(errs, <-s.errors)
}
return errors.Join(errs...)
}
// Close closes all listening connections
func (s *Servers) Close() {
for _, srv := range s.listeners {
if srv != nil {
srv.Close()
}
}
}