From ae1713e5f717a66863eb0289e3aa66c7069ac8bf Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Mon, 1 Jul 2024 15:49:22 +0300 Subject: [PATCH] dhcpsvc: use slog --- internal/dhcpsvc/config.go | 40 +++-- internal/dhcpsvc/config_test.go | 3 +- internal/dhcpsvc/dhcpsvc.go | 8 + internal/dhcpsvc/interface.go | 4 + internal/dhcpsvc/server.go | 20 ++- internal/dhcpsvc/server_test.go | 40 ++++- internal/dhcpsvc/v4.go | 253 +++++++++++++++------------ internal/dhcpsvc/v4_internal_test.go | 8 +- internal/dhcpsvc/v6.go | 62 ++++--- 9 files changed, 277 insertions(+), 161 deletions(-) diff --git a/internal/dhcpsvc/config.go b/internal/dhcpsvc/config.go index bb5c46a0..c1d7910d 100644 --- a/internal/dhcpsvc/config.go +++ b/internal/dhcpsvc/config.go @@ -2,11 +2,12 @@ package dhcpsvc import ( "fmt" - "slices" + "log/slog" "time" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/mapsutil" "github.com/AdguardTeam/golibs/netutil" - "golang.org/x/exp/maps" ) // Config is the configuration for the DHCP service. @@ -15,6 +16,9 @@ type Config struct { // interface identified by its name. Interfaces map[string]*InterfaceConfig + // Logger will be used to log the DHCP events. + Logger *slog.Logger + // LocalDomainName is the top-level domain name to use for resolving DHCP // clients' hostnames. LocalDomainName string @@ -38,36 +42,44 @@ type InterfaceConfig struct { } // Validate returns an error in conf if any. +// +// TODO(e.burkov): Unexport and rewrite the test. func (conf *Config) Validate() (err error) { switch { case conf == nil: return errNilConfig case !conf.Enabled: return nil - case conf.ICMPTimeout < 0: - return newMustErr("icmp timeout", "be non-negative", conf.ICMPTimeout) + } + + var errs []error + if conf.ICMPTimeout < 0 { + err = newMustErr("icmp timeout", "be non-negative", conf.ICMPTimeout) + errs = append(errs, err) } err = netutil.ValidateDomainName(conf.LocalDomainName) if err != nil { // Don't wrap the error since it's informative enough as is. - return err + errs = append(errs, err) } if len(conf.Interfaces) == 0 { - return errNoInterfaces + errs = append(errs, errNoInterfaces) + + return errors.Join(errs...) } - ifaces := maps.Keys(conf.Interfaces) - slices.Sort(ifaces) - - for _, iface := range ifaces { - if err = conf.Interfaces[iface].validate(); err != nil { - return fmt.Errorf("interface %q: %w", iface, err) + mapsutil.SortedRange(conf.Interfaces, func(iface string, ic *InterfaceConfig) (ok bool) { + err = ic.validate() + if err != nil { + errs = append(errs, fmt.Errorf("interface %q: %w", iface, err)) } - } - return nil + return true + }) + + return errors.Join(errs...) } // validate returns an error in ic, if any. diff --git a/internal/dhcpsvc/config_test.go b/internal/dhcpsvc/config_test.go index 6663d378..aa87b0d6 100644 --- a/internal/dhcpsvc/config_test.go +++ b/internal/dhcpsvc/config_test.go @@ -23,7 +23,8 @@ func TestConfig_Validate(t *testing.T) { }, { name: "empty", conf: &dhcpsvc.Config{ - Enabled: true, + Enabled: true, + Interfaces: testInterfaceConf, }, wantErrMsg: `bad domain name "": domain name is empty`, }, { diff --git a/internal/dhcpsvc/dhcpsvc.go b/internal/dhcpsvc/dhcpsvc.go index 41e0037e..5ffcf5cb 100644 --- a/internal/dhcpsvc/dhcpsvc.go +++ b/internal/dhcpsvc/dhcpsvc.go @@ -11,6 +11,14 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/next/agh" ) +const ( + // keyInterface is the key for logging the network interface name. + keyInterface = "iface" + + // keyFamily is the key for logging the handled address family. + keyFamily = "family" +) + // Interface is a DHCP service. // // TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate diff --git a/internal/dhcpsvc/interface.go b/internal/dhcpsvc/interface.go index ebb225e6..13dadb4a 100644 --- a/internal/dhcpsvc/interface.go +++ b/internal/dhcpsvc/interface.go @@ -2,6 +2,7 @@ package dhcpsvc import ( "fmt" + "log/slog" "slices" "time" ) @@ -11,6 +12,9 @@ import ( // // TODO(e.burkov): Add other methods as [DHCPServer] evolves. type netInterface struct { + // logger logs the events related to the network interface. + logger *slog.Logger + // name is the name of the network interface. name string diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index bc354b00..da64ca80 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -1,6 +1,7 @@ package dhcpsvc import ( + "context" "fmt" "net" "net/netip" @@ -10,6 +11,7 @@ import ( "time" "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/logutil/slogutil" "golang.org/x/exp/maps" ) @@ -43,8 +45,12 @@ type DHCPServer struct { // error if the given configuration can't be used. // // TODO(e.burkov): Use. -func New(conf *Config) (srv *DHCPServer, err error) { +func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) { + l := conf.Logger.With(slogutil.KeyPrefix, "dhcpsvc") + if !conf.Enabled { + l.DebugContext(ctx, "disabled") + // TODO(e.burkov): Perhaps return [Empty]? return nil, nil } @@ -59,22 +65,28 @@ func New(conf *Config) (srv *DHCPServer, err error) { var i4 *netInterfaceV4 var i6 *netInterfaceV6 + var errs []error + for _, ifaceName := range ifaceNames { iface := conf.Interfaces[ifaceName] - i4, err = newNetInterfaceV4(ifaceName, iface.IPv4) + i4, err = newNetInterfaceV4(ctx, l, ifaceName, iface.IPv4) if err != nil { - return nil, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err) + errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err)) } else if i4 != nil { ifaces4 = append(ifaces4, i4) } - i6 = newNetInterfaceV6(ifaceName, iface.IPv6) + i6 = newNetInterfaceV6(ctx, l, ifaceName, iface.IPv6) if i6 != nil { ifaces6 = append(ifaces6, i6) } } + if err = errors.Join(errs...); err != nil { + return nil, err + } + enabled := &atomic.Bool{} enabled.Store(conf.Enabled) diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go index 6d5bc9d8..d348d723 100644 --- a/internal/dhcpsvc/server_test.go +++ b/internal/dhcpsvc/server_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" + "github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +17,12 @@ import ( // testLocalTLD is a common local TLD for tests. const testLocalTLD = "local" +// testTimeout is a common timeout for tests and contexts. +const testTimeout time.Duration = 10 * time.Second + +// discardLog is a logger to discard test output. +var discardLog = slogutil.NewDiscardLogger() + // testInterfaceConf is a common set of interface configurations for tests. var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{ "eth0": { @@ -103,6 +110,7 @@ func TestNew(t *testing.T) { }{{ conf: &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: map[string]*dhcpsvc.InterfaceConfig{ "eth0": { @@ -116,6 +124,7 @@ func TestNew(t *testing.T) { }, { conf: &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: map[string]*dhcpsvc.InterfaceConfig{ "eth0": { @@ -129,6 +138,7 @@ func TestNew(t *testing.T) { }, { conf: &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: map[string]*dhcpsvc.InterfaceConfig{ "eth0": { @@ -143,6 +153,7 @@ func TestNew(t *testing.T) { }, { conf: &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: map[string]*dhcpsvc.InterfaceConfig{ "eth0": { @@ -156,17 +167,22 @@ func TestNew(t *testing.T) { `range start 127.0.0.1 is not within 192.168.0.1/24`, }} + ctx := testutil.ContextWithTimeout(t, testTimeout) + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := dhcpsvc.New(tc.conf) + _, err := dhcpsvc.New(ctx, tc.conf) testutil.AssertErrorMsg(t, tc.wantErrMsg, err) }) } } func TestDHCPServer_AddLease(t *testing.T) { - srv, err := dhcpsvc.New(&dhcpsvc.Config{ + ctx := testutil.ContextWithTimeout(t, testTimeout) + + srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, }) @@ -267,8 +283,11 @@ func TestDHCPServer_AddLease(t *testing.T) { } func TestDHCPServer_index(t *testing.T) { - srv, err := dhcpsvc.New(&dhcpsvc.Config{ + ctx := testutil.ContextWithTimeout(t, testTimeout) + + srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, }) @@ -342,8 +361,11 @@ func TestDHCPServer_index(t *testing.T) { } func TestDHCPServer_UpdateStaticLease(t *testing.T) { - srv, err := dhcpsvc.New(&dhcpsvc.Config{ + ctx := testutil.ContextWithTimeout(t, testTimeout) + + srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, }) @@ -462,8 +484,11 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) { } func TestDHCPServer_RemoveLease(t *testing.T) { - srv, err := dhcpsvc.New(&dhcpsvc.Config{ + ctx := testutil.ContextWithTimeout(t, testTimeout) + + srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, }) @@ -554,8 +579,11 @@ func TestDHCPServer_RemoveLease(t *testing.T) { } func TestDHCPServer_Reset(t *testing.T) { - srv, err := dhcpsvc.New(&dhcpsvc.Config{ + ctx := testutil.ContextWithTimeout(t, testTimeout) + + srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, + Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, }) diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index a93f1103..ed75bf89 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -1,13 +1,15 @@ package dhcpsvc import ( + "context" "fmt" + "log/slog" "net" "net/netip" "slices" "time" - "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/netutil" "github.com/google/gopacket/layers" ) @@ -43,25 +45,133 @@ type IPv4Config struct { } // validate returns an error in conf if any. -func (conf *IPv4Config) validate() (err error) { - switch { - case conf == nil: +func (c *IPv4Config) validate() (err error) { + if c == nil { return errNilConfig - case !conf.Enabled: - return nil - case !conf.GatewayIP.Is4(): - return newMustErr("gateway ip", "be a valid ipv4", conf.GatewayIP) - case !conf.SubnetMask.Is4(): - return newMustErr("subnet mask", "be a valid ipv4 cidr mask", conf.SubnetMask) - case !conf.RangeStart.Is4(): - return newMustErr("range start", "be a valid ipv4", conf.RangeStart) - case !conf.RangeEnd.Is4(): - return newMustErr("range end", "be a valid ipv4", conf.RangeEnd) - case conf.LeaseDuration <= 0: - return newMustErr("lease duration", "be less than %d", conf.LeaseDuration) - default: + } else if !c.Enabled { return nil } + + var errs []error + + if !c.GatewayIP.Is4() { + err = newMustErr("gateway ip", "be a valid ipv4", c.GatewayIP) + errs = append(errs, err) + } + + if !c.SubnetMask.Is4() { + err = newMustErr("subnet mask", "be a valid ipv4 cidr mask", c.SubnetMask) + errs = append(errs, err) + } + + if !c.RangeStart.Is4() { + err = newMustErr("range start", "be a valid ipv4", c.RangeStart) + errs = append(errs, err) + } + + if !c.RangeEnd.Is4() { + err = newMustErr("range end", "be a valid ipv4", c.RangeEnd) + errs = append(errs, err) + } + + if c.LeaseDuration <= 0 { + err = newMustErr("lease duration", "be less than %d", c.LeaseDuration) + errs = append(errs, err) + } + + return errors.Join(errs...) +} + +// netInterfaceV4 is a DHCP interface for IPv4 address family. +type netInterfaceV4 struct { + // gateway is the IP address of the network gateway. + gateway netip.Addr + + // subnet is the network subnet. + subnet netip.Prefix + + // addrSpace is the IPv4 address space allocated for leasing. + addrSpace ipRange + + // implicitOpts are the options listed in Appendix A of RFC 2131 and + // initialized with default values. It must not have intersections with + // explicitOpts. + implicitOpts layers.DHCPOptions + + // explicitOpts are the user-configured options. It must not have + // intersections with implicitOpts. + explicitOpts layers.DHCPOptions + + // netInterface is embedded here to provide some common network interface + // logic. + netInterface +} + +// newNetInterfaceV4 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 newNetInterfaceV4( + ctx context.Context, + l *slog.Logger, + name string, + conf *IPv4Config, +) (i *netInterfaceV4, err error) { + l = l.With( + keyInterface, name, + keyFamily, netutil.AddrFamilyIPv4, + ) + if !conf.Enabled { + l.DebugContext(ctx, "disabled") + + 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) + } + + i = &netInterfaceV4{ + gateway: conf.GatewayIP, + subnet: subnet, + addrSpace: addrSpace, + netInterface: netInterface{ + name: name, + leaseTTL: conf.LeaseDuration, + logger: l, + }, + } + i.implicitOpts, i.explicitOpts = conf.options(ctx, l) + + return i, nil +} + +// netInterfacesV4 is a slice of network interfaces of IPv4 address family. +type netInterfacesV4 []*netInterfaceV4 + +// find returns the first network interface within ifaces containing ip. It +// returns false if there is no such interface. +func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) { + i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) { + return iface.subnet.Contains(ip) + }) + if i < 0 { + return nil, false + } + + return &ifaces[i].netInterface, true } // options returns the implicit and explicit options for the interface. The two @@ -69,14 +179,14 @@ func (conf *IPv4Config) validate() (err error) { // values. // // TODO(e.burkov): DRY with the IPv6 version. -func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) { +func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPOptions) { // Set default values of host configuration parameters listed in Appendix A // of RFC-2131. - implicit = layers.DHCPOptions{ + imp = layers.DHCPOptions{ // Values From Configuration - layers.NewDHCPOption(layers.DHCPOptSubnetMask, conf.SubnetMask.AsSlice()), - layers.NewDHCPOption(layers.DHCPOptRouter, conf.GatewayIP.AsSlice()), + layers.NewDHCPOption(layers.DHCPOptSubnetMask, c.SubnetMask.AsSlice()), + layers.NewDHCPOption(layers.DHCPOptRouter, c.GatewayIP.AsSlice()), // IP-Layer Per Host @@ -228,110 +338,29 @@ func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) { // See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6. layers.NewDHCPOption(layers.DHCPOptTCPKeepAliveGarbage, []byte{0x1}), } - slices.SortFunc(implicit, compareV4OptionCodes) + slices.SortFunc(imp, compareV4OptionCodes) // Set values for explicitly configured options. - for _, exp := range conf.Options { - i, found := slices.BinarySearchFunc(implicit, exp, compareV4OptionCodes) + for _, e := range c.Options { + i, found := slices.BinarySearchFunc(imp, e, compareV4OptionCodes) if found { - implicit = slices.Delete(implicit, i, i+1) + imp = slices.Delete(imp, i, i+1) } - i, found = slices.BinarySearchFunc(explicit, exp, compareV4OptionCodes) - if exp.Length > 0 { - explicit = slices.Insert(explicit, i, exp) + i, found = slices.BinarySearchFunc(exp, e, compareV4OptionCodes) + if e.Length > 0 { + exp = slices.Insert(exp, i, e) } else if found { - explicit = slices.Delete(explicit, i, i+1) + exp = slices.Delete(exp, i, i+1) } } - log.Debug("dhcpsvc: v4: implicit options: %s", implicit) - log.Debug("dhcpsvc: v4: explicit options: %s", explicit) + l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp) - return implicit, explicit + return imp, exp } // compareV4OptionCodes compares option codes of a and b. func compareV4OptionCodes(a, b layers.DHCPOption) (res int) { return int(a.Type) - int(b.Type) } - -// netInterfaceV4 is a DHCP interface for IPv4 address family. -type netInterfaceV4 struct { - // gateway is the IP address of the network gateway. - gateway netip.Addr - - // subnet is the network subnet. - subnet netip.Prefix - - // addrSpace is the IPv4 address space allocated for leasing. - addrSpace ipRange - - // implicitOpts are the options listed in Appendix A of RFC 2131 and - // initialized with default values. It must not have intersections with - // explicitOpts. - implicitOpts layers.DHCPOptions - - // explicitOpts are the user-configured options. It must not have - // intersections with implicitOpts. - explicitOpts layers.DHCPOptions - - // netInterface is embedded here to provide some common network interface - // logic. - netInterface -} - -// newNetInterfaceV4 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 newNetInterfaceV4(name string, conf *IPv4Config) (i *netInterfaceV4, 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) - } - - i = &netInterfaceV4{ - gateway: conf.GatewayIP, - subnet: subnet, - addrSpace: addrSpace, - netInterface: netInterface{ - name: name, - leaseTTL: conf.LeaseDuration, - }, - } - i.implicitOpts, i.explicitOpts = conf.options() - - return i, nil -} - -// netInterfacesV4 is a slice of network interfaces of IPv4 address family. -type netInterfacesV4 []*netInterfaceV4 - -// find returns the first network interface within ifaces containing ip. It -// returns false if there is no such interface. -func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) { - i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) { - return iface.subnet.Contains(ip) - }) - if i < 0 { - return nil, false - } - - return &ifaces[i].netInterface, true -} diff --git a/internal/dhcpsvc/v4_internal_test.go b/internal/dhcpsvc/v4_internal_test.go index 0b65366e..7aa64505 100644 --- a/internal/dhcpsvc/v4_internal_test.go +++ b/internal/dhcpsvc/v4_internal_test.go @@ -3,7 +3,10 @@ package dhcpsvc import ( "net/netip" "testing" + "time" + "github.com/AdguardTeam/golibs/logutil/slogutil" + "github.com/AdguardTeam/golibs/testutil" "github.com/google/gopacket/layers" "github.com/stretchr/testify/assert" ) @@ -75,9 +78,12 @@ func TestIPv4Config_Options(t *testing.T) { wantExplicit: layers.DHCPOptions{opt1}, }} + ctx := testutil.ContextWithTimeout(t, time.Second) + l := slogutil.NewDiscardLogger() + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - imp, exp := tc.conf.options() + imp, exp := tc.conf.options(ctx, l) assert.Equal(t, tc.wantExplicit, exp) for c := range exp { diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go index 09342569..ce8fcb35 100644 --- a/internal/dhcpsvc/v6.go +++ b/internal/dhcpsvc/v6.go @@ -1,12 +1,14 @@ package dhcpsvc import ( + "context" "fmt" + "log/slog" "net/netip" "slices" "time" - "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/netutil" "github.com/google/gopacket/layers" ) @@ -38,19 +40,26 @@ type IPv6Config struct { } // validate returns an error in conf if any. -func (conf *IPv6Config) validate() (err error) { - switch { - case conf == nil: +func (c *IPv6Config) validate() (err error) { + if c == nil { return errNilConfig - case !conf.Enabled: - return nil - 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: + } else if !c.Enabled { return nil } + + var errs []error + + if !c.RangeStart.Is6() { + err = fmt.Errorf("range start %s should be a valid ipv6", c.RangeStart) + errs = append(errs, err) + } + + if c.LeaseDuration <= 0 { + err = fmt.Errorf("lease duration %s must be positive", c.LeaseDuration) + errs = append(errs, err) + } + + return errors.Join(errs...) } // options returns the implicit and explicit options for the interface. The two @@ -58,25 +67,24 @@ func (conf *IPv6Config) validate() (err error) { // values. // // TODO(e.burkov): Add implicit options according to RFC. -func (conf *IPv6Config) options() (implicit, explicit layers.DHCPv6Options) { +func (c *IPv6Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPv6Options) { // Set default values of host configuration parameters listed in RFC 8415. - implicit = layers.DHCPv6Options{} - slices.SortFunc(implicit, compareV6OptionCodes) + imp = layers.DHCPv6Options{} + slices.SortFunc(imp, compareV6OptionCodes) // Set values for explicitly configured options. - for _, exp := range conf.Options { - i, found := slices.BinarySearchFunc(implicit, exp, compareV6OptionCodes) + for _, e := range c.Options { + i, found := slices.BinarySearchFunc(imp, e, compareV6OptionCodes) if found { - implicit = slices.Delete(implicit, i, i+1) + imp = slices.Delete(imp, i, i+1) } - explicit = append(explicit, exp) + exp = append(exp, e) } - log.Debug("dhcpsvc: v6: implicit options: %s", implicit) - log.Debug("dhcpsvc: v6: explicit options: %s", explicit) + l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp) - return implicit, explicit + return imp, exp } // compareV6OptionCodes compares option codes of a and b. @@ -116,8 +124,16 @@ type netInterfaceV6 struct { // the given configuration. // // TODO(e.burkov): Validate properly. -func newNetInterfaceV6(name string, conf *IPv6Config) (i *netInterfaceV6) { +func newNetInterfaceV6( + ctx context.Context, + l *slog.Logger, + name string, + conf *IPv6Config, +) (i *netInterfaceV6) { + l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6) if !conf.Enabled { + l.DebugContext(ctx, "disabled") + return nil } @@ -130,7 +146,7 @@ func newNetInterfaceV6(name string, conf *IPv6Config) (i *netInterfaceV6) { raSLAACOnly: conf.RASLAACOnly, raAllowSLAAC: conf.RAAllowSLAAC, } - i.implicitOpts, i.explicitOpts = conf.options() + i.implicitOpts, i.explicitOpts = conf.options(ctx, l) return i }