Skip to content

Commit

Permalink
Pull request 2152: 4923 gopacket DHCP vol.7
Browse files Browse the repository at this point in the history
Updates #4923.

Squashed commit of the following:

commit 0f90eb3
Merge: 38b3165 bd99e3e
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 38b3165
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Feb 19 14:52:01 2024 +0300

    dhcpsvc: imp docs

commit 0a07892
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Feb 19 14:48:19 2024 +0300

    dhcpsvc: imp code

commit 30691f0
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Feb 15 19:57:41 2024 +0300

    dhcpsvc: imp code, dry

commit 20f5ef8
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Feb 15 15:57:09 2024 +0300

    dhcpsvc: finish leases methods
  • Loading branch information
EugeneOne1 committed Feb 20, 2024
1 parent bd99e3e commit 6fd0a62
Show file tree
Hide file tree
Showing 6 changed files with 637 additions and 81 deletions.
20 changes: 12 additions & 8 deletions internal/dhcpsvc/dhcpsvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
// Interface is a DHCP service.
//
// 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 {
agh.ServiceWithConfig[*Config]

Expand All @@ -29,6 +31,8 @@ type Interface interface {
// 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
// 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)

// IPByHost returns the IP address of the DHCP client with the given
Expand All @@ -44,17 +48,17 @@ type Interface interface {
// signatures instead of cloning the whole list.
Leases() (ls []*Lease)

// AddLease adds a new DHCP lease. It returns an error if the lease is
// invalid or already exists.
// AddLease adds a new DHCP lease. l must be valid. It returns an error if
// l already exists.
AddLease(l *Lease) (err error)

// UpdateStaticLease changes an existing DHCP lease. It returns an error if
// there is no lease with such hardware addressor if new values are invalid
// or already exist.
// UpdateStaticLease replaces an existing static DHCP lease. l must be
// valid. It returns an error if the lease with the given hardware address
// doesn't exist or if other values match another existing lease.
UpdateStaticLease(l *Lease) (err error)

// RemoveLease removes an existing DHCP lease. It returns an error if there
// is no lease equal to l.
// RemoveLease removes an existing DHCP lease. l must be valid. It returns
// an error if there is no lease equal to l.
RemoveLease(l *Lease) (err error)

// Reset removes all the DHCP leases.
Expand Down
26 changes: 26 additions & 0 deletions internal/dhcpsvc/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,29 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {

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 changes: 126 additions & 0 deletions internal/dhcpsvc/leaseindex.go
Original file line number Diff line number Diff line change
@@ -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
}
99 changes: 66 additions & 33 deletions internal/dhcpsvc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"net"
"net/netip"
"slices"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/AdguardTeam/golibs/errors"
"golang.org/x/exp/maps"
)

Expand All @@ -23,17 +23,11 @@ type DHCPServer struct {
// hostnames.
localTLD string

// leasesMu protects the ipIndex and nameIndex fields against concurrent
// access, as well as leaseHandlers within the interfaces.
// leasesMu protects the leases index as well as leases in 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
// leases stores the DHCP leases for quick lookups.
leases *leaseIndex

// interfaces4 is the set of IPv4 interfaces sorted by interface name.
interfaces4 netInterfacesV4
Expand Down Expand Up @@ -88,8 +82,7 @@ func New(conf *Config) (srv *DHCPServer, err error) {
enabled: enabled,
localTLD: conf.LocalDomainName,
leasesMu: &sync.RWMutex{},
leaseByIP: map[netip.Addr]*Lease{},
leaseByName: map[string]*Lease{},
leases: newLeaseIndex(),
interfaces4: ifaces4,
interfaces6: ifaces6,
icmpTimeout: conf.ICMPTimeout,
Expand Down Expand Up @@ -120,6 +113,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
leases = append(leases, lease.Clone())
}
}
for _, iface := range srv.interfaces6 {
for _, lease := range iface.leases {
leases = append(leases, lease.Clone())
}
}

return leases
}
Expand All @@ -129,7 +127,7 @@ func (srv *DHCPServer) HostByIP(ip netip.Addr) (host string) {
srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock()

if l, ok := srv.leaseByIP[ip]; ok {
if l, ok := srv.leases.leaseByAddr(ip); ok {
return l.Hostname
}

Expand All @@ -141,7 +139,7 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock()

if l, ok := srv.leaseByIP[ip]; ok {
if l, ok := srv.leases.leaseByAddr(ip); ok {
return l.HWAddr
}

Expand All @@ -150,12 +148,10 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {

// 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 {
if l, ok := srv.leases.leaseByName(host); ok {
return l.IP
}

Expand All @@ -173,39 +169,76 @@ func (srv *DHCPServer) Reset() (err error) {
for _, iface := range srv.interfaces6 {
iface.reset()
}

clear(srv.leaseByIP)
clear(srv.leaseByName)
srv.leases.clear()

return nil
}

// AddLease implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) AddLease(l *Lease) (err error) {
var ok bool
var iface *netInterface
defer func() { err = errors.Annotate(err, "adding lease: %w") }()

addr := l.IP

if addr.Is4() {
iface, ok = srv.interfaces4.find(addr)
} else {
iface, ok = srv.interfaces6.find(addr)
iface, err := srv.ifaceForAddr(addr)
if err != nil {
// Don't wrap the error since there is already an annotation deferred.
return err
}
if !ok {
return fmt.Errorf("no interface for IP address %s", addr)

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()

err = iface.insertLease(l)
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.leaseByIP[l.IP] = l
srv.leaseByName[strings.ToLower(l.Hostname)] = l
srv.leasesMu.Lock()
defer srv.leasesMu.Unlock()

return nil
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() {
iface, ok = srv.interfaces4.find(addr)
} else {
iface, ok = srv.interfaces6.find(addr)
}
if !ok {
return nil, fmt.Errorf("no interface for ip %s", addr)
}

return iface, nil
}

0 comments on commit 6fd0a62

Please sign in to comment.