mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-06-28 03:41:19 +00:00
Pull request 2525: 4923-gopacket-dhcp-vol.13
Squashed commit of the following:
commit 79d96a231649cecbb41b90796ddf0ae6c6bdba4c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Nov 18 14:05:37 2025 +0300
dhcpsvc: imp code, docs
commit 8944c8c44c5306760dda12085af05f37f7bca20f
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Mon Nov 17 17:21:41 2025 +0300
dhcpsvc: add lease allocation
This commit is contained in:
parent
9a32f73a1e
commit
ff0ef4f398
6 changed files with 299 additions and 22 deletions
22
internal/dhcpsvc/addresschecker.go
Normal file
22
internal/dhcpsvc/addresschecker.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package dhcpsvc
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// addressChecker checks addresses for availability.
|
||||
type addressChecker interface {
|
||||
// IsAvailable returns true if the address is available in the current
|
||||
// subnet. Any error is a network error.
|
||||
IsAvailable(ip netip.Addr) (ok bool, err error)
|
||||
}
|
||||
|
||||
// noopAddressChecker is an implementation of [addressChecker] that doesn't
|
||||
// perform any checks.
|
||||
type noopAddressChecker struct{}
|
||||
|
||||
// IsAvailable implements the [addressChecker] interface for noopAddressChecker.
|
||||
func (c noopAddressChecker) IsAvailable(ip netip.Addr) (ok bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Add ICMP implementation of [addressChecker], as required by
|
||||
// https://datatracker.ietf.org/doc/html/rfc2131#section-2.2.
|
||||
49
internal/dhcpsvc/bitset.go
Normal file
49
internal/dhcpsvc/bitset.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package dhcpsvc
|
||||
|
||||
const bitsPerWord = 64
|
||||
|
||||
// bitSet is a sparse bitSet. A nil *bitSet is an empty bitSet.
|
||||
type bitSet struct {
|
||||
words map[uint64]uint64
|
||||
}
|
||||
|
||||
// newBitSet returns a new bitset.
|
||||
func newBitSet() (s *bitSet) {
|
||||
return &bitSet{
|
||||
words: map[uint64]uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
// isSet returns true if the bit n is set.
|
||||
func (s *bitSet) isSet(n uint64) (ok bool) {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
wordIdx := n / bitsPerWord
|
||||
bitIdx := n % bitsPerWord
|
||||
|
||||
var word uint64
|
||||
word, ok = s.words[wordIdx]
|
||||
|
||||
return ok && word&(1<<bitIdx) != 0
|
||||
}
|
||||
|
||||
// set sets or unsets a bit.
|
||||
func (s *bitSet) set(n uint64, ok bool) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
wordIdx := n / bitsPerWord
|
||||
bitIdx := n % bitsPerWord
|
||||
|
||||
word := s.words[wordIdx]
|
||||
if ok {
|
||||
word |= 1 << bitIdx
|
||||
} else {
|
||||
word &^= 1 << bitIdx
|
||||
}
|
||||
|
||||
s.words[wordIdx] = word
|
||||
}
|
||||
90
internal/dhcpsvc/bitset_internal_test.go
Normal file
90
internal/dhcpsvc/bitset_internal_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBitSet(t *testing.T) {
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
var s *bitSet
|
||||
|
||||
ok := s.isSet(0)
|
||||
assert.False(t, ok)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
s.set(0, true)
|
||||
})
|
||||
|
||||
ok = s.isSet(0)
|
||||
assert.False(t, ok)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
s.set(0, false)
|
||||
})
|
||||
|
||||
ok = s.isSet(0)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("non_nil", func(t *testing.T) {
|
||||
s := newBitSet()
|
||||
|
||||
ok := s.isSet(0)
|
||||
assert.False(t, ok)
|
||||
|
||||
s.set(0, true)
|
||||
|
||||
ok = s.isSet(0)
|
||||
assert.True(t, ok)
|
||||
|
||||
s.set(0, false)
|
||||
|
||||
ok = s.isSet(0)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("non_nil_long", func(t *testing.T) {
|
||||
s := newBitSet()
|
||||
|
||||
s.set(0, true)
|
||||
s.set(math.MaxUint64, true)
|
||||
assert.Len(t, s.words, 2)
|
||||
|
||||
ok := s.isSet(0)
|
||||
assert.True(t, ok)
|
||||
|
||||
ok = s.isSet(math.MaxUint64)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("compare_to_map", func(t *testing.T) {
|
||||
m := map[uint64]struct{}{}
|
||||
s := newBitSet()
|
||||
|
||||
mapFunc := func(setNew, checkOld, delOld uint64) (ok bool) {
|
||||
m[setNew] = struct{}{}
|
||||
delete(m, delOld)
|
||||
_, ok = m[checkOld]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
setFunc := func(setNew, checkOld, delOld uint64) (ok bool) {
|
||||
s.set(setNew, true)
|
||||
s.set(delOld, false)
|
||||
ok = s.isSet(checkOld)
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
err := quick.CheckEqual(mapFunc, setFunc, &quick.Config{
|
||||
MaxCount: 10_000,
|
||||
MaxCountScale: 10,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
@ -4,8 +4,11 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// macKey contains hardware address as byte array of 6, 8, or 20 bytes.
|
||||
|
|
@ -38,15 +41,22 @@ type netInterface struct {
|
|||
// TODO(e.burkov): Consider removing it and using the value from context.
|
||||
logger *slog.Logger
|
||||
|
||||
// indexMu protects the index as well as leases in the interfaces.
|
||||
// indexMu protects the index, leases, and leasedOffsets.
|
||||
indexMu *sync.RWMutex
|
||||
|
||||
// leasedOffsets contains offsets from conf.ipRange.start that have been
|
||||
// leased.
|
||||
leasedOffsets *bitSet
|
||||
|
||||
// index stores the DHCP leases for quick lookups.
|
||||
index *leaseIndex
|
||||
|
||||
// leases is the set of DHCP leases assigned to this interface.
|
||||
leases map[macKey]*Lease
|
||||
|
||||
// addrSpace is the IPv4 address space allocated for leasing.
|
||||
addrSpace ipRange
|
||||
|
||||
// name is the name of the network interface.
|
||||
name string
|
||||
|
||||
|
|
@ -70,6 +80,9 @@ func (iface *netInterface) addLease(l *Lease) (err error) {
|
|||
|
||||
iface.leases[mk] = l
|
||||
|
||||
off, _ := iface.addrSpace.offset(l.IP)
|
||||
iface.leasedOffsets.set(off, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +101,7 @@ func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
|
|||
}
|
||||
|
||||
// removeLease removes an existing lease from iface. It returns an error if
|
||||
// there is no lease equal to l.
|
||||
// there is no lease equal to l. l must not be nil.
|
||||
func (iface *netInterface) removeLease(l *Lease) (err error) {
|
||||
mk := macToKey(l.HWAddr)
|
||||
_, found := iface.leases[mk]
|
||||
|
|
@ -98,5 +111,35 @@ func (iface *netInterface) removeLease(l *Lease) (err error) {
|
|||
|
||||
delete(iface.leases, mk)
|
||||
|
||||
off, _ := iface.addrSpace.offset(l.IP)
|
||||
iface.leasedOffsets.set(off, false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextIP generates a new free IP.
|
||||
func (iface *netInterface) nextIP() (ip netip.Addr) {
|
||||
r := iface.addrSpace
|
||||
ip = r.find(func(next netip.Addr) (ok bool) {
|
||||
offset, ok := r.offset(next)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("next: %s: %w", next, errors.ErrOutOfRange))
|
||||
}
|
||||
|
||||
return !iface.leasedOffsets.isSet(offset)
|
||||
})
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// findExpiredLease returns the first found lease that has expired. indexMu
|
||||
// must be locked.
|
||||
func (iface *netInterface) findExpiredLease(now time.Time) (l *Lease) {
|
||||
for _, lease := range iface.leases {
|
||||
if !lease.IsStatic && lease.Expiry.Before(now) {
|
||||
return lease
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -347,6 +347,8 @@ func hostname4(msg *layers.DHCPv4) (hostname string) {
|
|||
|
||||
// requestedOptions returns the list of options requested in DHCPv4 message, if
|
||||
// any.
|
||||
//
|
||||
// TODO(e.burkov): Use [iter.Seq1].
|
||||
func requestedOptions(msg *layers.DHCPv4) (opts []layers.DHCPOpt) {
|
||||
for _, opt := range msg.Options {
|
||||
l := len(opt.Data)
|
||||
|
|
|
|||
|
|
@ -131,15 +131,15 @@ type dhcpInterfaceV4 struct {
|
|||
// clock used to get current time.
|
||||
clock timeutil.Clock
|
||||
|
||||
// addrChecker checks addresses for availability.
|
||||
addrChecker addressChecker
|
||||
|
||||
// gateway is the IP address of the network gateway.
|
||||
gateway netip.Addr
|
||||
|
||||
// subnet is the network subnet of the interface.
|
||||
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.
|
||||
|
|
@ -172,17 +172,20 @@ func (srv *DHCPServer) newDHCPInterfaceV4(
|
|||
addrSpace, _ := newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
|
||||
iface = &dhcpInterfaceV4{
|
||||
gateway: conf.GatewayIP,
|
||||
clock: conf.Clock,
|
||||
subnet: netip.PrefixFrom(conf.GatewayIP, maskLen),
|
||||
addrSpace: addrSpace,
|
||||
// TODO(e.burkov): Use an ICMP implementation.
|
||||
addrChecker: noopAddressChecker{},
|
||||
gateway: conf.GatewayIP,
|
||||
clock: conf.Clock,
|
||||
subnet: netip.PrefixFrom(conf.GatewayIP, maskLen),
|
||||
common: &netInterface{
|
||||
logger: l,
|
||||
indexMu: srv.leasesMu,
|
||||
index: srv.leases,
|
||||
leases: map[macKey]*Lease{},
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
logger: l,
|
||||
indexMu: srv.leasesMu,
|
||||
index: srv.leases,
|
||||
leases: map[macKey]*Lease{},
|
||||
leasedOffsets: newBitSet(),
|
||||
name: name,
|
||||
addrSpace: addrSpace,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
}
|
||||
iface.implicitOpts, iface.explicitOpts = conf.options(ctx, l)
|
||||
|
|
@ -195,8 +198,8 @@ func (iface *dhcpInterfaceV4) commitLease(ctx context.Context, l *Lease, hostnam
|
|||
// TODO(e.burkov): Implement.
|
||||
}
|
||||
|
||||
// sendOffer sends a DHCPOFFER message to the client.
|
||||
func (iface *dhcpInterfaceV4) sendOffer(
|
||||
// respondOffer sends a DHCPOFFER message to the client.
|
||||
func (iface *dhcpInterfaceV4) respondOffer(
|
||||
ctx context.Context,
|
||||
rw responseWriter4,
|
||||
req *layers.DHCPv4,
|
||||
|
|
@ -316,16 +319,21 @@ func (iface *dhcpInterfaceV4) handleDiscover(
|
|||
l.DebugContext(ctx, "different requested ip", "requested", reqIP, "lease", lease.IP)
|
||||
}
|
||||
|
||||
iface.sendOffer(ctx, rw, req, lease)
|
||||
iface.respondOffer(ctx, rw, req, lease)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Allocate a new lease.
|
||||
lease = &Lease{}
|
||||
lease, err := iface.allocateLease(ctx, mac)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "allocating a lease", "error", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Send DHCPOFFER with new lease.
|
||||
iface.sendOffer(ctx, rw, req, lease)
|
||||
iface.respondOffer(ctx, rw, req, lease)
|
||||
}
|
||||
|
||||
// handleSelecting handles messages of type request in SELECTING state. req
|
||||
|
|
@ -481,7 +489,7 @@ func (iface *dhcpInterfaceV4) handleRenew(
|
|||
type dhcpInterfacesV4 []*dhcpInterfaceV4
|
||||
|
||||
// find returns the first network interface within ifaces containing ip. It
|
||||
// returns false if there is no such interface.
|
||||
// returns false if there is no such interface. ip must be valid.
|
||||
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
|
||||
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
|
||||
return iface.subnet.Contains(ip)
|
||||
|
|
@ -494,7 +502,7 @@ func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok boo
|
|||
}
|
||||
|
||||
// findInterface returns the first DHCPv4 interface within ifaces containing
|
||||
// ip. It returns false if there is no such interface.
|
||||
// ip. It returns false if there is no such interface. ip must be valid.
|
||||
func (ifaces dhcpInterfacesV4) findInterface(ip netip.Addr) (iface *dhcpInterfaceV4, ok bool) {
|
||||
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
|
||||
return iface.subnet.Contains(ip)
|
||||
|
|
@ -505,3 +513,66 @@ func (ifaces dhcpInterfacesV4) findInterface(ip netip.Addr) (iface *dhcpInterfac
|
|||
|
||||
return ifaces[i], true
|
||||
}
|
||||
|
||||
// allocateLease allocates a new lease for the MAC address. If there are no IP
|
||||
// addresses left, both l and err are nil. mac must be a valid according to
|
||||
// [netutil.ValidateMAC].
|
||||
func (iface *dhcpInterfaceV4) allocateLease(
|
||||
ctx context.Context,
|
||||
mac net.HardwareAddr,
|
||||
) (l *Lease, err error) {
|
||||
for {
|
||||
l, err = iface.reserveLease(ctx, mac)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reserving a lease: %w", err)
|
||||
}
|
||||
|
||||
var ok bool
|
||||
ok, err = iface.addrChecker.IsAvailable(l.IP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking address availability: %w", err)
|
||||
} else if ok {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
iface.common.logger.DebugContext(ctx, "address not available", "ip", l.IP)
|
||||
// TODO(e.burkov): Implement blacklisting of unavailable addresses.
|
||||
}
|
||||
}
|
||||
|
||||
// reserveLease reserves a lease for a client by its MAC-address. l is nil if a
|
||||
// new lease can't be allocated. mac must be a valid according to
|
||||
// [netutil.ValidateMAC].
|
||||
func (iface *dhcpInterfaceV4) reserveLease(
|
||||
ctx context.Context,
|
||||
mac net.HardwareAddr,
|
||||
) (l *Lease, err error) {
|
||||
nextIP := iface.common.nextIP()
|
||||
if nextIP == (netip.Addr{}) {
|
||||
l = iface.common.findExpiredLease(iface.clock.Now())
|
||||
if l == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Move validation from index methods into server's
|
||||
// methods and use index here.
|
||||
delete(iface.common.leases, macToKey(l.HWAddr))
|
||||
|
||||
l.HWAddr = slices.Clone(mac)
|
||||
iface.common.leases[macToKey(mac)] = l
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
l = &Lease{
|
||||
HWAddr: slices.Clone(mac),
|
||||
IP: nextIP,
|
||||
}
|
||||
|
||||
err = iface.common.index.add(ctx, iface.common.logger, l, iface.common)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue