Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/dhcpsvc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,20 @@ func TestIPv6Config_Validate(t *testing.T) {
name: "disabled",
conf: &dhcpsvc.IPv6Config{Enabled: false},
wantErrMsg: "",
}, {
name: "nil_clock",
conf: &dhcpsvc.IPv6Config{
Enabled: true,
Clock: nil,
RangeStart: testIPv6Conf.RangeStart,
LeaseDuration: testIPv6Conf.LeaseDuration,
},
wantErrMsg: "clock: no value",
}, {
name: "bad_range_start",
conf: &dhcpsvc.IPv6Config{
Enabled: true,
Clock: testIPv6Conf.Clock,
RangeStart: testIPv4Conf.GatewayIP,
LeaseDuration: 1 * time.Hour,
},
Expand All @@ -145,6 +155,7 @@ func TestIPv6Config_Validate(t *testing.T) {
name: "bad_lease_duration",
conf: &dhcpsvc.IPv6Config{
Enabled: true,
Clock: testIPv6Conf.Clock,
RangeStart: netip.MustParseAddr(testRangeStartV6Str),
LeaseDuration: 0,
},
Expand All @@ -153,6 +164,7 @@ func TestIPv6Config_Validate(t *testing.T) {
name: "valid",
conf: &dhcpsvc.IPv6Config{
Enabled: true,
Clock: testIPv6Conf.Clock,
RangeStart: netip.MustParseAddr(testRangeStartV6Str),
LeaseDuration: 1 * time.Hour,
},
Expand Down
22 changes: 12 additions & 10 deletions internal/dhcpsvc/dhcpsvc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ var (
Enabled: true,
}

// testIPv6Conf is a common valid IPv6 part of the interface configuration
// for tests.
testIPv6Conf = &dhcpsvc.IPv6Config{
Enabled: true,
Clock: testClock,
RangeStart: netip.MustParseAddr(testRangeStartV6Str),
LeaseDuration: testLeaseTTL,
RAAllowSLAAC: true,
RASLAACOnly: true,
}

// testIfaceAddr is a common valid IPv4 address of the test network
// interface, compliant with [testIPv4Conf], i.e. outside of the range,
// within the subnet, not equal to the gateway.
Expand All @@ -132,16 +143,6 @@ var (
testIfaceHWAddr = net.HardwareAddr{0x01, 0x01, 0x01, 0x01, 0x01, 0x01}
)

// testIPv6Conf is a common valid IPv6 part of the interface configuration for
// tests.
var testIPv6Conf = &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr(testRangeStartV6Str),
LeaseDuration: testLeaseTTL,
RAAllowSLAAC: true,
RASLAACOnly: true,
}

// testInterfaceConf is a common valid set of interface configurations for
// tests.
var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
Expand All @@ -161,6 +162,7 @@ var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
Clock: timeutil.SystemClock{},
RangeStart: netip.MustParseAddr(testAnotherRangeStartV6Str),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
Expand Down
2 changes: 1 addition & 1 deletion internal/dhcpsvc/handler4.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (iface *dhcpInterfaceV4) handleDiscover(
return
}

lease, err := iface.allocateLease(ctx, mac)
lease, err := iface.common.allocateLease(ctx, mac, iface.addrChecker, iface.clock)
if err != nil {
l.ErrorContext(ctx, "allocating a lease", slogutil.KeyError, err)

Expand Down
40 changes: 36 additions & 4 deletions internal/dhcpsvc/handler6.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
Expand Down Expand Up @@ -67,11 +68,9 @@ func (iface *dhcpInterfaceV6) handleDHCPv6(

// handleSolicit handles messages of type SOLICIT. req must not be nil and must
// be a valid DHCPv6 message of type SOLICIT. fd must be valid.
//
// TODO(e.burkov): Implement. This is a stub for now.
func (iface *dhcpInterfaceV6) handleSolicit(
ctx context.Context,
_ *frameData6,
fd *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDNoServer(req.Options)
Expand All @@ -82,7 +81,40 @@ func (iface *dhcpInterfaceV6) handleSolicit(
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

return nil
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()

lease, iaid := iface.allocateForSolicit(ctx, fd.ether.SrcMAC, req)

resp := &layers.DHCPv6{
MsgType: layers.DHCPv6MsgTypeAdverstise,
TransactionID: req.TransactionID,
}

if lease == nil {
l.DebugContext(ctx, "no ia_na in solicit or no addresses available")
resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, nil, false)

return respond6(fd, resp)
}

_, isRapidCommit := findOption6(req.Options, layers.DHCPv6OptRapidCommit)

if !isRapidCommit {
resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, lease, false)

return respond6(fd, resp)
}

err = iface.commit(ctx, req, lease)
if err != nil {
l.WarnContext(ctx, "committing rapid leases", slogutil.KeyError, err)
isRapidCommit = false
}

resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, lease, isRapidCommit)

return respond6(fd, resp)
}

// handleRequest handles messages of type REQUEST. req must not be nil and must
Expand Down
104 changes: 99 additions & 5 deletions internal/dhcpsvc/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"log/slog"
"net"
"net/netip"
"slices"
"sync"
"time"

"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
)

Expand Down Expand Up @@ -125,13 +127,18 @@ func (iface *netInterface) removeLease(l *Lease) (err error) {
}

// blockLease marks l as blocked for a configured TTL, as reported by
// [Lease.IsBlocked]. indexMu must be locked, It also removes the lease from
// iface. l must not be nil.
// [Lease.IsBlocked]. It also removes the lease from iface, but leaves it in
// the index. iface.indexMu must be locked, l and clock must not be nil.
func (iface *netInterface) blockLease(
ctx context.Context,
l *Lease,
clock timeutil.Clock,
) (err error) {
err = iface.removeLease(l)
if err != nil {
return fmt.Errorf("removing lease: %w", err)
}

l.HWAddr = blockedHardwareAddr
l.Hostname = ""
l.Expiry = clock.Now().Add(iface.leaseTTL)
Expand All @@ -145,7 +152,8 @@ func (iface *netInterface) blockLease(
return nil
}

// nextIP generates a new free IP.
// nextIP generates a new free IP. It returns netip.Addr{} if there are no free
// IPs in the address space. iface.indexMu must be locked.
func (iface *netInterface) nextIP() (ip netip.Addr) {
r := iface.addrSpace
ip = r.find(func(next netip.Addr) (ok bool) {
Expand All @@ -160,8 +168,8 @@ func (iface *netInterface) nextIP() (ip netip.Addr) {
return ip
}

// findExpiredLease returns the first found lease that has expired. indexMu
// must be locked.
// findExpiredLease returns the first found lease that has expired.
// iface.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) {
Expand All @@ -171,3 +179,89 @@ func (iface *netInterface) findExpiredLease(now time.Time) (l *Lease) {

return nil
}

// allocateLease allocates a new lease for the MAC address. If there are no IP
// addresses left, both lease and err are nil. mac must be a valid according to
// [netutil.ValidateMAC].
//
// TODO(e.burkov): Pass the precalculated macKey.
func (iface *netInterface) allocateLease(
ctx context.Context,
mac net.HardwareAddr,
checker addressChecker,
clock timeutil.Clock,
) (lease *Lease, err error) {
key := macToKey(mac)

for {
lease, err = iface.reserveLease(ctx, mac, clock)
if err != nil {
return nil, err
}

var ok bool
ok, err = checker.IsAvailable(lease.IP)
if err != nil {
return nil, fmt.Errorf("checking address availability: %w", err)
}

if ok {
iface.leases[key] = lease

off, _ := iface.addrSpace.offset(lease.IP)
iface.leasedOffsets.set(off, true)

return lease, nil
}

iface.logger.DebugContext(ctx, "address not available", "ip", lease.IP)

err = iface.blockLease(ctx, lease, clock)
if err != nil {
return nil, fmt.Errorf("blocking unavailable address: %w", err)
}
}
}

// reserveLease reserves a lease for a client by its MAC-address. lease is nil
// if a new lease can't be allocated. mac must be a valid according to
// [netutil.ValidateMAC]. iface.indexMu mutex must be locked.
func (iface *netInterface) reserveLease(
ctx context.Context,
mac net.HardwareAddr,
clock timeutil.Clock,
) (lease *Lease, err error) {
// TODO(e.burkov): Limit the number of attempts.
nextIP := iface.nextIP()
if nextIP != (netip.Addr{}) {
lease = &Lease{
HWAddr: slices.Clone(mac),
IP: nextIP,
Expiry: clock.Now().Add(iface.leaseTTL),
}

return lease, nil
}

lease = iface.findExpiredLease(clock.Now())
if lease == nil {
return nil, errors.Error("no addresses available to lease")
}

err = iface.index.remove(ctx, iface.logger, lease, iface)
if err != nil {
// TODO(e.burkov): Reconsider the severity of this error, it actually
// seems impossible to get the error about the existing lease from the
// method.
iface.logger.DebugContext(ctx, "deleting expired lease", slogutil.KeyError, err)
}

lease.HWAddr = slices.Clone(mac)
lease.Hostname = ""
lease.IsStatic = false
lease.updateExpiry(clock, iface.leaseTTL)

iface.leases[macToKey(mac)] = lease

return lease, nil
}
8 changes: 6 additions & 2 deletions internal/dhcpsvc/lease.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import (
//
// TODO(e.burkov): Add validation method.
//
// TODO(e.burkov): Migrate to add DUID and IAID fields for DHCPv6 leases.
// BUG(e.burkov): The implementation currently relies on the client's hardware
// address for client identification. This approach is not recommended by RFC
// 9915, so the database should be migrated to use the client's DUID and IAID
// for lease identification.
type Lease struct {
// IP is the IP address leased to the client. It must not be empty.
IP netip.Addr
Expand All @@ -29,7 +32,8 @@ type Lease struct {
// Hostname of the client. It may be empty if the lease is blocked.
Hostname string

// HWAddr is the physical hardware (MAC) address. It must not be nil.
// HWAddr is the physical hardware (MAC) address. It must be a valid
// hardware address of length 6, 8, or 20 bytes, see [netutil.ValidateMAC].
HWAddr net.HardwareAddr

// IsStatic defines if the lease is static.
Expand Down
2 changes: 1 addition & 1 deletion internal/dhcpsvc/leaseindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func (idx *leaseIndex) update(
}

// rangeLeases calls f for each lease in idx in an unspecified order until f
// returns false.
// returns false. It must not be called concurrently, f must not modify leases.
func (idx *leaseIndex) rangeLeases(f func(l *Lease) (cont bool)) {
for _, l := range idx.byName {
if !f(l) {
Expand Down
8 changes: 4 additions & 4 deletions internal/dhcpsvc/options4.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func (iface *dhcpInterfaceV4) appendRequestedOptions(
// Requirements Document, the server MUST include the default value for that
// parameter.
optWithCode := layers.DHCPOption{}
for _, code := range requestedOptions(req) {
for _, code := range requestedOptions4(req) {
optWithCode.Type = code
i, has := slices.BinarySearchFunc(iface.implicitOpts, optWithCode, compareV4OptionCodes)
if has {
Expand Down Expand Up @@ -422,11 +422,11 @@ func clientIdentifier4(msg *layers.DHCPv4) (id []byte) {
return nil
}

// requestedOptions returns the list of options requested in DHCPv4 message, if
// requestedOptions4 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) {
// TODO(e.burkov): Use [iter.Seq].
func requestedOptions4(msg *layers.DHCPv4) (opts []layers.DHCPOpt) {
for _, opt := range msg.Options {
l := len(opt.Data)
if opt.Type != layers.DHCPOptParamsRequest || l == 0 {
Expand Down
Loading
Loading