From c9ef29057e9e378779d1a7938ad13b6eebda8f50 Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Wed, 13 Sep 2023 17:53:55 +0300 Subject: [PATCH] dhcpsvc: add constructor, validations, tests --- internal/dhcpsvc/config.go | 143 ++++++++------ internal/dhcpsvc/errors.go | 11 ++ internal/dhcpsvc/iprange.go | 124 ++++++++++++ internal/dhcpsvc/iprange_internal_test.go | 225 ++++++++++++++++++++++ internal/dhcpsvc/server.go | 157 ++++++++++++++- internal/dhcpsvc/server_test.go | 174 +++++++++++++++++ 6 files changed, 768 insertions(+), 66 deletions(-) create mode 100644 internal/dhcpsvc/errors.go create mode 100644 internal/dhcpsvc/iprange.go create mode 100644 internal/dhcpsvc/iprange_internal_test.go create mode 100644 internal/dhcpsvc/server_test.go diff --git a/internal/dhcpsvc/config.go b/internal/dhcpsvc/config.go index ed2072636cc..0dc3f968ecd 100644 --- a/internal/dhcpsvc/config.go +++ b/internal/dhcpsvc/config.go @@ -5,13 +5,21 @@ import ( "net/netip" "time" - "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/netutil" "github.com/google/gopacket/layers" "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) +// InterfaceConfig is the configuration of a single DHCP interface. +type InterfaceConfig struct { + // IPv4 is the configuration of DHCP protocol for IPv4. + IPv4 *IPv4Config + + // IPv6 is the configuration of DHCP protocol for IPv6. + IPv6 *IPv6Config +} + // Config is the configuration for the DHCP service. type Config struct { // Interfaces stores configurations of DHCP server specific for the network @@ -29,13 +37,46 @@ type Config struct { Enabled bool } -// InterfaceConfig is the configuration of a single DHCP interface. -type InterfaceConfig struct { - // IPv4 is the configuration of DHCP protocol for IPv4. - IPv4 *IPv4Config +// Validate returns an error in conf if any. +func (conf *Config) Validate() (err error) { + switch { + case conf == nil: + return errNilConfig + case !conf.Enabled: + return nil + case conf.ICMPTimeout < 0: + return fmt.Errorf("icmp timeout %s must be non-negative", conf.ICMPTimeout) + } - // IPv6 is the configuration of DHCP protocol for IPv6. - IPv6 *IPv6Config + err = netutil.ValidateDomainName(conf.LocalDomainName) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + if len(conf.Interfaces) == 0 { + return errNoInterfaces + } + + ifaces := maps.Keys(conf.Interfaces) + slices.Sort(ifaces) + + for _, iface := range ifaces { + ifaceConf := conf.Interfaces[iface] + if ifaceConf == nil { + return fmt.Errorf("interface %q: %w", iface, errNilConfig) + } + + if err = ifaceConf.IPv4.validate(); err != nil { + return fmt.Errorf("interface %q: ipv4: %w", iface, err) + } + + if err = ifaceConf.IPv6.validate(); err != nil { + return fmt.Errorf("interface %q: ipv6: %w", iface, err) + } + } + + return nil } // IPv4Config is the interface-specific configuration for DHCPv4. @@ -66,6 +107,28 @@ type IPv4Config struct { Enabled bool } +// validate returns an error in conf if any. +func (conf *IPv4Config) validate() (err error) { + switch { + case conf == nil: + return errNilConfig + case !conf.Enabled: + return nil + case !conf.GatewayIP.Is4(): + return fmt.Errorf("gateway ip %s should be a valid ipv4", conf.GatewayIP) + case !conf.SubnetMask.Is4(): + return fmt.Errorf("subnet mask %s should be a valid ipv4 cidr", conf.SubnetMask) + case !conf.RangeStart.Is4(): + return fmt.Errorf("range start %s should be a valid ipv4", conf.RangeStart) + case !conf.RangeEnd.Is4(): + return fmt.Errorf("range end %s should be a valid ipv4", conf.RangeEnd) + case conf.LeaseDuration <= 0: + return fmt.Errorf("lease duration %s must be positive", conf.LeaseDuration) + default: + return nil + } +} + // IPv6Config is the interface-specific configuration for DHCPv6. type IPv6Config struct { // RangeStart is the first address in the range to assign to DHCP clients. @@ -90,66 +153,18 @@ type IPv6Config struct { Enabled bool } -// TODO(e.burkov): !! doc -const ErrNilConfig errors.Error = "config is nil" - -func (conf *Config) Validate() (err error) { +// validate returns an error in conf if any. +func (conf *IPv6Config) validate() (err error) { switch { case conf == nil: - return ErrNilConfig + return errNilConfig case !conf.Enabled: return nil - case conf.ICMPTimeout < 0: - return fmt.Errorf("icmp timeout %s must be non-negative", conf.ICMPTimeout) - } - - err = netutil.ValidateDomainName(conf.LocalDomainName) - if err != nil { - // Don't wrap the error since it's informative enough as is. - return err - } - - ifaces := maps.Keys(conf.Interfaces) - slices.Sort(ifaces) - - return errors.Join( - errors.Annotate(conf.validateV4(ifaces), "validating v4: %w"), - errors.Annotate(conf.validateV6(ifaces), "validating v6: %w"), - ) -} - -func (conf *Config) validateV4(ifaces []string) (err error) { - for _, iface := range ifaces { - ifaceConf := conf.Interfaces[iface] - if ifaceConf == nil { - return ErrNilConfig - } - - v4Conf := ifaceConf.IPv4 - switch { - case !v4Conf.Enabled: - continue - case !v4Conf.GatewayIP.Is4(): - return fmt.Errorf("interface %q: gateway ip should be a valid ipv4", iface) - case !v4Conf.SubnetMask.Is4(): - return fmt.Errorf("interface %q: subnet mask should be a valid ipv4 cidr", iface) - case !v4Conf.RangeStart.Is4(): - return fmt.Errorf("interface %q: range start should be a valid ipv4", iface) - case !v4Conf.RangeEnd.Is4(): - return fmt.Errorf("interface %q: range end should be a valid ipv4", iface) - } - - c.ipRange, err = newIPRange(rangeStart.AsSlice(), rangeEnd.AsSlice()) - if err != nil { - // Don't wrap the error since it's informative enough as is and there is - // an annotation deferred already. - return err - } + case !conf.RangeStart.Is6(): + return fmt.Errorf("range start %s should be a valid ipv6", conf.RangeStart) + case conf.LeaseDuration <= 0: + return fmt.Errorf("lease duration %s must be positive", conf.LeaseDuration) + default: + return nil } - - return nil -} - -func (conf *Config) validateV6(ifaces []string) (err error) { - return nil } diff --git a/internal/dhcpsvc/errors.go b/internal/dhcpsvc/errors.go new file mode 100644 index 00000000000..a7cc89312fb --- /dev/null +++ b/internal/dhcpsvc/errors.go @@ -0,0 +1,11 @@ +package dhcpsvc + +import "github.com/AdguardTeam/golibs/errors" + +const ( + // errNilConfig is returned when a nil config met. + errNilConfig errors.Error = "config is nil" + + // errNoInterfaces is returned when no interfaces found in configuration. + errNoInterfaces errors.Error = "no interfaces specified" +) diff --git a/internal/dhcpsvc/iprange.go b/internal/dhcpsvc/iprange.go new file mode 100644 index 00000000000..7a1b62b6c9e --- /dev/null +++ b/internal/dhcpsvc/iprange.go @@ -0,0 +1,124 @@ +package dhcpsvc + +import ( + "fmt" + "math" + "math/big" + "net" + "net/netip" + + "github.com/AdguardTeam/golibs/errors" +) + +// ipRange is an inclusive range of IP addresses. A nil range is a range that +// doesn't contain any IP addresses. +// +// It is safe for concurrent use. +// +// TODO(a.garipov): Perhaps create an optimized version with uint32 for IPv4 +// ranges? Or use one of uint128 packages? +type ipRange struct { + start *big.Int + end *big.Int +} + +// maxRangeLen is the maximum IP range length. The bitsets used in servers only +// accept uints, which can have the size of 32 bit. +const maxRangeLen = math.MaxUint32 + +// newIPRange creates a new IP address range. start must be less than end. The +// resulting range must not be greater than maxRangeLen. +func newIPRange(start, end netip.Addr) (r *ipRange, err error) { + defer func() { err = errors.Annotate(err, "invalid ip range: %w") }() + + if !start.Less(end) { + return nil, fmt.Errorf("start is greater than or equal to end") + } + + // Make sure that both are 16 bytes long to simplify handling in + // methods. + startData, endData := start.As16(), end.As16() + + startInt := (&big.Int{}).SetBytes(startData[:]) + endInt := (&big.Int{}).SetBytes(endData[:]) + diff := (&big.Int{}).Sub(endInt, startInt) + + if !diff.IsUint64() || diff.Uint64() > maxRangeLen { + return nil, fmt.Errorf("range is too large") + } + + return &ipRange{ + start: startInt, + end: endInt, + }, nil +} + +// contains returns true if r contains ip. +func (r *ipRange) contains(ip netip.Addr) (ok bool) { + if r == nil { + return false + } + + ipData := ip.As16() + + return r.containsInt((&big.Int{}).SetBytes(ipData[:])) +} + +// containsInt returns true if r contains ipInt. For internal use only. +func (r *ipRange) containsInt(ipInt *big.Int) (ok bool) { + return ipInt.Cmp(r.start) >= 0 && ipInt.Cmp(r.end) <= 0 +} + +// ipPredicate is a function that is called on every IP address in +// (*ipRange).find. ip is given in the 16-byte form. +type ipPredicate func(ip netip.Addr) (ok bool) + +// find finds the first IP address in r for which p returns true. ip is in the +// 16-byte form. It returns an empty [netip.Addr] if no addresses satisfy p. +func (r *ipRange) find(p ipPredicate) (ip netip.Addr) { + if r == nil { + return netip.Addr{} + } + + _1 := big.NewInt(1) + var ipData [16]byte + for i := (&big.Int{}).Set(r.start); i.Cmp(r.end) <= 0; i.Add(i, _1) { + i.FillBytes(ipData[:]) + ip = netip.AddrFrom16(ipData) + if p(ip) { + return ip + } + } + + return netip.Addr{} +} + +// offset returns the offset of ip from the beginning of r. It returns 0 and +// false if ip is not in r. +func (r *ipRange) offset(ip netip.Addr) (offset uint64, ok bool) { + if r == nil { + return 0, false + } + + ipData := ip.As16() + ipInt := (&big.Int{}).SetBytes(ipData[:]) + if !r.containsInt(ipInt) { + return 0, false + } + + offsetInt := (&big.Int{}).Sub(ipInt, r.start) + + // Assume that the range was checked against maxRangeLen during + // construction. + return offsetInt.Uint64(), true +} + +// String implements the fmt.Stringer interface for *ipRange. +func (r *ipRange) String() (s string) { + start, end := [16]byte{}, [16]byte{} + + r.start.FillBytes(start[:]) + r.end.FillBytes(end[:]) + + return fmt.Sprintf("%s-%s", net.IP(start[:]), net.IP(end[:])) +} diff --git a/internal/dhcpsvc/iprange_internal_test.go b/internal/dhcpsvc/iprange_internal_test.go new file mode 100644 index 00000000000..fcd43261613 --- /dev/null +++ b/internal/dhcpsvc/iprange_internal_test.go @@ -0,0 +1,225 @@ +package dhcpsvc + +import ( + "net/netip" + "testing" + + "github.com/AdguardTeam/golibs/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewIPRange(t *testing.T) { + start4 := netip.MustParseAddr("0.0.0.1") + end4 := netip.MustParseAddr("0.0.0.3") + start6 := netip.AddrFrom16([16]byte{ + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + }) + end6 := netip.AddrFrom16([16]byte{ + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, + }) + end6Large := netip.AddrFrom16([16]byte{ + 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, + }) + + testCases := []struct { + start netip.Addr + end netip.Addr + name string + wantErrMsg string + }{{ + start: start4, + end: end4, + name: "success_ipv4", + wantErrMsg: "", + }, { + start: start6, + end: end6, + name: "success_ipv6", + wantErrMsg: "", + }, { + start: end4, + end: start4, + name: "start_gt_end", + wantErrMsg: "invalid ip range: start is greater than or equal to end", + }, { + start: start4, + end: start4, + name: "start_eq_end", + wantErrMsg: "invalid ip range: start is greater than or equal to end", + }, { + start: start6, + end: end6Large, + name: "too_large", + wantErrMsg: "invalid ip range: range is too large", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := newIPRange(tc.start, tc.end) + testutil.AssertErrorMsg(t, tc.wantErrMsg, err) + }) + } +} + +func TestIPRange_Contains(t *testing.T) { + start, end := netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.3") + r, err := newIPRange(start, end) + require.NoError(t, err) + + testCases := []struct { + in netip.Addr + want assert.BoolAssertionFunc + name string + }{{ + in: start, + want: assert.True, + name: "start", + }, { + in: end, + want: assert.True, + name: "end", + }, { + in: start.Next(), + want: assert.True, + name: "within", + }, { + in: netip.MustParseAddr("0.0.0.0"), + want: assert.False, + name: "before", + }, { + in: netip.MustParseAddr("0.0.0.4"), + want: assert.False, + name: "after", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.want(t, r.contains(tc.in)) + }) + } + + t.Run("nil", func(t *testing.T) { + r = nil + assert.False(t, r.contains(start)) + }) +} + +func TestIPRange_Find(t *testing.T) { + start, end := netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.5") + r, err := newIPRange(start, end) + require.NoError(t, err) + + num, ok := r.offset(end) + require.True(t, ok) + + testCases := []struct { + predicate ipPredicate + want netip.Addr + name string + }{{ + predicate: func(ip netip.Addr) (ok bool) { + ipData := ip.AsSlice() + + return ipData[len(ipData)-1]%2 == 0 + }, + want: netip.MustParseAddr("0.0.0.2"), + name: "even", + }, { + predicate: func(ip netip.Addr) (ok bool) { + ipData := ip.AsSlice() + + return ipData[len(ipData)-1]%10 == 0 + }, + want: netip.Addr{}, + name: "none", + }, { + predicate: func(ip netip.Addr) (ok bool) { + return true + }, + want: start, + name: "first", + }, { + predicate: func(ip netip.Addr) (ok bool) { + off, _ := r.offset(ip) + + return off == num + }, + want: end, + name: "last", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := r.find(tc.predicate) + assert.Equal(t, tc.want, got) + }) + } + + t.Run("nil", func(t *testing.T) { + r = nil + assert.Equal(t, netip.Addr{}, r.find(func(netip.Addr) bool { return true })) + }) +} + +func TestIPRange_Offset(t *testing.T) { + start, end := netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.5") + r, err := newIPRange(start, end) + require.NoError(t, err) + + testCases := []struct { + in netip.Addr + name string + wantOffset uint64 + wantOK bool + }{{ + in: netip.MustParseAddr("0.0.0.2"), + name: "in", + wantOffset: 1, + wantOK: true, + }, { + in: start, + name: "in_start", + wantOffset: 0, + wantOK: true, + }, { + in: end, + name: "in_end", + wantOffset: 4, + wantOK: true, + }, { + in: netip.MustParseAddr("0.0.0.6"), + name: "out_after", + wantOffset: 0, + wantOK: false, + }, { + in: netip.MustParseAddr("0.0.0.0"), + name: "out_before", + wantOffset: 0, + wantOK: false, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + offset, ok := r.offset(tc.in) + assert.Equal(t, tc.wantOffset, offset) + assert.Equal(t, tc.wantOK, ok) + }) + } + + t.Run("nil", func(t *testing.T) { + r = nil + offset, ok := r.offset(start) + assert.False(t, ok) + assert.Zero(t, offset) + }) +} diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index 88b506abaf8..4dece53be1d 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -1,12 +1,165 @@ package dhcpsvc import ( + "fmt" + "net" + "net/netip" "sync/atomic" + "time" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) +// iface4 is a DHCP interface for IPv4 address family. +type iface4 struct { + // gateway is the IP address of the network gateway. + gateway netip.Addr + + // subnet is the network subnet. + // + // TODO(e.burkov): Make netip.Addr? + subnet netip.Prefix + + // addrSpace is the IPv4 address space allocated for leasing. + addrSpace *ipRange + + // name is the name of the interface. + name string + + // TODO(e.burkov): Add options. + + // leaseTTL is the time-to-live of dynamic leases on this interface. + leaseTTL time.Duration +} + +// newIface4 creates a new DHCP interface for IPv4 address family with the given +// configuration. It returns an error if the given configuration can't be used. +func newIface4(name string, conf *IPv4Config) (i *iface4, err error) { + if !conf.Enabled { + return nil, nil + } + + maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size() + subnet := netip.PrefixFrom(conf.GatewayIP, maskLen) + + switch { + case !subnet.Contains(conf.RangeStart): + return nil, fmt.Errorf("range start %s is not within %s", conf.RangeStart, subnet) + case !subnet.Contains(conf.RangeEnd): + return nil, fmt.Errorf("range end %s is not within %s", conf.RangeEnd, subnet) + } + + addrSpace, err := newIPRange(conf.RangeStart, conf.RangeEnd) + if err != nil { + return nil, err + } else if addrSpace.contains(conf.GatewayIP) { + return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace) + } + + return &iface4{ + name: name, + gateway: conf.GatewayIP, + subnet: subnet, + addrSpace: addrSpace, + leaseTTL: conf.LeaseDuration, + }, nil +} + +// iface6 is a DHCP interface for IPv6 address family. +// +// TODO(e.burkov): Add options. +type iface6 struct { + // rangeStart is the first IP address in the range. + rangeStart netip.Addr + + // name is the name of the interface. + name string + + // leaseTTL is the time-to-live of dynamic leases on this interface. + leaseTTL time.Duration + + // raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO + // flags. + raSLAACOnly bool + + // raAllowSLAAC defines if DHCP should send ICMPv6.RA packets with MO flags. + raAllowSLAAC bool +} + +// newIface6 creates a new DHCP interface for IPv6 address family with the given +// configuration. +// +// TODO(e.burkov): Validate properly. +func newIface6(name string, conf *IPv6Config) (i *iface6) { + if !conf.Enabled { + return nil + } + + return &iface6{ + name: name, + rangeStart: conf.RangeStart, + leaseTTL: conf.LeaseDuration, + raSLAACOnly: conf.RASLAACOnly, + raAllowSLAAC: conf.RAAllowSLAAC, + } +} + +// DHCPServer is a DHCP server for both IPv4 and IPv6 address families. type DHCPServer struct { + // enabled indicates whether the DHCP server is enabled and can provide + // information about its clients. enabled *atomic.Bool + + // interfaces4 is the set of IPv4 interfaces sorted by interface name. + interfaces4 []*iface4 + + // interfaces6 is the set of IPv6 interfaces sorted by interface name. + interfaces6 []*iface6 } -// func New(conf *Config) (srv *DHCPServer, err error) { -// } +// New creates a new DHCP server with the given configuration. It returns an +// error if the given configuration can't be used. +func New(conf *Config) (srv *DHCPServer, err error) { + if err = conf.Validate(); err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } else if !conf.Enabled { + // TODO(e.burkov): !! return Empty? + return nil, nil + } + + ifaces4 := make([]*iface4, len(conf.Interfaces)) + ifaces6 := make([]*iface6, len(conf.Interfaces)) + + ifaceNames := maps.Keys(conf.Interfaces) + slices.Sort(ifaceNames) + + var i4 *iface4 + var i6 *iface6 + + for _, ifaceName := range ifaceNames { + iface := conf.Interfaces[ifaceName] + + i4, err = newIface4(ifaceName, iface.IPv4) + if err != nil { + return nil, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err) + } else if i4 != nil { + ifaces4 = append(ifaces4, i4) + } + + i6 = newIface6(ifaceName, iface.IPv6) + if i6 != nil { + ifaces6 = append(ifaces6, i6) + } + } + + enabled := &atomic.Bool{} + enabled.Store(conf.Enabled) + + return &DHCPServer{ + enabled: enabled, + interfaces4: ifaces4, + interfaces6: ifaces6, + }, nil +} diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go new file mode 100644 index 00000000000..0dde69fdd5a --- /dev/null +++ b/internal/dhcpsvc/server_test.go @@ -0,0 +1,174 @@ +package dhcpsvc_test + +import ( + "net/netip" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" + "github.com/AdguardTeam/golibs/testutil" +) + +func TestNew(t *testing.T) { + const validLocalTLD = "local" + + validIPv4Conf := &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("192.168.0.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("192.168.0.2"), + RangeEnd: netip.MustParseAddr("192.168.0.254"), + LeaseDuration: 1 * time.Hour, + } + gwInRangeConf := &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("192.168.0.100"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("192.168.0.1"), + RangeEnd: netip.MustParseAddr("192.168.0.254"), + LeaseDuration: 1 * time.Hour, + } + badStartConf := &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("192.168.0.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("127.0.0.1"), + RangeEnd: netip.MustParseAddr("192.168.0.254"), + LeaseDuration: 1 * time.Hour, + } + + validIPv6Conf := &dhcpsvc.IPv6Config{ + Enabled: true, + RangeStart: netip.MustParseAddr("2001:db8::1"), + LeaseDuration: 1 * time.Hour, + RAAllowSLAAC: true, + RASLAACOnly: true, + } + + testCases := []struct { + conf *dhcpsvc.Config + name string + wantErrMsg string + }{{ + conf: nil, + name: "nil_config", + wantErrMsg: "config is nil", + }, { + conf: &dhcpsvc.Config{ + Enabled: false, + }, + name: "disabled", + wantErrMsg: "", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + }, + name: "bad_local_tld", + wantErrMsg: `bad domain name "": domain name is empty`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: nil, + }, + name: "no_interfaces", + wantErrMsg: "no interfaces specified", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": nil, + }, + }, + name: "nil_interface", + wantErrMsg: `interface "eth0": config is nil`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: nil, + IPv6: &dhcpsvc.IPv6Config{Enabled: false}, + }, + }, + }, + name: "nil_ipv4", + wantErrMsg: `interface "eth0": ipv4: config is nil`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: &dhcpsvc.IPv4Config{Enabled: false}, + IPv6: nil, + }, + }, + }, + name: "nil_ipv6", + wantErrMsg: `interface "eth0": ipv6: config is nil`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: validIPv4Conf, + IPv6: validIPv6Conf, + }, + }, + }, + name: "valid", + wantErrMsg: "", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: &dhcpsvc.IPv4Config{Enabled: false}, + IPv6: &dhcpsvc.IPv6Config{Enabled: false}, + }, + }, + }, + name: "disabled_interfaces", + wantErrMsg: "", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: gwInRangeConf, + IPv6: validIPv6Conf, + }, + }, + }, + name: "gateway_within_range", + wantErrMsg: `interface "eth0": ipv4: ` + + `gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: validLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: badStartConf, + IPv6: validIPv6Conf, + }, + }, + }, + name: "bad_start", + wantErrMsg: `interface "eth0": ipv4: ` + + `range start 127.0.0.1 is not within 192.168.0.1/24`, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := dhcpsvc.New(tc.conf) + testutil.AssertErrorMsg(t, tc.wantErrMsg, err) + }) + } +}