dhcpsvc: use slog

This commit is contained in:
Eugene Burkov 2024-07-01 15:49:22 +03:00
parent 3993f4c476
commit ae1713e5f7
9 changed files with 277 additions and 161 deletions

View file

@ -2,11 +2,12 @@ package dhcpsvc
import ( import (
"fmt" "fmt"
"slices" "log/slog"
"time" "time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/mapsutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"golang.org/x/exp/maps"
) )
// Config is the configuration for the DHCP service. // Config is the configuration for the DHCP service.
@ -15,6 +16,9 @@ type Config struct {
// interface identified by its name. // interface identified by its name.
Interfaces map[string]*InterfaceConfig 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 // LocalDomainName is the top-level domain name to use for resolving DHCP
// clients' hostnames. // clients' hostnames.
LocalDomainName string LocalDomainName string
@ -38,36 +42,44 @@ type InterfaceConfig struct {
} }
// Validate returns an error in conf if any. // Validate returns an error in conf if any.
//
// TODO(e.burkov): Unexport and rewrite the test.
func (conf *Config) Validate() (err error) { func (conf *Config) Validate() (err error) {
switch { switch {
case conf == nil: case conf == nil:
return errNilConfig return errNilConfig
case !conf.Enabled: case !conf.Enabled:
return nil 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) err = netutil.ValidateDomainName(conf.LocalDomainName)
if err != nil { if err != nil {
// Don't wrap the error since it's informative enough as is. // Don't wrap the error since it's informative enough as is.
return err errs = append(errs, err)
} }
if len(conf.Interfaces) == 0 { if len(conf.Interfaces) == 0 {
return errNoInterfaces errs = append(errs, errNoInterfaces)
return errors.Join(errs...)
} }
ifaces := maps.Keys(conf.Interfaces) mapsutil.SortedRange(conf.Interfaces, func(iface string, ic *InterfaceConfig) (ok bool) {
slices.Sort(ifaces) err = ic.validate()
if err != nil {
for _, iface := range ifaces { errs = append(errs, fmt.Errorf("interface %q: %w", iface, err))
if err = conf.Interfaces[iface].validate(); err != nil {
return fmt.Errorf("interface %q: %w", iface, err)
} }
}
return nil return true
})
return errors.Join(errs...)
} }
// validate returns an error in ic, if any. // validate returns an error in ic, if any.

View file

@ -23,7 +23,8 @@ func TestConfig_Validate(t *testing.T) {
}, { }, {
name: "empty", name: "empty",
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
Enabled: true, Enabled: true,
Interfaces: testInterfaceConf,
}, },
wantErrMsg: `bad domain name "": domain name is empty`, wantErrMsg: `bad domain name "": domain name is empty`,
}, { }, {

View file

@ -11,6 +11,14 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/next/agh" "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. // Interface is a DHCP service.
// //
// TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate // TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate

View file

@ -2,6 +2,7 @@ package dhcpsvc
import ( import (
"fmt" "fmt"
"log/slog"
"slices" "slices"
"time" "time"
) )
@ -11,6 +12,9 @@ import (
// //
// TODO(e.burkov): Add other methods as [DHCPServer] evolves. // TODO(e.burkov): Add other methods as [DHCPServer] evolves.
type netInterface struct { type netInterface struct {
// logger logs the events related to the network interface.
logger *slog.Logger
// name is the name of the network interface. // name is the name of the network interface.
name string name string

View file

@ -1,6 +1,7 @@
package dhcpsvc package dhcpsvc
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
@ -10,6 +11,7 @@ import (
"time" "time"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
@ -43,8 +45,12 @@ type DHCPServer struct {
// error if the given configuration can't be used. // error if the given configuration can't be used.
// //
// TODO(e.burkov): Use. // 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 { if !conf.Enabled {
l.DebugContext(ctx, "disabled")
// TODO(e.burkov): Perhaps return [Empty]? // TODO(e.burkov): Perhaps return [Empty]?
return nil, nil return nil, nil
} }
@ -59,22 +65,28 @@ func New(conf *Config) (srv *DHCPServer, err error) {
var i4 *netInterfaceV4 var i4 *netInterfaceV4
var i6 *netInterfaceV6 var i6 *netInterfaceV6
var errs []error
for _, ifaceName := range ifaceNames { for _, ifaceName := range ifaceNames {
iface := conf.Interfaces[ifaceName] iface := conf.Interfaces[ifaceName]
i4, err = newNetInterfaceV4(ifaceName, iface.IPv4) i4, err = newNetInterfaceV4(ctx, l, ifaceName, iface.IPv4)
if err != nil { 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 { } else if i4 != nil {
ifaces4 = append(ifaces4, i4) ifaces4 = append(ifaces4, i4)
} }
i6 = newNetInterfaceV6(ifaceName, iface.IPv6) i6 = newNetInterfaceV6(ctx, l, ifaceName, iface.IPv6)
if i6 != nil { if i6 != nil {
ifaces6 = append(ifaces6, i6) ifaces6 = append(ifaces6, i6)
} }
} }
if err = errors.Join(errs...); err != nil {
return nil, err
}
enabled := &atomic.Bool{} enabled := &atomic.Bool{}
enabled.Store(conf.Enabled) enabled.Store(conf.Enabled)

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -16,6 +17,12 @@ import (
// testLocalTLD is a common local TLD for tests. // testLocalTLD is a common local TLD for tests.
const testLocalTLD = "local" 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. // testInterfaceConf is a common set of interface configurations for tests.
var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{ var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
"eth0": { "eth0": {
@ -103,6 +110,7 @@ func TestNew(t *testing.T) {
}{{ }{{
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: map[string]*dhcpsvc.InterfaceConfig{ Interfaces: map[string]*dhcpsvc.InterfaceConfig{
"eth0": { "eth0": {
@ -116,6 +124,7 @@ func TestNew(t *testing.T) {
}, { }, {
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: map[string]*dhcpsvc.InterfaceConfig{ Interfaces: map[string]*dhcpsvc.InterfaceConfig{
"eth0": { "eth0": {
@ -129,6 +138,7 @@ func TestNew(t *testing.T) {
}, { }, {
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: map[string]*dhcpsvc.InterfaceConfig{ Interfaces: map[string]*dhcpsvc.InterfaceConfig{
"eth0": { "eth0": {
@ -143,6 +153,7 @@ func TestNew(t *testing.T) {
}, { }, {
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: map[string]*dhcpsvc.InterfaceConfig{ Interfaces: map[string]*dhcpsvc.InterfaceConfig{
"eth0": { "eth0": {
@ -156,17 +167,22 @@ func TestNew(t *testing.T) {
`range start 127.0.0.1 is not within 192.168.0.1/24`, `range start 127.0.0.1 is not within 192.168.0.1/24`,
}} }}
ctx := testutil.ContextWithTimeout(t, testTimeout)
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { 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) testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
}) })
} }
} }
func TestDHCPServer_AddLease(t *testing.T) { 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, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
}) })
@ -267,8 +283,11 @@ func TestDHCPServer_AddLease(t *testing.T) {
} }
func TestDHCPServer_index(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, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
}) })
@ -342,8 +361,11 @@ func TestDHCPServer_index(t *testing.T) {
} }
func TestDHCPServer_UpdateStaticLease(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, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
}) })
@ -462,8 +484,11 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
} }
func TestDHCPServer_RemoveLease(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, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
}) })
@ -554,8 +579,11 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
} }
func TestDHCPServer_Reset(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, Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
}) })

View file

@ -1,13 +1,15 @@
package dhcpsvc package dhcpsvc
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
"time" "time"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
) )
@ -43,25 +45,133 @@ type IPv4Config struct {
} }
// validate returns an error in conf if any. // validate returns an error in conf if any.
func (conf *IPv4Config) validate() (err error) { func (c *IPv4Config) validate() (err error) {
switch { if c == nil {
case conf == nil:
return errNilConfig return errNilConfig
case !conf.Enabled: } else if !c.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:
return nil 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 // options returns the implicit and explicit options for the interface. The two
@ -69,14 +179,14 @@ func (conf *IPv4Config) validate() (err error) {
// values. // values.
// //
// TODO(e.burkov): DRY with the IPv6 version. // 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 // Set default values of host configuration parameters listed in Appendix A
// of RFC-2131. // of RFC-2131.
implicit = layers.DHCPOptions{ imp = layers.DHCPOptions{
// Values From Configuration // Values From Configuration
layers.NewDHCPOption(layers.DHCPOptSubnetMask, conf.SubnetMask.AsSlice()), layers.NewDHCPOption(layers.DHCPOptSubnetMask, c.SubnetMask.AsSlice()),
layers.NewDHCPOption(layers.DHCPOptRouter, conf.GatewayIP.AsSlice()), layers.NewDHCPOption(layers.DHCPOptRouter, c.GatewayIP.AsSlice()),
// IP-Layer Per Host // 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. // See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.
layers.NewDHCPOption(layers.DHCPOptTCPKeepAliveGarbage, []byte{0x1}), layers.NewDHCPOption(layers.DHCPOptTCPKeepAliveGarbage, []byte{0x1}),
} }
slices.SortFunc(implicit, compareV4OptionCodes) slices.SortFunc(imp, compareV4OptionCodes)
// Set values for explicitly configured options. // Set values for explicitly configured options.
for _, exp := range conf.Options { for _, e := range c.Options {
i, found := slices.BinarySearchFunc(implicit, exp, compareV4OptionCodes) i, found := slices.BinarySearchFunc(imp, e, compareV4OptionCodes)
if found { if found {
implicit = slices.Delete(implicit, i, i+1) imp = slices.Delete(imp, i, i+1)
} }
i, found = slices.BinarySearchFunc(explicit, exp, compareV4OptionCodes) i, found = slices.BinarySearchFunc(exp, e, compareV4OptionCodes)
if exp.Length > 0 { if e.Length > 0 {
explicit = slices.Insert(explicit, i, exp) exp = slices.Insert(exp, i, e)
} else if found { } 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) l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp)
log.Debug("dhcpsvc: v4: explicit options: %s", explicit)
return implicit, explicit return imp, exp
} }
// compareV4OptionCodes compares option codes of a and b. // compareV4OptionCodes compares option codes of a and b.
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) { func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
return int(a.Type) - int(b.Type) 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
}

View file

@ -3,7 +3,10 @@ package dhcpsvc
import ( import (
"net/netip" "net/netip"
"testing" "testing"
"time"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -75,9 +78,12 @@ func TestIPv4Config_Options(t *testing.T) {
wantExplicit: layers.DHCPOptions{opt1}, wantExplicit: layers.DHCPOptions{opt1},
}} }}
ctx := testutil.ContextWithTimeout(t, time.Second)
l := slogutil.NewDiscardLogger()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { 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) assert.Equal(t, tc.wantExplicit, exp)
for c := range exp { for c := range exp {

View file

@ -1,12 +1,14 @@
package dhcpsvc package dhcpsvc
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"slices" "slices"
"time" "time"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
) )
@ -38,19 +40,26 @@ type IPv6Config struct {
} }
// validate returns an error in conf if any. // validate returns an error in conf if any.
func (conf *IPv6Config) validate() (err error) { func (c *IPv6Config) validate() (err error) {
switch { if c == nil {
case conf == nil:
return errNilConfig return errNilConfig
case !conf.Enabled: } else if !c.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:
return nil 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 // options returns the implicit and explicit options for the interface. The two
@ -58,25 +67,24 @@ func (conf *IPv6Config) validate() (err error) {
// values. // values.
// //
// TODO(e.burkov): Add implicit options according to RFC. // 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. // Set default values of host configuration parameters listed in RFC 8415.
implicit = layers.DHCPv6Options{} imp = layers.DHCPv6Options{}
slices.SortFunc(implicit, compareV6OptionCodes) slices.SortFunc(imp, compareV6OptionCodes)
// Set values for explicitly configured options. // Set values for explicitly configured options.
for _, exp := range conf.Options { for _, e := range c.Options {
i, found := slices.BinarySearchFunc(implicit, exp, compareV6OptionCodes) i, found := slices.BinarySearchFunc(imp, e, compareV6OptionCodes)
if found { 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) l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp)
log.Debug("dhcpsvc: v6: explicit options: %s", explicit)
return implicit, explicit return imp, exp
} }
// compareV6OptionCodes compares option codes of a and b. // compareV6OptionCodes compares option codes of a and b.
@ -116,8 +124,16 @@ type netInterfaceV6 struct {
// the given configuration. // the given configuration.
// //
// TODO(e.burkov): Validate properly. // 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 { if !conf.Enabled {
l.DebugContext(ctx, "disabled")
return nil return nil
} }
@ -130,7 +146,7 @@ func newNetInterfaceV6(name string, conf *IPv6Config) (i *netInterfaceV6) {
raSLAACOnly: conf.RASLAACOnly, raSLAACOnly: conf.RASLAACOnly,
raAllowSLAAC: conf.RAAllowSLAAC, raAllowSLAAC: conf.RAAllowSLAAC,
} }
i.implicitOpts, i.explicitOpts = conf.options() i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
return i return i
} }