diff --git a/internal/dhcpsvc/config.go b/internal/dhcpsvc/config.go index 52d28a33..d1683be8 100644 --- a/internal/dhcpsvc/config.go +++ b/internal/dhcpsvc/config.go @@ -19,6 +19,8 @@ type Config struct { // clients' hostnames. LocalDomainName string + // TODO(e.burkov): Add DB path. + // ICMPTimeout is the timeout for checking another DHCP server's presence. ICMPTimeout time.Duration @@ -68,12 +70,6 @@ func (conf *Config) Validate() (err error) { return nil } -// newMustErr returns an error that indicates that valName must be as must -// describes. -func newMustErr(valName, must string, val fmt.Stringer) (err error) { - return fmt.Errorf("%s %s must %s", valName, val, must) -} - // validate returns an error in ic, if any. func (ic *InterfaceConfig) validate() (err error) { if ic == nil { diff --git a/internal/dhcpsvc/dhcpsvc.go b/internal/dhcpsvc/dhcpsvc.go index efa7404f..8ab2cab7 100644 --- a/internal/dhcpsvc/dhcpsvc.go +++ b/internal/dhcpsvc/dhcpsvc.go @@ -7,48 +7,14 @@ import ( "context" "net" "net/netip" - "time" "github.com/AdguardTeam/AdGuardHome/internal/next/agh" - "golang.org/x/exp/slices" ) -// Lease is a DHCP lease. +// Interface is a DHCP service. // -// TODO(e.burkov): Consider moving it to [agh], since it also may be needed in -// [websvc]. -type Lease struct { - // IP is the IP address leased to the client. - IP netip.Addr - - // Expiry is the expiration time of the lease. - Expiry time.Time - - // Hostname of the client. - Hostname string - - // HWAddr is the physical hardware address (MAC address). - HWAddr net.HardwareAddr - - // IsStatic defines if the lease is static. - IsStatic bool -} - -// Clone returns a deep copy of l. -func (l *Lease) Clone() (clone *Lease) { - if l == nil { - return nil - } - - return &Lease{ - Expiry: l.Expiry, - Hostname: l.Hostname, - HWAddr: slices.Clone(l.HWAddr), - IP: l.IP, - IsStatic: l.IsStatic, - } -} - +// TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate +// interface. This is also valid for Enabled method. type Interface interface { agh.ServiceWithConfig[*Config] @@ -71,7 +37,8 @@ type Interface interface { // hostname, either set or generated. IPByHost(host string) (ip netip.Addr) - // Leases returns all the active DHCP leases. + // Leases returns all the active DHCP leases. The returned slice should be + // a clone. // // TODO(e.burkov): Consider implementing iterating methods with appropriate // signatures instead of cloning the whole list. @@ -91,6 +58,8 @@ type Interface interface { RemoveLease(l *Lease) (err error) // Reset removes all the DHCP leases. + // + // TODO(e.burkov): If it's really needed? Reset() (err error) } diff --git a/internal/dhcpsvc/errors.go b/internal/dhcpsvc/errors.go index a7cc8931..fdb97b76 100644 --- a/internal/dhcpsvc/errors.go +++ b/internal/dhcpsvc/errors.go @@ -1,6 +1,10 @@ package dhcpsvc -import "github.com/AdguardTeam/golibs/errors" +import ( + "fmt" + + "github.com/AdguardTeam/golibs/errors" +) const ( // errNilConfig is returned when a nil config met. @@ -9,3 +13,9 @@ const ( // errNoInterfaces is returned when no interfaces found in configuration. errNoInterfaces errors.Error = "no interfaces specified" ) + +// newMustErr returns an error that indicates that valName must be as must +// describes. +func newMustErr(valName, must string, val fmt.Stringer) (err error) { + return fmt.Errorf("%s %s must %s", valName, val, must) +} diff --git a/internal/dhcpsvc/interface.go b/internal/dhcpsvc/interface.go new file mode 100644 index 00000000..56b52382 --- /dev/null +++ b/internal/dhcpsvc/interface.go @@ -0,0 +1,41 @@ +package dhcpsvc + +import ( + "fmt" + "time" + + "golang.org/x/exp/slices" +) + +// netInterface is a common part of any network interface within the DHCP +// server. +// +// TODO(e.burkov): Add other methods as [DHCPServer] evolves. +type netInterface struct { + // name is the name of the network interface. + name string + + // leases is a set of leases sorted by hardware address. + leases []*Lease + + // leaseTTL is the default Time-To-Live value for leases. + leaseTTL time.Duration +} + +// reset clears all the slices in iface for reuse. +func (iface *netInterface) reset() { + iface.leases = iface.leases[:0] +} + +// insertLease inserts the given lease into iface. It returns an error if the +// lease can't be inserted. +func (iface *netInterface) insertLease(l *Lease) (err error) { + i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) + if found { + return fmt.Errorf("lease for mac %s already exists", l.HWAddr) + } + + iface.leases = slices.Insert(iface.leases, i, l) + + return nil +} diff --git a/internal/dhcpsvc/lease.go b/internal/dhcpsvc/lease.go new file mode 100644 index 00000000..6f7006a1 --- /dev/null +++ b/internal/dhcpsvc/lease.go @@ -0,0 +1,53 @@ +package dhcpsvc + +import ( + "bytes" + "net" + "net/netip" + "time" + + "golang.org/x/exp/slices" +) + +// Lease is a DHCP lease. +// +// TODO(e.burkov): Consider moving it to [agh], since it also may be needed in +// [websvc]. +// +// TODO(e.burkov): Add validation method. +type Lease struct { + // IP is the IP address leased to the client. + IP netip.Addr + + // Expiry is the expiration time of the lease. + Expiry time.Time + + // Hostname of the client. + Hostname string + + // HWAddr is the physical hardware address (MAC address). + HWAddr net.HardwareAddr + + // IsStatic defines if the lease is static. + IsStatic bool +} + +// Clone returns a deep copy of l. +func (l *Lease) Clone() (clone *Lease) { + if l == nil { + return nil + } + + return &Lease{ + Expiry: l.Expiry, + Hostname: l.Hostname, + HWAddr: slices.Clone(l.HWAddr), + IP: l.IP, + IsStatic: l.IsStatic, + } +} + +// compareLeaseMAC compares two [Lease]s by hardware address. +func compareLeaseMAC(a, b *Lease) (res int) { + return bytes.Compare(a.HWAddr, b.HWAddr) +} diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index 2d24ef27..d4dbeb9f 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -2,6 +2,10 @@ package dhcpsvc import ( "fmt" + "net" + "net/netip" + "strings" + "sync" "sync/atomic" "time" @@ -15,18 +19,27 @@ type DHCPServer struct { // information about its clients. enabled *atomic.Bool - // localTLD is the top-level domain name to use for resolving DHCP - // clients' hostnames. + // localTLD is the top-level domain name to use for resolving DHCP clients' + // hostnames. localTLD string + // leasesMu protects the ipIndex and nameIndex fields against concurrent + // access, as well as leaseHandlers within the interfaces. + leasesMu *sync.RWMutex + + // leaseByIP is a lookup shortcut for leases by their IP addresses. + leaseByIP map[netip.Addr]*Lease + + // leaseByName is a lookup shortcut for leases by their hostnames. + // + // TODO(e.burkov): Use a slice of leases with the same hostname? + leaseByName map[string]*Lease + // interfaces4 is the set of IPv4 interfaces sorted by interface name. - interfaces4 []*iface4 + interfaces4 netInterfacesV4 // interfaces6 is the set of IPv6 interfaces sorted by interface name. - interfaces6 []*iface6 - - // leases is the set of active DHCP leases. - leases []*Lease + interfaces6 netInterfacesV6 // icmpTimeout is the timeout for checking another DHCP server's presence. icmpTimeout time.Duration @@ -42,26 +55,27 @@ func New(conf *Config) (srv *DHCPServer, err error) { return nil, nil } - ifaces4 := make([]*iface4, len(conf.Interfaces)) - ifaces6 := make([]*iface6, len(conf.Interfaces)) + // 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 *iface4 - var i6 *iface6 + var i4 *netInterfaceV4 + var i6 *netInterfaceV6 for _, ifaceName := range ifaceNames { iface := conf.Interfaces[ifaceName] - i4, err = newIface4(ifaceName, iface.IPv4) + i4, err = newNetInterfaceV4(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) + i6 = newNetInterfaceV6(ifaceName, iface.IPv6) if i6 != nil { ifaces6 = append(ifaces6, i6) } @@ -70,13 +84,20 @@ func New(conf *Config) (srv *DHCPServer, err error) { enabled := &atomic.Bool{} enabled.Store(conf.Enabled) - return &DHCPServer{ + srv = &DHCPServer{ enabled: enabled, + localTLD: conf.LocalDomainName, + leasesMu: &sync.RWMutex{}, + leaseByIP: map[netip.Addr]*Lease{}, + leaseByName: map[string]*Lease{}, interfaces4: ifaces4, interfaces6: ifaces6, - localTLD: conf.LocalDomainName, icmpTimeout: conf.ICMPTimeout, - }, nil + } + + // TODO(e.burkov): Load leases. + + return srv, nil } // type check @@ -91,10 +112,100 @@ func (srv *DHCPServer) Enabled() (ok bool) { // Leases implements the [Interface] interface for *DHCPServer. func (srv *DHCPServer) Leases() (leases []*Lease) { - leases = make([]*Lease, 0, len(srv.leases)) - for _, lease := range srv.leases { - leases = append(leases, lease.Clone()) + srv.leasesMu.RLock() + defer srv.leasesMu.RUnlock() + + for _, iface := range srv.interfaces4 { + for _, lease := range iface.leases { + leases = append(leases, lease.Clone()) + } } return leases } + +// HostByIP implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) HostByIP(ip netip.Addr) (host string) { + srv.leasesMu.RLock() + defer srv.leasesMu.RUnlock() + + if l, ok := srv.leaseByIP[ip]; ok { + return l.Hostname + } + + return "" +} + +// MACByIP implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { + srv.leasesMu.RLock() + defer srv.leasesMu.RUnlock() + + if l, ok := srv.leaseByIP[ip]; ok { + return l.HWAddr + } + + return nil +} + +// IPByHost implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) { + lowered := strings.ToLower(host) + + srv.leasesMu.RLock() + defer srv.leasesMu.RUnlock() + + if l, ok := srv.leaseByName[lowered]; ok { + return l.IP + } + + return netip.Addr{} +} + +// Reset implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) Reset() (err error) { + srv.leasesMu.Lock() + defer srv.leasesMu.Unlock() + + for _, iface := range srv.interfaces4 { + iface.reset() + } + for _, iface := range srv.interfaces6 { + iface.reset() + } + + maps.Clear(srv.leaseByIP) + maps.Clear(srv.leaseByName) + + return nil +} + +// AddLease implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) AddLease(l *Lease) (err error) { + var ok bool + var iface *netInterface + + addr := l.IP + + if addr.Is4() { + iface, ok = srv.interfaces4.find(addr) + } else { + iface, ok = srv.interfaces6.find(addr) + } + if !ok { + return fmt.Errorf("no interface for IP address %s", addr) + } + + srv.leasesMu.Lock() + defer srv.leasesMu.Unlock() + + err = iface.insertLease(l) + if err != nil { + return err + } + + srv.leaseByIP[l.IP] = l + srv.leaseByName[strings.ToLower(l.Hostname)] = l + + return nil +} diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go index 6475dfa4..3db16000 100644 --- a/internal/dhcpsvc/server_test.go +++ b/internal/dhcpsvc/server_test.go @@ -1,12 +1,15 @@ package dhcpsvc_test import ( + "net" "net/netip" "testing" "time" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // testLocalTLD is a common local TLD for tests. @@ -113,3 +116,113 @@ func TestNew(t *testing.T) { }) } } + +func TestDHCPServer_index(t *testing.T) { + srv, err := dhcpsvc.New(&dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: &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, + }, + IPv6: &dhcpsvc.IPv6Config{ + Enabled: true, + RangeStart: netip.MustParseAddr("2001:db8::1"), + LeaseDuration: 1 * time.Hour, + RAAllowSLAAC: true, + RASLAACOnly: true, + }, + }, + "eth1": { + IPv4: &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("172.16.0.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("172.16.0.2"), + RangeEnd: netip.MustParseAddr("172.16.0.255"), + LeaseDuration: 1 * time.Hour, + }, + IPv6: &dhcpsvc.IPv6Config{ + Enabled: true, + RangeStart: netip.MustParseAddr("2001:db9::1"), + LeaseDuration: 1 * time.Hour, + RAAllowSLAAC: true, + RASLAACOnly: true, + }, + }, + }, + }) + require.NoError(t, err) + + const ( + host1 = "host1" + host2 = "host2" + host3 = "host3" + host4 = "host4" + host5 = "host5" + ) + + ip1 := netip.MustParseAddr("192.168.0.2") + ip2 := netip.MustParseAddr("192.168.0.3") + ip3 := netip.MustParseAddr("172.16.0.3") + ip4 := netip.MustParseAddr("172.16.0.4") + + mac1 := net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06} + mac2 := net.HardwareAddr{0x06, 0x05, 0x04, 0x03, 0x02, 0x01} + mac3 := net.HardwareAddr{0x05, 0x04, 0x03, 0x02, 0x01, 0x00} + + leases := []*dhcpsvc.Lease{{ + Hostname: host1, + IP: ip1, + HWAddr: mac1, + IsStatic: true, + }, { + Hostname: host2, + IP: ip2, + HWAddr: mac2, + IsStatic: true, + }, { + Hostname: host3, + IP: ip3, + HWAddr: mac3, + IsStatic: true, + }, { + Hostname: host4, + IP: ip4, + HWAddr: mac1, + IsStatic: true, + }} + for _, l := range leases { + require.NoError(t, srv.AddLease(l)) + } + + t.Run("ip_idx", func(t *testing.T) { + assert.Equal(t, ip1, srv.IPByHost(host1)) + assert.Equal(t, ip2, srv.IPByHost(host2)) + assert.Equal(t, ip3, srv.IPByHost(host3)) + assert.Equal(t, ip4, srv.IPByHost(host4)) + assert.Equal(t, netip.Addr{}, srv.IPByHost(host5)) + }) + + t.Run("name_idx", func(t *testing.T) { + assert.Equal(t, host1, srv.HostByIP(ip1)) + assert.Equal(t, host2, srv.HostByIP(ip2)) + assert.Equal(t, host3, srv.HostByIP(ip3)) + assert.Equal(t, host4, srv.HostByIP(ip4)) + assert.Equal(t, "", srv.HostByIP(netip.Addr{})) + }) + + t.Run("mac_idx", func(t *testing.T) { + assert.Equal(t, mac1, srv.MACByIP(ip1)) + assert.Equal(t, mac2, srv.MACByIP(ip2)) + assert.Equal(t, mac3, srv.MACByIP(ip3)) + assert.Equal(t, mac1, srv.MACByIP(ip4)) + assert.Nil(t, srv.MACByIP(netip.Addr{})) + }) +} diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index e74c06b3..e28e3845 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -64,69 +64,6 @@ func (conf *IPv4Config) validate() (err error) { } } -// 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. - subnet netip.Prefix - - // addrSpace is the IPv4 address space allocated for leasing. - addrSpace ipRange - - // name is the name of the interface. - name string - - // 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 - - // 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) - } - - i = &iface4{ - name: name, - gateway: conf.GatewayIP, - subnet: subnet, - addrSpace: addrSpace, - leaseTTL: conf.LeaseDuration, - } - i.implicitOpts, i.explicitOpts = conf.options() - - return i, nil -} - // options returns the implicit and explicit options for the interface. The two // lists are disjoint and the implicit options are initialized with default // values. @@ -318,3 +255,83 @@ func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) { 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/v6.go b/internal/dhcpsvc/v6.go index 2dc832b6..9387c3be 100644 --- a/internal/dhcpsvc/v6.go +++ b/internal/dhcpsvc/v6.go @@ -6,6 +6,7 @@ import ( "time" "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/netutil" "github.com/google/gopacket/layers" "golang.org/x/exp/slices" ) @@ -52,57 +53,6 @@ func (conf *IPv6Config) validate() (err error) { } } -// 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 - - // implicitOpts are the DHCPv6 options listed in RFC 8415 (and others) and - // initialized with default values. It must not have intersections with - // explicitOpts. - implicitOpts layers.DHCPv6Options - - // explicitOpts are the user-configured options. It must not have - // intersections with implicitOpts. - explicitOpts layers.DHCPv6Options - - // 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 - } - - i = &iface6{ - name: name, - rangeStart: conf.RangeStart, - leaseTTL: conf.LeaseDuration, - raSLAACOnly: conf.RASLAACOnly, - raAllowSLAAC: conf.RAAllowSLAAC, - } - i.implicitOpts, i.explicitOpts = conf.options() - - return i -} - // options returns the implicit and explicit options for the interface. The two // lists are disjoint and the implicit options are initialized with default // values. @@ -133,3 +83,79 @@ func (conf *IPv6Config) options() (implicit, explicit layers.DHCPv6Options) { 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. +type netInterfaceV6 struct { + // rangeStart is the first IP address in the range. + rangeStart netip.Addr + + // implicitOpts are the DHCPv6 options listed in RFC 8415 (and others) and + // initialized with default values. It must not have intersections with + // explicitOpts. + implicitOpts layers.DHCPv6Options + + // explicitOpts are the user-configured options. It must not have + // intersections with implicitOpts. + explicitOpts layers.DHCPv6Options + + // netInterface is embedded here to provide some common network interface + // logic. + netInterface + + // 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 +} + +// newNetInterfaceV6 creates a new DHCP interface for IPv6 address family with +// the given configuration. +// +// TODO(e.burkov): Validate properly. +func newNetInterfaceV6(name string, conf *IPv6Config) (i *netInterfaceV6) { + if !conf.Enabled { + return nil + } + + i = &netInterfaceV6{ + rangeStart: conf.RangeStart, + netInterface: netInterface{ + name: name, + leaseTTL: conf.LeaseDuration, + }, + raSLAACOnly: conf.RASLAACOnly, + raAllowSLAAC: conf.RAAllowSLAAC, + } + i.implicitOpts, i.explicitOpts = conf.options() + + return i +} + +// netInterfacesV4 is a slice of network interfaces of IPv4 address family. +type netInterfacesV6 []*netInterfaceV6 + +// find returns the first network interface within ifaces containing ip. It +// returns false if there is no such interface. +func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) { + // prefLen is the length of prefix to match ip against. + // + // TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy + // implementation where the allocated range constrained by the first address + // and the first address with last byte set to 0xff. Proper prefixes should + // be used instead. + const prefLen = netutil.IPv6BitLen - 8 + + i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) { + return !iface.rangeStart.Less(ip) && + netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip) + }) + if i < 0 { + return nil, false + } + + return &ifaces[i].netInterface, true +}