Pull request 2130: 4923 gopacket dhcp vol.6
Updates #4923.
Squashed commit of the following:
commit 14ae8dc3680eae7d3ecb9e37a44c2e68221c5085
Merge: 280a4dbc7 713901c2a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Wed Jan 31 13:52:52 2024 +0300
Merge branch 'master' into 4923-gopacket-dhcp-vol.6
commit 280a4dbc728ff67c7659f91734a74c87bf0bda43
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Jan 30 20:20:04 2024 +0300
dhcpsvc: imp docs
commit 310ed67b9bf22f88c4414095bfbfc1a29c6db6d5
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Jan 30 18:51:49 2024 +0300
dhcpsvc: generalize
commit e4c2cae73a729be4db244d3042d93fcc9742bb34
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Jan 30 12:37:38 2024 +0300
dhcpsvc: imp code
commit 9a60d3529293ce1f0e8da70da05958f81e1d0092
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Fri Jan 26 16:28:04 2024 +0300
dhcpsvc: imp code
commit 120c0472f3a3df2ebc0495a40936c8f94156db4b
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Thu Jan 25 20:44:09 2024 +0300
dhcpsvc: imp code, names, docs
commit a92f44c75279868d8e07fe7d468278025a245d13
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Wed Jan 24 16:01:35 2024 +0300
dhcpsvc: imp code, docs
commit 18b3f237b7523f649b49563e852c298fe02fa8ae
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Thu Jan 18 15:29:36 2024 +0300
dhcpsvc: add some lease-related methods
This commit is contained in:
parent
713901c2a0
commit
aa872dfe98
|
@ -19,6 +19,8 @@ type Config struct {
|
||||||
// clients' hostnames.
|
// clients' hostnames.
|
||||||
LocalDomainName string
|
LocalDomainName string
|
||||||
|
|
||||||
|
// TODO(e.burkov): Add DB path.
|
||||||
|
|
||||||
// ICMPTimeout is the timeout for checking another DHCP server's presence.
|
// ICMPTimeout is the timeout for checking another DHCP server's presence.
|
||||||
ICMPTimeout time.Duration
|
ICMPTimeout time.Duration
|
||||||
|
|
||||||
|
@ -68,12 +70,6 @@ func (conf *Config) Validate() (err error) {
|
||||||
return nil
|
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.
|
// validate returns an error in ic, if any.
|
||||||
func (ic *InterfaceConfig) validate() (err error) {
|
func (ic *InterfaceConfig) validate() (err error) {
|
||||||
if ic == nil {
|
if ic == nil {
|
||||||
|
|
|
@ -7,48 +7,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"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
|
// TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate
|
||||||
// [websvc].
|
// interface. This is also valid for Enabled 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
agh.ServiceWithConfig[*Config]
|
agh.ServiceWithConfig[*Config]
|
||||||
|
|
||||||
|
@ -71,7 +37,8 @@ type Interface interface {
|
||||||
// hostname, either set or generated.
|
// hostname, either set or generated.
|
||||||
IPByHost(host string) (ip netip.Addr)
|
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
|
// TODO(e.burkov): Consider implementing iterating methods with appropriate
|
||||||
// signatures instead of cloning the whole list.
|
// signatures instead of cloning the whole list.
|
||||||
|
@ -91,6 +58,8 @@ type Interface interface {
|
||||||
RemoveLease(l *Lease) (err error)
|
RemoveLease(l *Lease) (err error)
|
||||||
|
|
||||||
// Reset removes all the DHCP leases.
|
// Reset removes all the DHCP leases.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): If it's really needed?
|
||||||
Reset() (err error)
|
Reset() (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package dhcpsvc
|
package dhcpsvc
|
||||||
|
|
||||||
import "github.com/AdguardTeam/golibs/errors"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// errNilConfig is returned when a nil config met.
|
// errNilConfig is returned when a nil config met.
|
||||||
|
@ -9,3 +13,9 @@ const (
|
||||||
// errNoInterfaces is returned when no interfaces found in configuration.
|
// errNoInterfaces is returned when no interfaces found in configuration.
|
||||||
errNoInterfaces errors.Error = "no interfaces specified"
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -2,6 +2,10 @@ package dhcpsvc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,18 +19,27 @@ type DHCPServer struct {
|
||||||
// information about its clients.
|
// information about its clients.
|
||||||
enabled *atomic.Bool
|
enabled *atomic.Bool
|
||||||
|
|
||||||
// localTLD is the top-level domain name to use for resolving DHCP
|
// localTLD is the top-level domain name to use for resolving DHCP clients'
|
||||||
// clients' hostnames.
|
// hostnames.
|
||||||
localTLD string
|
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 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 is the set of IPv6 interfaces sorted by interface name.
|
||||||
interfaces6 []*iface6
|
interfaces6 netInterfacesV6
|
||||||
|
|
||||||
// leases is the set of active DHCP leases.
|
|
||||||
leases []*Lease
|
|
||||||
|
|
||||||
// icmpTimeout is the timeout for checking another DHCP server's presence.
|
// icmpTimeout is the timeout for checking another DHCP server's presence.
|
||||||
icmpTimeout time.Duration
|
icmpTimeout time.Duration
|
||||||
|
@ -42,26 +55,27 @@ func New(conf *Config) (srv *DHCPServer, err error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ifaces4 := make([]*iface4, len(conf.Interfaces))
|
// TODO(e.burkov): Add validations scoped to the network interfaces set.
|
||||||
ifaces6 := make([]*iface6, len(conf.Interfaces))
|
ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces))
|
||||||
|
ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces))
|
||||||
|
|
||||||
ifaceNames := maps.Keys(conf.Interfaces)
|
ifaceNames := maps.Keys(conf.Interfaces)
|
||||||
slices.Sort(ifaceNames)
|
slices.Sort(ifaceNames)
|
||||||
|
|
||||||
var i4 *iface4
|
var i4 *netInterfaceV4
|
||||||
var i6 *iface6
|
var i6 *netInterfaceV6
|
||||||
|
|
||||||
for _, ifaceName := range ifaceNames {
|
for _, ifaceName := range ifaceNames {
|
||||||
iface := conf.Interfaces[ifaceName]
|
iface := conf.Interfaces[ifaceName]
|
||||||
|
|
||||||
i4, err = newIface4(ifaceName, iface.IPv4)
|
i4, err = newNetInterfaceV4(ifaceName, iface.IPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err)
|
return nil, 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 = newIface6(ifaceName, iface.IPv6)
|
i6 = newNetInterfaceV6(ifaceName, iface.IPv6)
|
||||||
if i6 != nil {
|
if i6 != nil {
|
||||||
ifaces6 = append(ifaces6, i6)
|
ifaces6 = append(ifaces6, i6)
|
||||||
}
|
}
|
||||||
|
@ -70,13 +84,20 @@ func New(conf *Config) (srv *DHCPServer, err error) {
|
||||||
enabled := &atomic.Bool{}
|
enabled := &atomic.Bool{}
|
||||||
enabled.Store(conf.Enabled)
|
enabled.Store(conf.Enabled)
|
||||||
|
|
||||||
return &DHCPServer{
|
srv = &DHCPServer{
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
|
localTLD: conf.LocalDomainName,
|
||||||
|
leasesMu: &sync.RWMutex{},
|
||||||
|
leaseByIP: map[netip.Addr]*Lease{},
|
||||||
|
leaseByName: map[string]*Lease{},
|
||||||
interfaces4: ifaces4,
|
interfaces4: ifaces4,
|
||||||
interfaces6: ifaces6,
|
interfaces6: ifaces6,
|
||||||
localTLD: conf.LocalDomainName,
|
|
||||||
icmpTimeout: conf.ICMPTimeout,
|
icmpTimeout: conf.ICMPTimeout,
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
// TODO(e.burkov): Load leases.
|
||||||
|
|
||||||
|
return srv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
|
@ -91,10 +112,100 @@ func (srv *DHCPServer) Enabled() (ok bool) {
|
||||||
|
|
||||||
// Leases implements the [Interface] interface for *DHCPServer.
|
// Leases implements the [Interface] interface for *DHCPServer.
|
||||||
func (srv *DHCPServer) Leases() (leases []*Lease) {
|
func (srv *DHCPServer) Leases() (leases []*Lease) {
|
||||||
leases = make([]*Lease, 0, len(srv.leases))
|
srv.leasesMu.RLock()
|
||||||
for _, lease := range srv.leases {
|
defer srv.leasesMu.RUnlock()
|
||||||
|
|
||||||
|
for _, iface := range srv.interfaces4 {
|
||||||
|
for _, lease := range iface.leases {
|
||||||
leases = append(leases, lease.Clone())
|
leases = append(leases, lease.Clone())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return leases
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package dhcpsvc_test
|
package dhcpsvc_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testLocalTLD is a common local TLD for tests.
|
// 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{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
// options returns the implicit and explicit options for the interface. The two
|
||||||
// lists are disjoint and the implicit options are initialized with default
|
// lists are disjoint and the implicit options are initialized with default
|
||||||
// values.
|
// values.
|
||||||
|
@ -318,3 +255,83 @@ func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) {
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"golang.org/x/exp/slices"
|
"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
|
// options returns the implicit and explicit options for the interface. The two
|
||||||
// lists are disjoint and the implicit options are initialized with default
|
// lists are disjoint and the implicit options are initialized with default
|
||||||
// values.
|
// values.
|
||||||
|
@ -133,3 +83,79 @@ func (conf *IPv6Config) options() (implicit, explicit layers.DHCPv6Options) {
|
||||||
func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) {
|
func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) {
|
||||||
return int(a.Code) - int(b.Code)
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue