diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..13600a2 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +coredhcp.io \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1da455a --- /dev/null +++ b/LICENSE @@ -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. diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..259a24e --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-tactile \ No newline at end of file diff --git a/cmds/client/README.md b/cmds/client/README.md new file mode 100644 index 0000000..7696738 --- /dev/null +++ b/cmds/client/README.md @@ -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*** diff --git a/cmds/client/main.go b/cmds/client/main.go new file mode 100644 index 0000000..ac65a55 --- /dev/null +++ b/cmds/client/main.go @@ -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) + } +} diff --git a/cmds/coredhcp-generator/README.md b/cmds/coredhcp-generator/README.md new file mode 100644 index 0000000..db41bca --- /dev/null +++ b/cmds/coredhcp-generator/README.md @@ -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. diff --git a/cmds/coredhcp-generator/core-plugins.txt b/cmds/coredhcp-generator/core-plugins.txt new file mode 100644 index 0000000..f96e6bb --- /dev/null +++ b/cmds/coredhcp-generator/core-plugins.txt @@ -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 diff --git a/cmds/coredhcp-generator/coredhcp.go.template b/cmds/coredhcp-generator/coredhcp.go.template new file mode 100644 index 0000000..ddf58f0 --- /dev/null +++ b/cmds/coredhcp-generator/coredhcp.go.template @@ -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_ 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) + } +} diff --git a/cmds/coredhcp-generator/main.go b/cmds/coredhcp-generator/main.go new file mode 100644 index 0000000..471cfd7 --- /dev/null +++ b/cmds/coredhcp-generator/main.go @@ -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)) +} diff --git a/cmds/coredhcp/config.yml b/cmds/coredhcp/config.yml new file mode 100644 index 0000000..727ce13 --- /dev/null +++ b/cmds/coredhcp/config.yml @@ -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: + # 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: [autorefresh] + # The file format is one lease per line, " " + # 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 IPs> + #- dns: 2001:4860:4860::8888 2001:4860:4860::8844 + + # nbp can add information about the location of a network boot program + # - nbp: + #- nbp: "http://[2001:db8:a::1]/nbp" + + # prefix provides prefix delegation. + # - prefix: + # 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: + # 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: + # 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 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: + - router: 192.168.1.1 + + # netmask advertises the network mask for the IPs assigned through this + # server + # - netmask: + - netmask: 255.255.255.0 + + # range allocates leases within a range of IPs + # - range: + # * 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: , [, ...] + # 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 diff --git a/cmds/coredhcp/config.yml.example b/cmds/coredhcp/config.yml.example new file mode 100644 index 0000000..c9cb91e --- /dev/null +++ b/cmds/coredhcp/config.yml.example @@ -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: + # 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: [autorefresh] + # The file format is one lease per line, " " + # 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 IPs> + - dns: 2001:4860:4860::8888 2001:4860:4860::8844 + + # nbp can add information about the location of a network boot program + # - nbp: + - nbp: "http://[2001:db8:a::1]/nbp" + + # prefix provides prefix delegation. + # - prefix: + # 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: + # 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: + # 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 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: + - router: 192.168.1.1 + + # netmask advertises the network mask for the IPs assigned through this + # server + # - netmask: + - netmask: 255.255.255.0 + + # range allocates leases within a range of IPs + # - range: + # * 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: , [, ...] + # 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 diff --git a/cmds/coredhcp/coredhcp b/cmds/coredhcp/coredhcp new file mode 100755 index 0000000..1481e57 Binary files /dev/null and b/cmds/coredhcp/coredhcp differ diff --git a/cmds/coredhcp/file_leases.txt.example b/cmds/coredhcp/file_leases.txt.example new file mode 100644 index 0000000..e113c41 --- /dev/null +++ b/cmds/coredhcp/file_leases.txt.example @@ -0,0 +1 @@ +00:11:22:33:44:55 2001:2::1 diff --git a/cmds/coredhcp/leases.txt b/cmds/coredhcp/leases.txt new file mode 100644 index 0000000..e016cd5 Binary files /dev/null and b/cmds/coredhcp/leases.txt differ diff --git a/cmds/coredhcp/main.go b/cmds/coredhcp/main.go new file mode 100644 index 0000000..e044270 --- /dev/null +++ b/cmds/coredhcp/main.go @@ -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) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a3a98ca --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..9833a84 --- /dev/null +++ b/config/config_test.go @@ -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) + } + } +} diff --git a/config/errors.go b/config/errors.go new file mode 100644 index 0000000..91072ba --- /dev/null +++ b/config/errors.go @@ -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) +} diff --git a/config/static_ip.csv b/config/static_ip.csv new file mode 100755 index 0000000..a1c181a --- /dev/null +++ b/config/static_ip.csv @@ -0,0 +1,2 @@ +42:9c:aa:1b:87:d5,172.16.11.20 +d2:ab:13:8a:e8:15,172.16.11.21 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..479fd3b --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0611e33 --- /dev/null +++ b/go.sum @@ -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= diff --git a/grpc_server/dhcpServer/dhcp_server.pb.go b/grpc_server/dhcpServer/dhcp_server.pb.go new file mode 100644 index 0000000..015468e --- /dev/null +++ b/grpc_server/dhcpServer/dhcp_server.pb.go @@ -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 +} diff --git a/grpc_server/dhcpServer/dhcp_server_grpc.pb.go b/grpc_server/dhcpServer/dhcp_server_grpc.pb.go new file mode 100644 index 0000000..6029f53 --- /dev/null +++ b/grpc_server/dhcpServer/dhcp_server_grpc.pb.go @@ -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", +} diff --git a/grpc_server/grpc_server.go b/grpc_server/grpc_server.go new file mode 100644 index 0000000..167f4d2 --- /dev/null +++ b/grpc_server/grpc_server.go @@ -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) + } +} \ No newline at end of file diff --git a/handler/handler.go b/handler/handler.go new file mode 100644 index 0000000..52e67a9 --- /dev/null +++ b/handler/handler.go @@ -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) diff --git a/integ/server6/leases-dhcpv6-test.txt b/integ/server6/leases-dhcpv6-test.txt new file mode 100644 index 0000000..fc07cc6 --- /dev/null +++ b/integ/server6/leases-dhcpv6-test.txt @@ -0,0 +1 @@ +de:ad:be:ef:00:00 2001:db8::10:1 diff --git a/integ/server6/server6.go b/integ/server6/server6.go new file mode 100644 index 0000000..630295e --- /dev/null +++ b/integ/server6/server6.go @@ -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) + } +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..4c07fea --- /dev/null +++ b/logger/logger.go @@ -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 = "" + } + 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) +} diff --git a/plugins/allocators/allocator.go b/plugins/allocators/allocator.go new file mode 100644 index 0000000..0fb12f4 --- /dev/null +++ b/plugins/allocators/allocator.go @@ -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") diff --git a/plugins/allocators/bitmap/bitmap.go b/plugins/allocators/bitmap/bitmap.go new file mode 100644 index 0000000..37b0fbe --- /dev/null +++ b/plugins/allocators/bitmap/bitmap.go @@ -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< 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 +} diff --git a/plugins/allocators/bitmap/bitmap_ipv4_test.go b/plugins/allocators/bitmap/bitmap_ipv4_test.go new file mode 100644 index 0000000..50c71fd --- /dev/null +++ b/plugins/allocators/bitmap/bitmap_ipv4_test.go @@ -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) + } +} diff --git a/plugins/allocators/bitmap/bitmap_test.go b/plugins/allocators/bitmap/bitmap_test.go new file mode 100644 index 0000000..de67c6f --- /dev/null +++ b/plugins/allocators/bitmap/bitmap_test.go @@ -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() + } + } + }) +} diff --git a/plugins/allocators/ipcalc.go b/plugins/allocators/ipcalc.go new file mode 100644 index 0000000..a667eee --- /dev/null +++ b/plugins/allocators/ipcalc.go @@ -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 +} diff --git a/plugins/allocators/ipcalc_test.go b/plugins/allocators/ipcalc_test.go new file mode 100644 index 0000000..83e8922 --- /dev/null +++ b/plugins/allocators/ipcalc_test.go @@ -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 + // 0 + // 0 + // 254 + // 16667973 + // 8534002176 + // 1092352278528 + // 71588398925611008 + // 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:: + // 2001:db8::1 + // 2001:eb7:: + // 2002:db8:: + // 2001:db8:0:7f:8000:: + // Operation overflows + // 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, + ) + } +} diff --git a/plugins/autoconfigure/plugin.go b/plugins/autoconfigure/plugin.go new file mode 100644 index 0000000..04cbe67 --- /dev/null +++ b/plugins/autoconfigure/plugin.go @@ -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 +} diff --git a/plugins/autoconfigure/plugin_test.go b/plugins/autoconfigure/plugin_test.go new file mode 100644 index 0000000..7826dcb --- /dev/null +++ b/plugins/autoconfigure/plugin_test.go @@ -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") + } +} diff --git a/plugins/dns/plugin.go b/plugins/dns/plugin.go new file mode 100644 index 0000000..b6b8cc8 --- /dev/null +++ b/plugins/dns/plugin.go @@ -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 +} diff --git a/plugins/dns/plugin_test.go b/plugins/dns/plugin_test.go new file mode 100644 index 0000000..ae97208 --- /dev/null +++ b/plugins/dns/plugin_test.go @@ -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)) + } +} diff --git a/plugins/example/plugin.go b/plugins/example/plugin.go new file mode 100644 index 0000000..6262c54 --- /dev/null +++ b/plugins/example/plugin.go @@ -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 +} diff --git a/plugins/file/plugin.go b/plugins/file/plugin.go new file mode 100644 index 0000000..52dbd9e --- /dev/null +++ b/plugins/file/plugin.go @@ -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 +} diff --git a/plugins/file/plugin_test.go b/plugins/file/plugin_test.go new file mode 100644 index 0000000..ddba983 --- /dev/null +++ b/plugins/file/plugin_test.go @@ -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)) + }) +} diff --git a/plugins/ipv6only/plugin.go b/plugins/ipv6only/plugin.go new file mode 100644 index 0000000..da97a3b --- /dev/null +++ b/plugins/ipv6only/plugin.go @@ -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 +} diff --git a/plugins/ipv6only/plugin_test.go b/plugins/ipv6only/plugin_test.go new file mode 100644 index 0000000..8fd8d50 --- /dev/null +++ b/plugins/ipv6only/plugin_test.go @@ -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") + } +} diff --git a/plugins/leasetime/plugin.go b/plugins/leasetime/plugin.go new file mode 100644 index 0000000..a4e3305 --- /dev/null +++ b/plugins/leasetime/plugin.go @@ -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 +} diff --git a/plugins/mtu/plugin.go b/plugins/mtu/plugin.go new file mode 100644 index 0000000..135bf01 --- /dev/null +++ b/plugins/mtu/plugin.go @@ -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 +} diff --git a/plugins/mtu/plugin_test.go b/plugins/mtu/plugin_test.go new file mode 100644 index 0000000..f826a71 --- /dev/null +++ b/plugins/mtu/plugin_test.go @@ -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) + } +} diff --git a/plugins/nbp/nbp.go b/plugins/nbp/nbp.go new file mode 100644 index 0000000..619c795 --- /dev/null +++ b/plugins/nbp/nbp.go @@ -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 +} diff --git a/plugins/netmask/plugin.go b/plugins/netmask/plugin.go new file mode 100644 index 0000000..d151547 --- /dev/null +++ b/plugins/netmask/plugin.go @@ -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 +} diff --git a/plugins/netmask/plugin_test.go b/plugins/netmask/plugin_test.go new file mode 100644 index 0000000..7ddd0d8 --- /dev/null +++ b/plugins/netmask/plugin_test.go @@ -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) +} diff --git a/plugins/plugin.go b/plugins/plugin.go new file mode 100644 index 0000000..aa94a79 --- /dev/null +++ b/plugins/plugin.go @@ -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 +} diff --git a/plugins/prefix/plugin.go b/plugins/prefix/plugin.go new file mode 100644 index 0000000..8190b76 --- /dev/null +++ b/plugins/prefix/plugin.go @@ -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 +} diff --git a/plugins/prefix/plugin_test.go b/plugins/prefix/plugin_test.go new file mode 100644 index 0000000..5f7b4c1 --- /dev/null +++ b/plugins/prefix/plugin_test.go @@ -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) + } +} diff --git a/plugins/range/plugin.go b/plugins/range/plugin.go new file mode 100644 index 0000000..ac0666f --- /dev/null +++ b/plugins/range/plugin.go @@ -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 +} diff --git a/plugins/range/plugin.go.new b/plugins/range/plugin.go.new new file mode 100644 index 0000000..004e22a --- /dev/null +++ b/plugins/range/plugin.go.new @@ -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 +} diff --git a/plugins/range/static_ip.go b/plugins/range/static_ip.go new file mode 100644 index 0000000..403e97e --- /dev/null +++ b/plugins/range/static_ip.go @@ -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 +} \ No newline at end of file diff --git a/plugins/range/storage.go b/plugins/range/storage.go new file mode 100644 index 0000000..ccc49c4 --- /dev/null +++ b/plugins/range/storage.go @@ -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 +} diff --git a/plugins/range/storage_test.go b/plugins/range/storage_test.go new file mode 100644 index 0000000..c9f1017 --- /dev/null +++ b/plugins/range/storage_test.go @@ -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") +} diff --git a/plugins/router/plugin.go b/plugins/router/plugin.go new file mode 100644 index 0000000..a2da616 --- /dev/null +++ b/plugins/router/plugin.go @@ -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 +} diff --git a/plugins/searchdomains/plugin.go b/plugins/searchdomains/plugin.go new file mode 100644 index 0000000..0ec8e61 --- /dev/null +++ b/plugins/searchdomains/plugin.go @@ -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 +} diff --git a/plugins/searchdomains/plugin_test.go b/plugins/searchdomains/plugin_test.go new file mode 100644 index 0000000..23cb29d --- /dev/null +++ b/plugins/searchdomains/plugin_test.go @@ -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) + +} diff --git a/plugins/serverid/plugin.go b/plugins/serverid/plugin.go new file mode 100644 index 0000000..7ac59dc --- /dev/null +++ b/plugins/serverid/plugin.go @@ -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 +} diff --git a/plugins/serverid/plugin_test.go b/plugins/serverid/plugin_test.go new file mode 100644 index 0000000..58fd5a1 --- /dev/null +++ b/plugins/serverid/plugin_test.go @@ -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") + } +} diff --git a/plugins/sleep/plugin.go b/plugins/sleep/plugin.go new file mode 100644 index 0000000..cef89e5 --- /dev/null +++ b/plugins/sleep/plugin.go @@ -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 + } +} diff --git a/plugins/staticroute/plugin.go b/plugins/staticroute/plugin.go new file mode 100644 index 0000000..4488e2e --- /dev/null +++ b/plugins/staticroute/plugin.go @@ -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 +} diff --git a/plugins/staticroute/plugin_test.go b/plugins/staticroute/plugin_test.go new file mode 100644 index 0000000..685d87f --- /dev/null +++ b/plugins/staticroute/plugin_test.go @@ -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()) + } + } +} diff --git a/server/handle.go b/server/handle.go new file mode 100644 index 0000000..b78dd67 --- /dev/null +++ b/server/handle.go @@ -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)) + } +} diff --git a/server/sendEthernet.go b/server/sendEthernet.go new file mode 100644 index 0000000..35caf86 --- /dev/null +++ b/server/sendEthernet.go @@ -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, ð, &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, ðAddr) + if err != nil { + return fmt.Errorf("Cannot send frame via socket: %v", err) + } + return nil +} diff --git a/server/serve.go b/server/serve.go new file mode 100644 index 0000000..b341e08 --- /dev/null +++ b/server/serve.go @@ -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() + } + } +}