mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-21 20:45:33 +03:00
Pull request 2152: 4923 gopacket DHCP vol.7
Updates #4923.
Squashed commit of the following:
commit 0f90eb3596fcbca0d87cb4eb857d45aea26f3854
Merge: 38b3165b6 bd99e3e09
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Mon Feb 19 20:11:38 2024 +0300
Merge branch 'master' into 4923-gopacket-dhcp-vol.7
commit 38b3165b696c9a3f69484d8e4c2c847340ed9363
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Mon Feb 19 14:52:01 2024 +0300
dhcpsvc: imp docs
commit 0a078920a20de9fb2d864c90d2311800c6f3bc3f
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Mon Feb 19 14:48:19 2024 +0300
dhcpsvc: imp code
commit 30691f0d989c48b2f0dff8079952615dbfbdaea1
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Thu Feb 15 19:57:41 2024 +0300
dhcpsvc: imp code, dry
commit 20f5ef80fb2d1cad869883da3684a01e5b8b3315
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Thu Feb 15 15:57:09 2024 +0300
dhcpsvc: finish leases methods
This commit is contained in:
parent
bd99e3e09d
commit
6fd0a624ca
6 changed files with 639 additions and 83 deletions
|
@ -14,7 +14,9 @@ import (
|
||||||
// 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
|
||||||
// interface. This is also valid for Enabled method.
|
// interface. This is also applicable to Enabled method.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Reconsider the requirements for the leases validity.
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
agh.ServiceWithConfig[*Config]
|
agh.ServiceWithConfig[*Config]
|
||||||
|
|
||||||
|
@ -29,6 +31,8 @@ type Interface interface {
|
||||||
// MACByIP returns the MAC address for the given IP address leased. It
|
// MACByIP returns the MAC address for the given IP address leased. It
|
||||||
// returns nil if there is no such client, due to an assumption that a DHCP
|
// returns nil if there is no such client, due to an assumption that a DHCP
|
||||||
// client must always have a MAC address.
|
// client must always have a MAC address.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Think of a contract for the returned value.
|
||||||
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
|
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
|
||||||
|
|
||||||
// IPByHost returns the IP address of the DHCP client with the given
|
// IPByHost returns the IP address of the DHCP client with the given
|
||||||
|
@ -44,17 +48,17 @@ type Interface interface {
|
||||||
// signatures instead of cloning the whole list.
|
// signatures instead of cloning the whole list.
|
||||||
Leases() (ls []*Lease)
|
Leases() (ls []*Lease)
|
||||||
|
|
||||||
// AddLease adds a new DHCP lease. It returns an error if the lease is
|
// AddLease adds a new DHCP lease. l must be valid. It returns an error if
|
||||||
// invalid or already exists.
|
// l already exists.
|
||||||
AddLease(l *Lease) (err error)
|
AddLease(l *Lease) (err error)
|
||||||
|
|
||||||
// UpdateStaticLease changes an existing DHCP lease. It returns an error if
|
// UpdateStaticLease replaces an existing static DHCP lease. l must be
|
||||||
// there is no lease with such hardware addressor if new values are invalid
|
// valid. It returns an error if the lease with the given hardware address
|
||||||
// or already exist.
|
// doesn't exist or if other values match another existing lease.
|
||||||
UpdateStaticLease(l *Lease) (err error)
|
UpdateStaticLease(l *Lease) (err error)
|
||||||
|
|
||||||
// RemoveLease removes an existing DHCP lease. It returns an error if there
|
// RemoveLease removes an existing DHCP lease. l must be valid. It returns
|
||||||
// is no lease equal to l.
|
// an error if there is no lease equal to l.
|
||||||
RemoveLease(l *Lease) (err error)
|
RemoveLease(l *Lease) (err error)
|
||||||
|
|
||||||
// Reset removes all the DHCP leases.
|
// Reset removes all the DHCP leases.
|
||||||
|
|
|
@ -38,3 +38,29 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateLease replaces an existing lease within iface with the given one. It
|
||||||
|
// returns an error if there is no lease with such hardware address.
|
||||||
|
func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
|
||||||
|
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("no lease for mac %s", l.HWAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
prev, iface.leases[i] = iface.leases[i], l
|
||||||
|
|
||||||
|
return prev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeLease removes an existing lease from iface. It returns an error if
|
||||||
|
// there is no lease equal to l.
|
||||||
|
func (iface *netInterface) removeLease(l *Lease) (err error) {
|
||||||
|
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("no lease for mac %s", l.HWAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
iface.leases = slices.Delete(iface.leases, i, i+1)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
126
internal/dhcpsvc/leaseindex.go
Normal file
126
internal/dhcpsvc/leaseindex.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package dhcpsvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// leaseIndex is the set of leases indexed by their identifiers for quick
|
||||||
|
// lookup.
|
||||||
|
type leaseIndex struct {
|
||||||
|
// byAddr is a lookup shortcut for leases by their IP addresses.
|
||||||
|
byAddr map[netip.Addr]*Lease
|
||||||
|
|
||||||
|
// byName is a lookup shortcut for leases by their hostnames.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use a slice of leases with the same hostname?
|
||||||
|
byName map[string]*Lease
|
||||||
|
}
|
||||||
|
|
||||||
|
// newLeaseIndex returns a new index for [Lease]s.
|
||||||
|
func newLeaseIndex() *leaseIndex {
|
||||||
|
return &leaseIndex{
|
||||||
|
byAddr: map[netip.Addr]*Lease{},
|
||||||
|
byName: map[string]*Lease{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// leaseByAddr returns a lease by its IP address.
|
||||||
|
func (idx *leaseIndex) leaseByAddr(addr netip.Addr) (l *Lease, ok bool) {
|
||||||
|
l, ok = idx.byAddr[addr]
|
||||||
|
|
||||||
|
return l, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// leaseByName returns a lease by its hostname.
|
||||||
|
func (idx *leaseIndex) leaseByName(name string) (l *Lease, ok bool) {
|
||||||
|
// TODO(e.burkov): Probably, use a case-insensitive comparison and store in
|
||||||
|
// slice. This would require a benchmark.
|
||||||
|
l, ok = idx.byName[strings.ToLower(name)]
|
||||||
|
|
||||||
|
return l, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear removes all leases from idx.
|
||||||
|
func (idx *leaseIndex) clear() {
|
||||||
|
clear(idx.byAddr)
|
||||||
|
clear(idx.byName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add adds l into idx and into iface. l must be valid, iface should be
|
||||||
|
// responsible for l's IP. It returns an error if l duplicates at least a
|
||||||
|
// single value of another lease.
|
||||||
|
func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
|
||||||
|
loweredName := strings.ToLower(l.Hostname)
|
||||||
|
|
||||||
|
if _, ok := idx.byAddr[l.IP]; ok {
|
||||||
|
return fmt.Errorf("lease for ip %s already exists", l.IP)
|
||||||
|
} else if _, ok = idx.byName[loweredName]; ok {
|
||||||
|
return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = iface.insertLease(l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
idx.byAddr[l.IP] = l
|
||||||
|
idx.byName[loweredName] = l
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove removes l from idx and from iface. l must be valid, iface should
|
||||||
|
// contain the same lease or the lease itself. It returns an error if the lease
|
||||||
|
// not found.
|
||||||
|
func (idx *leaseIndex) remove(l *Lease, iface *netInterface) (err error) {
|
||||||
|
loweredName := strings.ToLower(l.Hostname)
|
||||||
|
|
||||||
|
if _, ok := idx.byAddr[l.IP]; !ok {
|
||||||
|
return fmt.Errorf("no lease for ip %s", l.IP)
|
||||||
|
} else if _, ok = idx.byName[loweredName]; !ok {
|
||||||
|
return fmt.Errorf("no lease for hostname %s", l.Hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = iface.removeLease(l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(idx.byAddr, l.IP)
|
||||||
|
delete(idx.byName, loweredName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// update updates l in idx and in iface. l must be valid, iface should be
|
||||||
|
// responsible for l's IP. It returns an error if l duplicates at least a
|
||||||
|
// single value of another lease, except for the updated lease itself.
|
||||||
|
func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) {
|
||||||
|
loweredName := strings.ToLower(l.Hostname)
|
||||||
|
|
||||||
|
existing, ok := idx.byAddr[l.IP]
|
||||||
|
if ok && !slices.Equal(l.HWAddr, existing.HWAddr) {
|
||||||
|
return fmt.Errorf("lease for ip %s already exists", l.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, ok = idx.byName[loweredName]
|
||||||
|
if ok && !slices.Equal(l.HWAddr, existing.HWAddr) {
|
||||||
|
return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
prev, err := iface.updateLease(l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(idx.byAddr, prev.IP)
|
||||||
|
delete(idx.byName, strings.ToLower(prev.Hostname))
|
||||||
|
|
||||||
|
idx.byAddr[l.IP] = l
|
||||||
|
idx.byName[loweredName] = l
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -5,11 +5,11 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,17 +23,11 @@ type DHCPServer struct {
|
||||||
// hostnames.
|
// hostnames.
|
||||||
localTLD string
|
localTLD string
|
||||||
|
|
||||||
// leasesMu protects the ipIndex and nameIndex fields against concurrent
|
// leasesMu protects the leases index as well as leases in the interfaces.
|
||||||
// access, as well as leaseHandlers within the interfaces.
|
|
||||||
leasesMu *sync.RWMutex
|
leasesMu *sync.RWMutex
|
||||||
|
|
||||||
// leaseByIP is a lookup shortcut for leases by their IP addresses.
|
// leases stores the DHCP leases for quick lookups.
|
||||||
leaseByIP map[netip.Addr]*Lease
|
leases *leaseIndex
|
||||||
|
|
||||||
// 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 netInterfacesV4
|
interfaces4 netInterfacesV4
|
||||||
|
@ -88,8 +82,7 @@ func New(conf *Config) (srv *DHCPServer, err error) {
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
localTLD: conf.LocalDomainName,
|
localTLD: conf.LocalDomainName,
|
||||||
leasesMu: &sync.RWMutex{},
|
leasesMu: &sync.RWMutex{},
|
||||||
leaseByIP: map[netip.Addr]*Lease{},
|
leases: newLeaseIndex(),
|
||||||
leaseByName: map[string]*Lease{},
|
|
||||||
interfaces4: ifaces4,
|
interfaces4: ifaces4,
|
||||||
interfaces6: ifaces6,
|
interfaces6: ifaces6,
|
||||||
icmpTimeout: conf.ICMPTimeout,
|
icmpTimeout: conf.ICMPTimeout,
|
||||||
|
@ -120,6 +113,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
|
||||||
leases = append(leases, lease.Clone())
|
leases = append(leases, lease.Clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, iface := range srv.interfaces6 {
|
||||||
|
for _, lease := range iface.leases {
|
||||||
|
leases = append(leases, lease.Clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return leases
|
return leases
|
||||||
}
|
}
|
||||||
|
@ -129,7 +127,7 @@ func (srv *DHCPServer) HostByIP(ip netip.Addr) (host string) {
|
||||||
srv.leasesMu.RLock()
|
srv.leasesMu.RLock()
|
||||||
defer srv.leasesMu.RUnlock()
|
defer srv.leasesMu.RUnlock()
|
||||||
|
|
||||||
if l, ok := srv.leaseByIP[ip]; ok {
|
if l, ok := srv.leases.leaseByAddr(ip); ok {
|
||||||
return l.Hostname
|
return l.Hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +139,7 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
|
||||||
srv.leasesMu.RLock()
|
srv.leasesMu.RLock()
|
||||||
defer srv.leasesMu.RUnlock()
|
defer srv.leasesMu.RUnlock()
|
||||||
|
|
||||||
if l, ok := srv.leaseByIP[ip]; ok {
|
if l, ok := srv.leases.leaseByAddr(ip); ok {
|
||||||
return l.HWAddr
|
return l.HWAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,12 +148,10 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
|
||||||
|
|
||||||
// IPByHost implements the [Interface] interface for *DHCPServer.
|
// IPByHost implements the [Interface] interface for *DHCPServer.
|
||||||
func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
|
func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
|
||||||
lowered := strings.ToLower(host)
|
|
||||||
|
|
||||||
srv.leasesMu.RLock()
|
srv.leasesMu.RLock()
|
||||||
defer srv.leasesMu.RUnlock()
|
defer srv.leasesMu.RUnlock()
|
||||||
|
|
||||||
if l, ok := srv.leaseByName[lowered]; ok {
|
if l, ok := srv.leases.leaseByName(host); ok {
|
||||||
return l.IP
|
return l.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,39 +169,76 @@ func (srv *DHCPServer) Reset() (err error) {
|
||||||
for _, iface := range srv.interfaces6 {
|
for _, iface := range srv.interfaces6 {
|
||||||
iface.reset()
|
iface.reset()
|
||||||
}
|
}
|
||||||
|
srv.leases.clear()
|
||||||
clear(srv.leaseByIP)
|
|
||||||
clear(srv.leaseByName)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddLease implements the [Interface] interface for *DHCPServer.
|
// AddLease implements the [Interface] interface for *DHCPServer.
|
||||||
func (srv *DHCPServer) AddLease(l *Lease) (err error) {
|
func (srv *DHCPServer) AddLease(l *Lease) (err error) {
|
||||||
var ok bool
|
defer func() { err = errors.Annotate(err, "adding lease: %w") }()
|
||||||
var iface *netInterface
|
|
||||||
|
|
||||||
addr := l.IP
|
addr := l.IP
|
||||||
|
iface, err := srv.ifaceForAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since there is already an annotation deferred.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.leasesMu.Lock()
|
||||||
|
defer srv.leasesMu.Unlock()
|
||||||
|
|
||||||
|
return srv.leases.add(l, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStaticLease implements the [Interface] interface for *DHCPServer.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Support moving leases between interfaces.
|
||||||
|
func (srv *DHCPServer) UpdateStaticLease(l *Lease) (err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "updating static lease: %w") }()
|
||||||
|
|
||||||
|
addr := l.IP
|
||||||
|
iface, err := srv.ifaceForAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since there is already an annotation deferred.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.leasesMu.Lock()
|
||||||
|
defer srv.leasesMu.Unlock()
|
||||||
|
|
||||||
|
return srv.leases.update(l, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveLease implements the [Interface] interface for *DHCPServer.
|
||||||
|
func (srv *DHCPServer) RemoveLease(l *Lease) (err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "removing lease: %w") }()
|
||||||
|
|
||||||
|
addr := l.IP
|
||||||
|
iface, err := srv.ifaceForAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since there is already an annotation deferred.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.leasesMu.Lock()
|
||||||
|
defer srv.leasesMu.Unlock()
|
||||||
|
|
||||||
|
return srv.leases.remove(l, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ifaceForAddr returns the handled network interface for the given IP address,
|
||||||
|
// or an error if no such interface exists.
|
||||||
|
func (srv *DHCPServer) ifaceForAddr(addr netip.Addr) (iface *netInterface, err error) {
|
||||||
|
var ok bool
|
||||||
if addr.Is4() {
|
if addr.Is4() {
|
||||||
iface, ok = srv.interfaces4.find(addr)
|
iface, ok = srv.interfaces4.find(addr)
|
||||||
} else {
|
} else {
|
||||||
iface, ok = srv.interfaces6.find(addr)
|
iface, ok = srv.interfaces6.find(addr)
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("no interface for IP address %s", addr)
|
return nil, fmt.Errorf("no interface for ip %s", addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.leasesMu.Lock()
|
return iface, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dhcpsvc_test
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,6 +16,52 @@ import (
|
||||||
// testLocalTLD is a common local TLD for tests.
|
// testLocalTLD is a common local TLD for tests.
|
||||||
const testLocalTLD = "local"
|
const testLocalTLD = "local"
|
||||||
|
|
||||||
|
// testInterfaceConf is a common set of interface configurations for tests.
|
||||||
|
var testInterfaceConf = 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustParseMAC parses a hardware address from s and requires no errors.
|
||||||
|
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
|
||||||
|
mac, err := net.ParseMAC(s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return mac
|
||||||
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
validIPv4Conf := &dhcpsvc.IPv4Config{
|
validIPv4Conf := &dhcpsvc.IPv4Config{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
@ -117,46 +164,113 @@ func TestNew(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDHCPServer_AddLease(t *testing.T) {
|
||||||
|
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||||
|
Enabled: true,
|
||||||
|
LocalDomainName: testLocalTLD,
|
||||||
|
Interfaces: testInterfaceConf,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const (
|
||||||
|
host1 = "host1"
|
||||||
|
host2 = "host2"
|
||||||
|
host3 = "host3"
|
||||||
|
)
|
||||||
|
|
||||||
|
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||||
|
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||||
|
ip3 := netip.MustParseAddr("2001:db8::2")
|
||||||
|
|
||||||
|
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
|
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{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac1,
|
||||||
|
IsStatic: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
lease *dhcpsvc.Lease
|
||||||
|
wantErrMsg string
|
||||||
|
}{{
|
||||||
|
name: "outside_range",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host2,
|
||||||
|
IP: netip.MustParseAddr("1.2.3.4"),
|
||||||
|
HWAddr: mac2,
|
||||||
|
},
|
||||||
|
wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
|
||||||
|
}, {
|
||||||
|
name: "duplicate_ip",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host2,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac2,
|
||||||
|
},
|
||||||
|
wantErrMsg: "adding lease: lease for ip " + ip1.String() +
|
||||||
|
" already exists",
|
||||||
|
}, {
|
||||||
|
name: "duplicate_hostname",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip2,
|
||||||
|
HWAddr: mac2,
|
||||||
|
},
|
||||||
|
wantErrMsg: "adding lease: lease for hostname " + host1 +
|
||||||
|
" already exists",
|
||||||
|
}, {
|
||||||
|
name: "duplicate_hostname_case",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: strings.ToUpper(host1),
|
||||||
|
IP: ip2,
|
||||||
|
HWAddr: mac2,
|
||||||
|
},
|
||||||
|
wantErrMsg: "adding lease: lease for hostname " +
|
||||||
|
strings.ToUpper(host1) + " already exists",
|
||||||
|
}, {
|
||||||
|
name: "duplicate_mac",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host2,
|
||||||
|
IP: ip2,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "adding lease: lease for mac " + mac1.String() +
|
||||||
|
" already exists",
|
||||||
|
}, {
|
||||||
|
name: "valid",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host2,
|
||||||
|
IP: ip2,
|
||||||
|
HWAddr: mac2,
|
||||||
|
},
|
||||||
|
wantErrMsg: "",
|
||||||
|
}, {
|
||||||
|
name: "valid_v6",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host3,
|
||||||
|
IP: ip3,
|
||||||
|
HWAddr: mac3,
|
||||||
|
},
|
||||||
|
wantErrMsg: "",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(tc.lease))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDHCPServer_index(t *testing.T) {
|
func TestDHCPServer_index(t *testing.T) {
|
||||||
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
Interfaces: testInterfaceConf,
|
||||||
"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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -173,9 +287,9 @@ func TestDHCPServer_index(t *testing.T) {
|
||||||
ip3 := netip.MustParseAddr("172.16.0.3")
|
ip3 := netip.MustParseAddr("172.16.0.3")
|
||||||
ip4 := netip.MustParseAddr("172.16.0.4")
|
ip4 := netip.MustParseAddr("172.16.0.4")
|
||||||
|
|
||||||
mac1 := net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
|
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
mac2 := net.HardwareAddr{0x06, 0x05, 0x04, 0x03, 0x02, 0x01}
|
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
|
||||||
mac3 := net.HardwareAddr{0x05, 0x04, 0x03, 0x02, 0x01, 0x00}
|
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
|
||||||
|
|
||||||
leases := []*dhcpsvc.Lease{{
|
leases := []*dhcpsvc.Lease{{
|
||||||
Hostname: host1,
|
Hostname: host1,
|
||||||
|
@ -226,3 +340,256 @@ func TestDHCPServer_index(t *testing.T) {
|
||||||
assert.Nil(t, srv.MACByIP(netip.Addr{}))
|
assert.Nil(t, srv.MACByIP(netip.Addr{}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
||||||
|
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||||
|
Enabled: true,
|
||||||
|
LocalDomainName: testLocalTLD,
|
||||||
|
Interfaces: testInterfaceConf,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const (
|
||||||
|
host1 = "host1"
|
||||||
|
host2 = "host2"
|
||||||
|
host3 = "host3"
|
||||||
|
host4 = "host4"
|
||||||
|
host5 = "host5"
|
||||||
|
host6 = "host6"
|
||||||
|
)
|
||||||
|
|
||||||
|
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||||
|
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||||
|
ip3 := netip.MustParseAddr("192.168.0.4")
|
||||||
|
ip4 := netip.MustParseAddr("2001:db8::2")
|
||||||
|
ip5 := netip.MustParseAddr("2001:db8::3")
|
||||||
|
|
||||||
|
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
|
mac2 := mustParseMAC(t, "01:02:03:04:05:07")
|
||||||
|
mac3 := mustParseMAC(t, "06:05:04:03:02:01")
|
||||||
|
mac4 := mustParseMAC(t, "06:05:04:03:02:02")
|
||||||
|
|
||||||
|
leases := []*dhcpsvc.Lease{{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac1,
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Hostname: host2,
|
||||||
|
IP: ip2,
|
||||||
|
HWAddr: mac2,
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Hostname: host4,
|
||||||
|
IP: ip4,
|
||||||
|
HWAddr: mac4,
|
||||||
|
IsStatic: true,
|
||||||
|
}}
|
||||||
|
for _, l := range leases {
|
||||||
|
require.NoError(t, srv.AddLease(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
lease *dhcpsvc.Lease
|
||||||
|
wantErrMsg string
|
||||||
|
}{{
|
||||||
|
name: "outside_range",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: netip.MustParseAddr("1.2.3.4"),
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "updating static lease: no interface for ip 1.2.3.4",
|
||||||
|
}, {
|
||||||
|
name: "not_found",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host3,
|
||||||
|
IP: ip3,
|
||||||
|
HWAddr: mac3,
|
||||||
|
},
|
||||||
|
wantErrMsg: "updating static lease: no lease for mac " + mac3.String(),
|
||||||
|
}, {
|
||||||
|
name: "duplicate_ip",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip2,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "updating static lease: lease for ip " + ip2.String() +
|
||||||
|
" already exists",
|
||||||
|
}, {
|
||||||
|
name: "duplicate_hostname",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host2,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "updating static lease: lease for hostname " + host2 +
|
||||||
|
" already exists",
|
||||||
|
}, {
|
||||||
|
name: "duplicate_hostname_case",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: strings.ToUpper(host2),
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "updating static lease: lease for hostname " +
|
||||||
|
strings.ToUpper(host2) + " already exists",
|
||||||
|
}, {
|
||||||
|
name: "valid",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host3,
|
||||||
|
IP: ip3,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "",
|
||||||
|
}, {
|
||||||
|
name: "valid_v6",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host6,
|
||||||
|
IP: ip5,
|
||||||
|
HWAddr: mac4,
|
||||||
|
},
|
||||||
|
wantErrMsg: "",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(tc.lease))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDHCPServer_RemoveLease(t *testing.T) {
|
||||||
|
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||||
|
Enabled: true,
|
||||||
|
LocalDomainName: testLocalTLD,
|
||||||
|
Interfaces: testInterfaceConf,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const (
|
||||||
|
host1 = "host1"
|
||||||
|
host2 = "host2"
|
||||||
|
host3 = "host3"
|
||||||
|
)
|
||||||
|
|
||||||
|
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||||
|
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||||
|
ip3 := netip.MustParseAddr("2001:db8::2")
|
||||||
|
|
||||||
|
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
|
mac2 := mustParseMAC(t, "02:03:04:05:06:07")
|
||||||
|
mac3 := mustParseMAC(t, "06:05:04:03:02:01")
|
||||||
|
|
||||||
|
leases := []*dhcpsvc.Lease{{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac1,
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Hostname: host3,
|
||||||
|
IP: ip3,
|
||||||
|
HWAddr: mac3,
|
||||||
|
IsStatic: true,
|
||||||
|
}}
|
||||||
|
for _, l := range leases {
|
||||||
|
require.NoError(t, srv.AddLease(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
lease *dhcpsvc.Lease
|
||||||
|
wantErrMsg string
|
||||||
|
}{{
|
||||||
|
name: "not_found_mac",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac2,
|
||||||
|
},
|
||||||
|
wantErrMsg: "removing lease: no lease for mac " + mac2.String(),
|
||||||
|
}, {
|
||||||
|
name: "not_found_ip",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip2,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "removing lease: no lease for ip " + ip2.String(),
|
||||||
|
}, {
|
||||||
|
name: "not_found_host",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host2,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "removing lease: no lease for hostname " + host2,
|
||||||
|
}, {
|
||||||
|
name: "valid",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host1,
|
||||||
|
IP: ip1,
|
||||||
|
HWAddr: mac1,
|
||||||
|
},
|
||||||
|
wantErrMsg: "",
|
||||||
|
}, {
|
||||||
|
name: "valid_v6",
|
||||||
|
lease: &dhcpsvc.Lease{
|
||||||
|
Hostname: host3,
|
||||||
|
IP: ip3,
|
||||||
|
HWAddr: mac3,
|
||||||
|
},
|
||||||
|
wantErrMsg: "",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(tc.lease))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Empty(t, srv.Leases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDHCPServer_Reset(t *testing.T) {
|
||||||
|
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||||
|
Enabled: true,
|
||||||
|
LocalDomainName: testLocalTLD,
|
||||||
|
Interfaces: testInterfaceConf,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leases := []*dhcpsvc.Lease{{
|
||||||
|
Hostname: "host1",
|
||||||
|
IP: netip.MustParseAddr("192.168.0.2"),
|
||||||
|
HWAddr: mustParseMAC(t, "01:02:03:04:05:06"),
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Hostname: "host2",
|
||||||
|
IP: netip.MustParseAddr("192.168.0.3"),
|
||||||
|
HWAddr: mustParseMAC(t, "06:05:04:03:02:01"),
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Hostname: "host3",
|
||||||
|
IP: netip.MustParseAddr("2001:db8::2"),
|
||||||
|
HWAddr: mustParseMAC(t, "02:03:04:05:06:07"),
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Hostname: "host4",
|
||||||
|
IP: netip.MustParseAddr("2001:db8::3"),
|
||||||
|
HWAddr: mustParseMAC(t, "06:05:04:03:02:02"),
|
||||||
|
IsStatic: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, l := range leases {
|
||||||
|
require.NoError(t, srv.AddLease(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, srv.Leases(), len(leases))
|
||||||
|
|
||||||
|
require.NoError(t, srv.Reset())
|
||||||
|
|
||||||
|
assert.Empty(t, srv.Leases())
|
||||||
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
|
||||||
const prefLen = netutil.IPv6BitLen - 8
|
const prefLen = netutil.IPv6BitLen - 8
|
||||||
|
|
||||||
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) {
|
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) {
|
||||||
return !iface.rangeStart.Less(ip) &&
|
return !ip.Less(iface.rangeStart) &&
|
||||||
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
|
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
|
||||||
})
|
})
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|
Loading…
Reference in a new issue