diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index 53dc21dc..cd1e93b2 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -6,14 +6,12 @@ import ( "log/slog" "net" "net/netip" - "slices" "sync" "sync/atomic" "time" "github.com/AdguardTeam/golibs/errors" - "github.com/AdguardTeam/golibs/logutil/slogutil" - "golang.org/x/exp/maps" + "github.com/AdguardTeam/golibs/mapsutil" ) // DHCPServer is a DHCP server for both IPv4 and IPv6 address families. @@ -23,8 +21,6 @@ type DHCPServer struct { enabled *atomic.Bool // logger logs common DHCP events. - // - // TODO(e.burkov): Use. logger *slog.Logger // localTLD is the top-level domain name to use for resolving DHCP clients' @@ -52,8 +48,7 @@ type DHCPServer struct { // // TODO(e.burkov): Use. func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) { - l := conf.Logger.With(slogutil.KeyPrefix, "dhcpsvc") - + l := conf.Logger if !conf.Enabled { l.DebugContext(ctx, "disabled") @@ -64,31 +59,24 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) { // TODO(e.burkov): Add validations scoped to the network interfaces set. ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces)) ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces)) - - ifaceNames := maps.Keys(conf.Interfaces) - slices.Sort(ifaceNames) - - var i4 *netInterfaceV4 - var i6 *netInterfaceV6 - var errs []error - for _, ifaceName := range ifaceNames { - iface := conf.Interfaces[ifaceName] - - i4, err = newNetInterfaceV4(ctx, l, ifaceName, iface.IPv4) + mapsutil.SortedRange(conf.Interfaces, func(name string, iface *InterfaceConfig) (cont bool) { + var i4 *netInterfaceV4 + i4, err = newNetInterfaceV4(ctx, l, name, iface.IPv4) if err != nil { - errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err)) + errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err)) } else if i4 != nil { ifaces4 = append(ifaces4, i4) } - i6 = newNetInterfaceV6(ctx, l, ifaceName, iface.IPv6) + i6 := newNetInterfaceV6(ctx, l, name, iface.IPv6) if i6 != nil { ifaces6 = append(ifaces6, i6) } - } + return true + }) if err = errors.Join(errs...); err != nil { return nil, err } @@ -178,7 +166,7 @@ func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) { } // Reset implements the [Interface] interface for *DHCPServer. -func (srv *DHCPServer) Reset() (err error) { +func (srv *DHCPServer) Reset(ctx context.Context) (err error) { srv.leasesMu.Lock() defer srv.leasesMu.Unlock() @@ -190,11 +178,13 @@ func (srv *DHCPServer) Reset() (err error) { } srv.leases.clear() + srv.logger.DebugContext(ctx, "reset leases") + return nil } // AddLease implements the [Interface] interface for *DHCPServer. -func (srv *DHCPServer) AddLease(l *Lease) (err error) { +func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) { defer func() { err = errors.Annotate(err, "adding lease: %w") }() addr := l.IP @@ -207,13 +197,27 @@ func (srv *DHCPServer) AddLease(l *Lease) (err error) { srv.leasesMu.Lock() defer srv.leasesMu.Unlock() - return srv.leases.add(l, iface) + err = srv.leases.add(l, iface) + if err != nil { + // Don't wrap the error since there is already an annotation deferred. + return err + } + + iface.logger.DebugContext( + ctx, "added lease", + "hostname", l.Hostname, + "ip", l.IP, + "mac", l.HWAddr, + "static", l.IsStatic, + ) + + return nil } // UpdateStaticLease implements the [Interface] interface for *DHCPServer. // // TODO(e.burkov): Support moving leases between interfaces. -func (srv *DHCPServer) UpdateStaticLease(l *Lease) (err error) { +func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err error) { defer func() { err = errors.Annotate(err, "updating static lease: %w") }() addr := l.IP @@ -226,11 +230,25 @@ func (srv *DHCPServer) UpdateStaticLease(l *Lease) (err error) { srv.leasesMu.Lock() defer srv.leasesMu.Unlock() - return srv.leases.update(l, iface) + err = srv.leases.update(l, iface) + if err != nil { + // Don't wrap the error since there is already an annotation deferred. + return err + } + + iface.logger.DebugContext( + ctx, "updated lease", + "hostname", l.Hostname, + "ip", l.IP, + "mac", l.HWAddr, + "static", l.IsStatic, + ) + + return nil } // RemoveLease implements the [Interface] interface for *DHCPServer. -func (srv *DHCPServer) RemoveLease(l *Lease) (err error) { +func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) { defer func() { err = errors.Annotate(err, "removing lease: %w") }() addr := l.IP @@ -243,7 +261,21 @@ func (srv *DHCPServer) RemoveLease(l *Lease) (err error) { srv.leasesMu.Lock() defer srv.leasesMu.Unlock() - return srv.leases.remove(l, iface) + err = srv.leases.remove(l, iface) + if err != nil { + // Don't wrap the error since there is already an annotation deferred. + return err + } + + iface.logger.DebugContext( + ctx, "removed lease", + "hostname", l.Hostname, + "ip", l.IP, + "mac", l.HWAddr, + "static", l.IsStatic, + ) + + return nil } // ifaceForAddr returns the handled network interface for the given IP address, diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go index d348d723..5f6f002f 100644 --- a/internal/dhcpsvc/server_test.go +++ b/internal/dhcpsvc/server_test.go @@ -202,7 +202,7 @@ func TestDHCPServer_AddLease(t *testing.T) { mac2 := mustParseMAC(t, "06:05:04:03:02:01") mac3 := mustParseMAC(t, "02:03:04:05:06:07") - require.NoError(t, srv.AddLease(&dhcpsvc.Lease{ + require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{ Hostname: host1, IP: ip1, HWAddr: mac1, @@ -277,7 +277,7 @@ func TestDHCPServer_AddLease(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(tc.lease)) + testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease)) }) } } @@ -332,7 +332,7 @@ func TestDHCPServer_index(t *testing.T) { IsStatic: true, }} for _, l := range leases { - require.NoError(t, srv.AddLease(l)) + require.NoError(t, srv.AddLease(ctx, l)) } t.Run("ip_idx", func(t *testing.T) { @@ -408,7 +408,7 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) { IsStatic: true, }} for _, l := range leases { - require.NoError(t, srv.AddLease(l)) + require.NoError(t, srv.AddLease(ctx, l)) } testCases := []struct { @@ -478,7 +478,7 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(tc.lease)) + testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease)) }) } } @@ -520,7 +520,7 @@ func TestDHCPServer_RemoveLease(t *testing.T) { IsStatic: true, }} for _, l := range leases { - require.NoError(t, srv.AddLease(l)) + require.NoError(t, srv.AddLease(ctx, l)) } testCases := []struct { @@ -571,7 +571,7 @@ func TestDHCPServer_RemoveLease(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(tc.lease)) + testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(ctx, tc.lease)) }) } @@ -612,12 +612,12 @@ func TestDHCPServer_Reset(t *testing.T) { }} for _, l := range leases { - require.NoError(t, srv.AddLease(l)) + require.NoError(t, srv.AddLease(ctx, l)) } require.Len(t, srv.Leases(), len(leases)) - require.NoError(t, srv.Reset()) + require.NoError(t, srv.Reset(ctx)) assert.Empty(t, srv.Leases()) } diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index ed75bf89..09df8013 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -75,7 +75,7 @@ func (c *IPv4Config) validate() (err error) { } if c.LeaseDuration <= 0 { - err = newMustErr("lease duration", "be less than %d", c.LeaseDuration) + err = newMustErr("icmp timeout", "be positive", c.LeaseDuration) errs = append(errs, err) } @@ -341,15 +341,15 @@ func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp laye slices.SortFunc(imp, compareV4OptionCodes) // Set values for explicitly configured options. - for _, e := range c.Options { - i, found := slices.BinarySearchFunc(imp, e, compareV4OptionCodes) + for _, o := range c.Options { + i, found := slices.BinarySearchFunc(imp, o, compareV4OptionCodes) if found { imp = slices.Delete(imp, i, i+1) } - i, found = slices.BinarySearchFunc(exp, e, compareV4OptionCodes) - if e.Length > 0 { - exp = slices.Insert(exp, i, e) + i, found = slices.BinarySearchFunc(exp, o, compareV4OptionCodes) + if o.Length > 0 { + exp = slices.Insert(exp, i, o) } else if found { exp = slices.Delete(exp, i, i+1) } diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go index ce8fcb35..a1ee56ac 100644 --- a/internal/dhcpsvc/v6.go +++ b/internal/dhcpsvc/v6.go @@ -62,36 +62,6 @@ func (c *IPv6Config) validate() (err error) { return errors.Join(errs...) } -// options returns the implicit and explicit options for the interface. The two -// lists are disjoint and the implicit options are initialized with default -// values. -// -// TODO(e.burkov): Add implicit options according to RFC. -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. - imp = layers.DHCPv6Options{} - slices.SortFunc(imp, compareV6OptionCodes) - - // Set values for explicitly configured options. - for _, e := range c.Options { - i, found := slices.BinarySearchFunc(imp, e, compareV6OptionCodes) - if found { - imp = slices.Delete(imp, i, i+1) - } - - exp = append(exp, e) - } - - l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp) - - return imp, exp -} - -// compareV6OptionCodes compares option codes of a and b. -func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) { - return int(a.Code) - int(b.Code) -} - // netInterfaceV6 is a DHCP interface for IPv6 address family. // // TODO(e.burkov): Add options. @@ -142,6 +112,7 @@ func newNetInterfaceV6( netInterface: netInterface{ name: name, leaseTTL: conf.LeaseDuration, + logger: l, }, raSLAACOnly: conf.RASLAACOnly, raAllowSLAAC: conf.RAAllowSLAAC, @@ -175,3 +146,33 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool return &ifaces[i].netInterface, true } + +// options returns the implicit and explicit options for the interface. The two +// lists are disjoint and the implicit options are initialized with default +// values. +// +// TODO(e.burkov): Add implicit options according to RFC. +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. + imp = layers.DHCPv6Options{} + slices.SortFunc(imp, compareV6OptionCodes) + + // Set values for explicitly configured options. + for _, e := range c.Options { + i, found := slices.BinarySearchFunc(imp, e, compareV6OptionCodes) + if found { + imp = slices.Delete(imp, i, i+1) + } + + exp = append(exp, e) + } + + l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp) + + return imp, exp +} + +// compareV6OptionCodes compares option codes of a and b. +func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) { + return int(a.Code) - int(b.Code) +}