add all files from Hong
This commit is contained in:
341
config/config.go
Normal file
341
config/config.go
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("config")
|
||||
|
||||
type protocolVersion int
|
||||
|
||||
const (
|
||||
protocolV6 protocolVersion = 6
|
||||
protocolV4 protocolVersion = 4
|
||||
)
|
||||
|
||||
// Config holds the DHCPv6/v4 server configuration
|
||||
type Config struct {
|
||||
v *viper.Viper
|
||||
Server6 *ServerConfig
|
||||
Server4 *ServerConfig
|
||||
}
|
||||
|
||||
// New returns a new initialized instance of a Config object
|
||||
func New() *Config {
|
||||
return &Config{v: viper.New()}
|
||||
}
|
||||
|
||||
// ServerConfig holds a server configuration that is specific to either the
|
||||
// DHCPv6 server or the DHCPv4 server.
|
||||
type ServerConfig struct {
|
||||
Addresses []net.UDPAddr
|
||||
Plugins []PluginConfig
|
||||
}
|
||||
|
||||
// PluginConfig holds the configuration of a plugin
|
||||
type PluginConfig struct {
|
||||
Name string
|
||||
Args []string
|
||||
}
|
||||
|
||||
// Load reads a configuration file and returns a Config object, or an error if
|
||||
// any.
|
||||
func Load(pathOverride string) (*Config, error) {
|
||||
log.Print("Loading configuration")
|
||||
c := New()
|
||||
c.v.SetConfigType("yml")
|
||||
if pathOverride != "" {
|
||||
c.v.SetConfigFile(pathOverride)
|
||||
} else {
|
||||
c.v.SetConfigName("config")
|
||||
c.v.AddConfigPath(".")
|
||||
c.v.AddConfigPath("$XDG_CONFIG_HOME/coredhcp/")
|
||||
c.v.AddConfigPath("$HOME/.coredhcp/")
|
||||
c.v.AddConfigPath("/etc/coredhcp/")
|
||||
}
|
||||
|
||||
if err := c.v.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.parseConfig(protocolV6); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.parseConfig(protocolV4); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.Server6 == nil && c.Server4 == nil {
|
||||
return nil, ConfigErrorFromString("need at least one valid config for DHCPv6 or DHCPv4")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func protoVersionCheck(v protocolVersion) error {
|
||||
if v != protocolV6 && v != protocolV4 {
|
||||
return fmt.Errorf("invalid protocol version: %d", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePlugins(pluginList []interface{}) ([]PluginConfig, error) {
|
||||
plugins := make([]PluginConfig, 0, len(pluginList))
|
||||
for idx, val := range pluginList {
|
||||
conf := cast.ToStringMap(val)
|
||||
if conf == nil {
|
||||
return nil, ConfigErrorFromString("dhcpv6: plugin #%d is not a string map", idx)
|
||||
}
|
||||
// make sure that only one item is specified, since it's a
|
||||
// map name -> args
|
||||
if len(conf) != 1 {
|
||||
return nil, ConfigErrorFromString("dhcpv6: exactly one plugin per item can be specified")
|
||||
}
|
||||
var (
|
||||
name string
|
||||
args []string
|
||||
)
|
||||
// only one item, as enforced above, so read just that
|
||||
for k, v := range conf {
|
||||
name = k
|
||||
args = strings.Fields(cast.ToString(v))
|
||||
break
|
||||
}
|
||||
plugins = append(plugins, PluginConfig{Name: name, Args: args})
|
||||
}
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// BUG(Natolumin): listen specifications of the form `[ip6]%iface:port` or
|
||||
// `[ip6]%iface` are not supported, even though they are the default format of
|
||||
// the `ss` utility in linux. Use `[ip6%iface]:port` instead
|
||||
|
||||
// splitHostPort splits an address of the form ip%zone:port into ip,zone and port.
|
||||
// It still returns if any of these are unset (unlike net.SplitHostPort which
|
||||
// returns an error if there is no port)
|
||||
func splitHostPort(hostport string) (ip string, zone string, port string, err error) {
|
||||
ip, port, err = net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
// Either there is no port, or a more serious error.
|
||||
// Supply a synthetic port to differentiate cases
|
||||
var altErr error
|
||||
if ip, _, altErr = net.SplitHostPort(hostport + ":0"); altErr != nil {
|
||||
// Invalid even with a fake port. Return the original error
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
if i := strings.LastIndexByte(ip, '%'); i >= 0 {
|
||||
ip, zone = ip[:i], ip[i+1:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) getListenAddress(addr string, ver protocolVersion) (*net.UDPAddr, error) {
|
||||
if err := protoVersionCheck(ver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipStr, ifname, portStr, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, ConfigErrorFromString("dhcpv%d: %v", ver, err)
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ipStr == "" {
|
||||
switch ver {
|
||||
case protocolV4:
|
||||
ip = net.IPv4zero
|
||||
case protocolV6:
|
||||
ip = net.IPv6unspecified
|
||||
default:
|
||||
panic("BUG: Unknown protocol version")
|
||||
}
|
||||
}
|
||||
if ip == nil {
|
||||
return nil, ConfigErrorFromString("dhcpv%d: invalid IP address in `listen` directive: %s", ver, ipStr)
|
||||
}
|
||||
if ip4 := ip.To4(); (ver == protocolV6 && ip4 != nil) || (ver == protocolV4 && ip4 == nil) {
|
||||
return nil, ConfigErrorFromString("dhcpv%d: not a valid IPv%d address in `listen` directive: '%s'", ver, ver, ipStr)
|
||||
}
|
||||
|
||||
var port int
|
||||
if portStr == "" {
|
||||
switch ver {
|
||||
case protocolV4:
|
||||
port = dhcpv4.ServerPort
|
||||
case protocolV6:
|
||||
port = dhcpv6.DefaultServerPort
|
||||
default:
|
||||
panic("BUG: Unknown protocol version")
|
||||
}
|
||||
} else {
|
||||
port, err = strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, ConfigErrorFromString("dhcpv%d: invalid `listen` port '%s'", ver, portStr)
|
||||
}
|
||||
}
|
||||
|
||||
listener := net.UDPAddr{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Zone: ifname,
|
||||
}
|
||||
return &listener, nil
|
||||
}
|
||||
|
||||
func (c *Config) getPlugins(ver protocolVersion) ([]PluginConfig, error) {
|
||||
if err := protoVersionCheck(ver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginList := cast.ToSlice(c.v.Get(fmt.Sprintf("server%d.plugins", ver)))
|
||||
if pluginList == nil {
|
||||
return nil, ConfigErrorFromString("dhcpv%d: invalid plugins section, not a list or no plugin specified", ver)
|
||||
}
|
||||
return parsePlugins(pluginList)
|
||||
}
|
||||
|
||||
func (c *Config) parseConfig(ver protocolVersion) error {
|
||||
if err := protoVersionCheck(ver); err != nil {
|
||||
return err
|
||||
}
|
||||
if exists := c.v.Get(fmt.Sprintf("server%d", ver)); exists == nil {
|
||||
// it is valid to have no server configuration defined
|
||||
return nil
|
||||
}
|
||||
// read plugin configuration
|
||||
plugins, err := c.getPlugins(ver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range plugins {
|
||||
log.Printf("DHCPv%d: found plugin `%s` with %d args: %v", ver, p.Name, len(p.Args), p.Args)
|
||||
}
|
||||
|
||||
listeners, err := c.parseListen(ver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sc := ServerConfig{
|
||||
Addresses: listeners,
|
||||
Plugins: plugins,
|
||||
}
|
||||
if ver == protocolV6 {
|
||||
c.Server6 = &sc
|
||||
} else if ver == protocolV4 {
|
||||
c.Server4 = &sc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BUG(Natolumin): When listening on link-local multicast addresses without
|
||||
// binding to a specific interface, new interfaces coming up after the server
|
||||
// starts will not be taken into account.
|
||||
|
||||
func expandLLMulticast(addr *net.UDPAddr) ([]net.UDPAddr, error) {
|
||||
if !addr.IP.IsLinkLocalMulticast() && !addr.IP.IsInterfaceLocalMulticast() {
|
||||
return nil, errors.New("Address is not multicast")
|
||||
}
|
||||
if addr.Zone != "" {
|
||||
return nil, errors.New("Address is already zoned")
|
||||
}
|
||||
var needFlags = net.FlagMulticast
|
||||
if addr.IP.To4() != nil {
|
||||
// We need to be able to send broadcast responses in ipv4
|
||||
needFlags |= net.FlagBroadcast
|
||||
}
|
||||
|
||||
ifs, err := net.Interfaces()
|
||||
ret := make([]net.UDPAddr, 0, len(ifs))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not list network interfaces: %v", err)
|
||||
}
|
||||
for _, iface := range ifs {
|
||||
if (iface.Flags & needFlags) != needFlags {
|
||||
continue
|
||||
}
|
||||
caddr := *addr
|
||||
caddr.Zone = iface.Name
|
||||
ret = append(ret, caddr)
|
||||
}
|
||||
if len(ret) == 0 {
|
||||
return nil, errors.New("No suitable interface found for multicast listener")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func defaultListen(ver protocolVersion) ([]net.UDPAddr, error) {
|
||||
switch ver {
|
||||
case protocolV4:
|
||||
return []net.UDPAddr{{Port: dhcpv4.ServerPort}}, nil
|
||||
case protocolV6:
|
||||
l, err := expandLLMulticast(&net.UDPAddr{IP: dhcpv6.AllDHCPRelayAgentsAndServers, Port: dhcpv6.DefaultServerPort})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l,
|
||||
net.UDPAddr{IP: dhcpv6.AllDHCPServers, Port: dhcpv6.DefaultServerPort},
|
||||
// XXX: Do we want to listen on [::] as default ?
|
||||
)
|
||||
return l, nil
|
||||
}
|
||||
return nil, errors.New("defaultListen: Incorrect protocol version")
|
||||
}
|
||||
|
||||
func (c *Config) parseListen(ver protocolVersion) ([]net.UDPAddr, error) {
|
||||
if err := protoVersionCheck(ver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listen := c.v.Get(fmt.Sprintf("server%d.listen", ver))
|
||||
|
||||
// Provide an emulation of the old keyword "interface" to avoid breaking config files
|
||||
if iface := c.v.Get(fmt.Sprintf("server%d.interface", ver)); iface != nil && listen != nil {
|
||||
return nil, ConfigErrorFromString("interface is a deprecated alias for listen, " +
|
||||
"both cannot be used at the same time. Choose one and remove the other.")
|
||||
} else if iface != nil {
|
||||
listen = "%" + cast.ToString(iface)
|
||||
}
|
||||
|
||||
if listen == nil {
|
||||
return defaultListen(ver)
|
||||
}
|
||||
|
||||
addrs, err := cast.ToStringSliceE(listen)
|
||||
if err != nil {
|
||||
addrs = []string{cast.ToString(listen)}
|
||||
}
|
||||
|
||||
listeners := []net.UDPAddr{}
|
||||
for _, a := range addrs {
|
||||
l, err := c.getListenAddress(a, ver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if l.Zone == "" && (l.IP.IsLinkLocalMulticast() || l.IP.IsInterfaceLocalMulticast()) {
|
||||
// link-local multicast specified without interface gets expanded to listen on all interfaces
|
||||
expanded, err := expandLLMulticast(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listeners = append(listeners, expanded...)
|
||||
continue
|
||||
}
|
||||
|
||||
listeners = append(listeners, *l)
|
||||
}
|
||||
return listeners, nil
|
||||
}
|
||||
52
config/config_test.go
Normal file
52
config/config_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSplitHostPort(t *testing.T) {
|
||||
testcases := []struct {
|
||||
hostport string
|
||||
ip string
|
||||
zone string
|
||||
port string
|
||||
err bool // Should return an error (ie true for err != nil)
|
||||
}{
|
||||
{"0.0.0.0:67", "0.0.0.0", "", "67", false},
|
||||
{"192.0.2.0", "192.0.2.0", "", "", false},
|
||||
{"192.0.2.9%eth0", "192.0.2.9", "eth0", "", false},
|
||||
{"0.0.0.0%eth0:67", "0.0.0.0", "eth0", "67", false},
|
||||
{"0.0.0.0:20%eth0:67", "0.0.0.0", "eth0", "67", true},
|
||||
{"2001:db8::1:547", "", "", "547", true}, // [] mandatory for v6
|
||||
{"[::]:547", "::", "", "547", false},
|
||||
{"[fe80::1%eth0]", "fe80::1", "eth0", "", false},
|
||||
{"[fe80::1]:eth1", "fe80::1", "", "eth1", false}, // no validation of ports in this function
|
||||
{"fe80::1%eth0:547", "fe80::1", "eth0", "547", true}, // [] mandatory for v6 even with %zone
|
||||
{"fe80::1%eth0", "fe80::1", "eth0", "547", true}, // [] mandatory for v6 even without port
|
||||
{"[2001:db8::2]47", "fe80::1", "eth0", "547", true}, // garbage after []
|
||||
{"[ff02::1:2]%srv_u:547", "ff02::1:2", "srv_u", "547", true}, // FIXME: Linux `ss` format, we should accept but net.SplitHostPort doesn't
|
||||
{":http", "", "", "http", false},
|
||||
{"%eth0:80", "", "eth0", "80", false}, // janky, but looks valid enough for "[::%eth0]:80" imo
|
||||
{"%eth0", "", "eth0", "", false}, // janky
|
||||
{"fe80::1]:80", "fe80::1", "", "80", true}, // unbalanced ]
|
||||
{"fe80::1%eth0]", "fe80::1", "eth0", "", true}, // unbalanced ], no port
|
||||
{"", "", "", "", false}, // trivial case, still valid
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
ip, zone, port, err := splitHostPort(tc.hostport)
|
||||
if tc.err != (err != nil) {
|
||||
errState := "not "
|
||||
if tc.err {
|
||||
errState = ""
|
||||
}
|
||||
t.Errorf("Mismatched error state: %s should %serror (got err: %v)\n", tc.hostport, errState, err)
|
||||
continue
|
||||
}
|
||||
if err == nil && (ip != tc.ip || zone != tc.zone || port != tc.port) {
|
||||
t.Errorf("%s => \"%s\", \"%s\", \"%s\" expected \"%s\",\"%s\",\"%s\"\n", tc.hostport, ip, zone, port, tc.ip, tc.zone, tc.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
config/errors.go
Normal file
32
config/errors.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConfigError is an error type returned upon configuration errors.
|
||||
type ConfigError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// ConfigErrorFromString returns a ConfigError from the given error string.
|
||||
func ConfigErrorFromString(format string, args ...interface{}) *ConfigError {
|
||||
return &ConfigError{
|
||||
err: fmt.Errorf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigErrorFromError returns a ConfigError from the given error object.
|
||||
func ConfigErrorFromError(err error) *ConfigError {
|
||||
return &ConfigError{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (ce ConfigError) Error() string {
|
||||
return fmt.Sprintf("error parsing config: %v", ce.err)
|
||||
}
|
||||
2
config/static_ip.csv
Executable file
2
config/static_ip.csv
Executable file
@@ -0,0 +1,2 @@
|
||||
42:9c:aa:1b:87:d5,172.16.11.20
|
||||
d2:ab:13:8a:e8:15,172.16.11.21
|
||||
|
Reference in New Issue
Block a user