Pull request 2732: AGDNS-3863-gopacket-dhcp-vol.31
Some checks failed
build / test (macOS-latest) (push) Has been cancelled
build / test (ubuntu-latest) (push) Has been cancelled
build / test (windows-latest) (push) Has been cancelled
lint / go-lint (push) Has been cancelled
lint / eslint (push) Has been cancelled
build / build-release (push) Has been cancelled
build / notify (push) Has been cancelled
lint / notify (push) Has been cancelled

Updates #4923.

Squashed commit of the following:

commit 14537164e48e4bc30f9b3594abbfab60c7b3f84c
Merge: 8ae926b76 254e17dab
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jul 27 17:42:32 2026 +0300

    Merge branch 'master' into AGDNS-3863-gopacket-dhcp-vol.31

commit 8ae926b76c
Merge: 55e177638 336e9c9df
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jul 27 17:36:42 2026 +0300

    Merge branch 'master' into AGDNS-3863-gopacket-dhcp-vol.31

commit 55e177638c
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jul 27 16:38:51 2026 +0300

    dhcspvc: imp docs

commit 9088177898
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jul 27 15:57:27 2026 +0300

    dhcpsvc: imp code

commit 94529fe836
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Jul 24 18:11:46 2026 +0300

    dhcpsvc: imp code

commit c0c26d4ce1
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Jul 24 18:00:58 2026 +0300

    dhcpsvc: handle confirm
This commit is contained in:
Eugene Burkov 2026-07-27 15:01:33 +00:00
parent 254e17dabf
commit 8e56afa92e
12 changed files with 426 additions and 228 deletions

View file

@ -11,12 +11,12 @@ type addressChecker interface {
// noopAddressChecker is an implementation of [addressChecker] that doesn't
// perform any checks.
//
// TODO(e.burkov): Add ICMP implementation of [addressChecker], as required by
// https://datatracker.ietf.org/doc/html/rfc2131#section-2.2.
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.

View file

@ -149,7 +149,7 @@ func TestIPv6Config_Validate(t *testing.T) {
RangeStart: testIPv4Conf.GatewayIP,
LeaseDuration: 1 * time.Hour,
},
wantErrMsg: "range start " + testGatewayIPv4Str + " must be a valid ipv6",
wantErrMsg: "range start: " + testGatewayIPv4Str + ": must be a valid ipv6",
}, {
name: "bad_lease_duration",
conf: &dhcpsvc.IPv6Config{

View file

@ -270,6 +270,10 @@ var (
// testIPv6Static is the test IP address for a known static lease.
testIPv6Static = netip.MustParseAddr("2001:db8::65")
// testIPv6OtherSubnet is the test IP address for a client on another
// subnet.
testIPv6OtherSubnet = netip.MustParseAddr(testAnotherRangeStartV6Str)
)
// Time-related variables for test cases.

View file

@ -4,6 +4,8 @@ import (
"cmp"
"context"
"fmt"
"log/slog"
"net"
"net/netip"
"slices"
@ -112,6 +114,8 @@ func (iface *dhcpInterfaceV4) handleDHCPv4(
// handleDiscover handles messages of type DHCPDISCOVER. req must be a
// DHCPDISCOVER message, fd must be valid.
//
// TODO(e.burkov): Remove allocated leases after client have chosen one.
func (iface *dhcpInterfaceV4) handleDiscover(
ctx context.Context,
req *layers.DHCPv4,
@ -127,6 +131,8 @@ func (iface *dhcpInterfaceV4) handleDiscover(
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
now := iface.clock.Now()
lease, hasLease := iface.common.leases[mk]
if hasLease {
reqIP, hasReqIP := requestedIPv4(req)
@ -134,13 +140,13 @@ func (iface *dhcpInterfaceV4) handleDiscover(
l.DebugContext(ctx, "different requested ip", "requested", reqIP, "lease", lease.IP)
}
lease.updateExpiry(iface.clock, iface.common.leaseTTL)
lease.updateExpiry(now, iface.common.leaseTTL)
iface.respondOffer(ctx, req, fd, lease, idOpt)
return
}
lease, err := iface.common.allocateLease(ctx, mac, iface.addrChecker, iface.clock)
lease, err := iface.common.allocateLease(ctx, mac, now)
if err != nil {
l.ErrorContext(ctx, "allocating a lease", slogutil.KeyError, err)
@ -155,8 +161,6 @@ func (iface *dhcpInterfaceV4) handleDiscover(
// DHCPREQUEST message. req must not be nil, fd must be valid.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.
//
// TODO(e.burkov): Remove allocated leases after client have chosen one.
func (iface *dhcpInterfaceV4) handleRequest(
ctx context.Context,
req *layers.DHCPv4,
@ -292,7 +296,9 @@ func (iface *dhcpInterfaceV4) handleInitReboot(
if !hasLease {
// If the DHCP server has no record of this client, then it MUST remain
// silent, and MAY output a warning to the network administrator.
l.WarnContext(ctx, "no existing lease", "mac", mac)
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.
l.InfoContext(ctx, "no existing lease", "mac", mac)
return
}
@ -342,7 +348,6 @@ func (iface *dhcpInterfaceV4) handleRenew(
// silent, and MAY output a warning to the network administrator.
l.InfoContext(ctx, "no existing lease", "mac", mac)
// TODO(e.burkov): Investigate if we should respond with NAK.
return
}
@ -369,52 +374,37 @@ func (iface *dhcpInterfaceV4) handleRenew(
// handleDecline handles messages of type DHCPDECLINE. req must be a
// DHCPDECLINE message.
//
// TODO(e.burkov): Log the message option, as the request should include one.
//
// TODO(e.burkov): Consider DRY'ing this with [dhcpInterfaceV4.handleRelease].
func (iface *dhcpInterfaceV4) handleDecline(ctx context.Context, req *layers.DHCPv4) {
l := iface.common.logger
reqIP, hasReqIP := requestedIPv4(req)
if !hasReqIP {
l.DebugContext(ctx, "skipping decline message without requested ip")
if !hasReqIP || !iface.subnet.Contains(reqIP) {
l.DebugContext(ctx, "skipping decline message", "requested_ip", reqIP)
return
}
if !iface.subnet.Contains(reqIP) {
l.DebugContext(ctx, "skipping decline message", "requestedip", reqIP)
return
}
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.ErrorContext(ctx, "decline message for non-existing lease", "mac", mac)
return
}
if lease.IP != reqIP {
l.ErrorContext(ctx, "decline mismatch", "ip", reqIP, "lease", lease.IP)
lease := iface.leaseByMacWithIP(ctx, l, req.ClientHWAddr, reqIP)
if lease == nil {
return
}
l.WarnContext(ctx, "lease reported to be unavailable", "ip", lease.IP)
err := iface.common.blockLease(ctx, lease, iface.clock)
err := iface.common.blockLease(ctx, lease, iface.clock.Now())
if err != nil {
l.ErrorContext(ctx, "blocking lease", slogutil.KeyError, err)
}
var args []any
if msg, ok := message4(req); ok {
args = append(args, "message", msg)
}
l.DebugContext(ctx, "lease declined", args...)
}
// handleRelease handles messages of type DHCPRELEASE. req must be a
@ -422,32 +412,21 @@ func (iface *dhcpInterfaceV4) handleDecline(ctx context.Context, req *layers.DHC
//
// TODO(e.burkov): Retain the lease instead of removing it completely.
func (iface *dhcpInterfaceV4) handleRelease(ctx context.Context, req *layers.DHCPv4) {
l := iface.common.logger
l := iface.common.logger.With("msg_type", layers.DHCPMsgTypeRelease)
ip, _ := netip.AddrFromSlice(req.ClientIP.To4())
if !iface.subnet.Contains(ip) {
ip, ok := netip.AddrFromSlice(req.ClientIP.To4())
if !ok || !iface.subnet.Contains(ip) {
l.DebugContext(ctx, "skipping release message", "clientip", ip)
return
}
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.WarnContext(ctx, "release message for non-existing lease", "mac", mac)
return
}
if lease.IP != ip {
l.WarnContext(ctx, "release mismatch", "ip", ip, "lease", lease.IP)
lease := iface.leaseByMacWithIP(ctx, l, req.ClientHWAddr, ip)
if lease == nil {
return
}
@ -458,3 +437,29 @@ func (iface *dhcpInterfaceV4) handleRelease(ctx context.Context, req *layers.DHC
return
}
}
// leaseByMacWithIP returns the lease for the given MAC address and IP address.
// It returns nil if the lease doesn't exist or if the IP address doesn't match
// the lease, logging each case. logger must not be nil, mac must be a valid
// MAC address, ip must be a valid IPv4 address.
func (iface *dhcpInterfaceV4) leaseByMacWithIP(
ctx context.Context,
logger *slog.Logger,
mac net.HardwareAddr,
ip netip.Addr,
) (lease *Lease) {
lease, ok := iface.common.leases[macToKey(mac)]
if !ok {
logger.WarnContext(ctx, "non-existent lease", "mac", mac)
return nil
}
if lease.IP != ip {
logger.WarnContext(ctx, "ip doesn't match", "mac", mac, "expected", ip, "actual", lease.IP)
return nil
}
return lease
}

View file

@ -40,8 +40,6 @@ func (srv *DHCPServer) serveEther6(ctx context.Context, iface *dhcpInterfaceV6,
// serveV6 handles the ethernet packet of IPv6 type. iface and pkt must not be
// nil. iface and fd must be valid. pkt must be an IPv6 packet.
//
//lint:ignore U1000 TODO(e.burkov): Use.
func (srv *DHCPServer) serveV6(
ctx context.Context,
iface *dhcpInterfaceV6,
@ -52,9 +50,7 @@ func (srv *DHCPServer) serveV6(
msg, ok := pkt.Layer(layers.LayerTypeDHCPv6).(*layers.DHCPv6)
if !ok {
// TODO(e.burkov): Consider adding some debug information about the
// actual received packet.
srv.logger.DebugContext(ctx, "skipping non-dhcpv6 packet")
srv.logger.DebugContext(ctx, "skipping non-dhcpv6 packet", "pkt", pkt)
return nil
}
@ -207,11 +203,9 @@ func (iface *dhcpInterfaceV6) handleRequest(
// handleConfirm handles messages of type CONFIRM. req must not be nil and must
// be a valid DHCPv6 message of type CONFIRM. fd must be valid.
//
// TODO(e.burkov): Implement. This is a stub for now.
func (iface *dhcpInterfaceV6) handleConfirm(
ctx context.Context,
_ *frameData6,
fd *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDNoServer(req.Options)
@ -222,7 +216,35 @@ func (iface *dhcpInterfaceV6) handleConfirm(
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
// Collect all addresses from IA_NA options and check if they are
// appropriate for the link to which the client is attached.
//
// See RFC 9915 Section 18.3.3.
allOnLink, hasAddrs := iface.confirmAddrsOnLink(ctx, req)
if !hasAddrs {
// If the server is unable to perform this test (for example, the server
// does not have information about prefixes on the link to which the
// client is connected) or there were no addresses in any of the IAs
// sent by the client, the server MUST NOT send a Reply to the client.
//
// See RFC 9915 Section 18.3.3.
l.DebugContext(ctx, "no addresses in IA_NA options")
return nil
}
status := layers.DHCPv6StatusCodeSuccess
if !allOnLink {
status = layers.DHCPv6StatusCodeNotOnLink
}
resp := &layers.DHCPv6{
MsgType: layers.DHCPv6MsgTypeReply,
TransactionID: req.TransactionID,
Options: iface.newConfirmRespOpts(fd, cliID, status),
}
return respond6(fd, resp)
}
// handleRenew handles messages of type RENEW. req must not be nil and must be

View file

@ -44,42 +44,42 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, false),
in: newDHCPv6Solicit(t, testHWUnknown, testIPv6Unknown, false),
name: "new",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6SOLICIT(t, testHWStatic, testIPv6Static, false),
in: newDHCPv6Solicit(t, testHWStatic, testIPv6Static, false),
name: "existing_static",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWStatic),
newOptIANA(t, testIAID, testIPv6Static),
newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6SOLICIT(t, testHWDynamic, testIPv6Dynamic, false),
in: newDHCPv6Solicit(t, testHWDynamic, testIPv6Dynamic, false),
name: "existing_dynamic",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWDynamic),
newOptIANA(t, testIAID, testIPv6Dynamic),
newOptIANA(t, testIAID, testIPv6Dynamic, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6SOLICIT(t, testHWExpired, testIPv6Expired, false),
in: newDHCPv6Solicit(t, testHWExpired, testIPv6Expired, false),
name: "existing_expired",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWExpired),
newOptIANA(t, testIAID, testIPv6Expired),
newOptIANA(t, testIAID, testIPv6Expired, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
@ -118,7 +118,7 @@ func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) {
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, true),
in: newDHCPv6Solicit(t, testHWUnknown, testIPv6Unknown, true),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testExpiryDynamicLease,
@ -130,37 +130,37 @@ func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) {
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
},
}, {
in: newDHCPv6SOLICIT(t, testHWStatic, testIPv6Static, true),
in: newDHCPv6Solicit(t, testHWStatic, testIPv6Static, true),
want: testLease6Static,
name: "existing",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWStatic),
newOptIANA(t, testIAID, testIPv6Static),
newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
},
}, {
in: newDHCPv6SOLICIT(t, testHWDynamic, testIPv6Dynamic, true),
in: newDHCPv6Solicit(t, testHWDynamic, testIPv6Dynamic, true),
want: testLease6Dynamic,
name: "existing_dynamic",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWDynamic),
newOptIANA(t, testIAID, testIPv6Dynamic),
newOptIANA(t, testIAID, testIPv6Dynamic, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
},
}, {
in: newDHCPv6SOLICIT(t, testHWExpired, testIPv6Expired, true),
in: newDHCPv6Solicit(t, testHWExpired, testIPv6Expired, true),
want: &dhcpsvc.Lease{
IP: testIPv6Expired,
Expiry: testExpiryDynamicLease,
@ -172,7 +172,7 @@ func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) {
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWExpired),
newOptIANA(t, testIAID, testIPv6Expired),
newOptIANA(t, testIAID, testIPv6Expired, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
@ -218,15 +218,13 @@ func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) {
func TestDHCPServer_ServeEther6_request(t *testing.T) {
t.Parallel()
notOnLinkAddr := netip.MustParseAddr(testAnotherRangeStartV6Str)
testCases := []struct {
in gopacket.Packet
want *dhcpsvc.Lease
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
in: newDHCPv6Request(t, testHWUnknown, testIPv6Unknown),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testExpiryDynamicLease,
@ -238,12 +236,12 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, notOnLinkAddr),
in: newDHCPv6Request(t, testHWUnknown, testIPv6OtherSubnet),
want: nil,
name: "not_on_link",
wantOpts: layers.DHCPv6Options{
@ -254,18 +252,18 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWStatic, testIPv6Static),
in: newDHCPv6Request(t, testHWStatic, testIPv6Static),
want: testLease6Static,
name: "existing_static",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWStatic),
newOptIANA(t, testIAID, testIPv6Static),
newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, netip.Addr{}),
in: newDHCPv6Request(t, testHWUnknown, netip.Addr{}),
want: nil,
name: "no_iana",
wantOpts: layers.DHCPv6Options{
@ -320,8 +318,8 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) {
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, false),
in: newDHCPv6Request(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6Solicit(t, testHWUnknown, testIPv6Unknown, false),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testExpiryDynamicLease,
@ -333,13 +331,13 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) {
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, true),
in: newDHCPv6Request(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6Solicit(t, testHWUnknown, testIPv6Unknown, true),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testExpiryDynamicLease,
@ -351,7 +349,7 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) {
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
@ -396,8 +394,101 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) {
}
}
// newDHCPv6SOLICIT creates a new DHCPv6 SOLICIT packet for testing.
func newDHCPv6SOLICIT(
func TestDHCPServer_ServeEther6_confirm(t *testing.T) {
t.Parallel()
testCases := []struct {
in gopacket.Packet
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6Confirm(
t,
testHWUnknown,
newOptIANA(t, testIAID, testIPv6Unknown, 0),
),
name: "success",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
},
}, {
in: newDHCPv6Confirm(
t,
testHWDynamic,
newOptIANA(t, testIAID, testIPv6Dynamic, 0),
newOptIANA(t, testIAID+1, testIPv6Static, 0),
),
name: "success_multiple",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWDynamic),
},
}, {
in: newDHCPv6Confirm(
t,
testHWUnknown,
newOptIANA(t, testIAID, testIPv6OtherSubnet, 0),
),
name: "not_on_link",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptStatusCode(t, layers.DHCPv6StatusCodeNotOnLink),
},
}, {
in: newDHCPv6Confirm(
t,
testHWUnknown,
newOptIANA(t, testIAID, testIPv6Unknown, 0),
newOptIANA(t, testIAID+1, testIPv6OtherSubnet, 0),
),
name: "mixed",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptStatusCode(t, layers.DHCPv6StatusCodeNotOnLink),
},
}, {
in: newDHCPv6Confirm(t, testHWUnknown),
name: "no_iana",
wantOpts: nil,
}, {
in: newDHCPv6Confirm(
t,
testHWUnknown,
newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeSuccess),
),
name: "no_addrs",
wantOpts: nil,
}}
for _, tc := range testCases {
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV6)
startTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Interfaces: testIPv6InterfacesConf,
Logger: testLogger,
NetworkDeviceManager: ndMgr,
Enabled: true,
})
testutil.RequireSend(t, inCh, tc.in, testTimeout)
assertValidResponse6(t, req, outCh, tc.wantOpts)
})
}
}
// newDHCPv6Solicit creates a new DHCPv6 SOLICIT packet for testing.
func newDHCPv6Solicit(
tb testing.TB,
hwAddr net.HardwareAddr,
reqIP netip.Addr,
@ -406,7 +497,6 @@ func newDHCPv6SOLICIT(
tb.Helper()
eth := newEthernetLayer(tb, hwAddr, nil, layers.EthernetTypeIPv6)
ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
dhcp := &layers.DHCPv6{
@ -423,7 +513,7 @@ func newDHCPv6SOLICIT(
}
if reqIP.IsValid() && reqIP.Is6() {
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP))
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL))
}
if rapidCommit {
@ -434,8 +524,8 @@ func newDHCPv6SOLICIT(
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
}
// newDHCPv6REQUEST creates a new DHCPv6 REQUEST packet for testing.
func newDHCPv6REQUEST(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) {
// newDHCPv6Request creates a new DHCPv6 REQUEST packet for testing.
func newDHCPv6Request(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) {
tb.Helper()
eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6)
@ -456,12 +546,41 @@ func newDHCPv6REQUEST(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pk
}
if reqIP.IsValid() && reqIP.Is6() {
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP))
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL))
}
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
}
// newDHCPv6Confirm creates a new DHCPv6 CONFIRM packet for testing. addrs
// provides the addresses included within IA_NA options in the packet. If addrs
// is empty, the packet contains no IA_NA options.
func newDHCPv6Confirm(
tb testing.TB,
mac net.HardwareAddr,
ianas ...layers.DHCPv6Option,
) (pkt gopacket.Packet) {
tb.Helper()
eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6)
ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
dhcp := &layers.DHCPv6{
MsgType: layers.DHCPv6MsgTypeConfirm,
HopCount: 0,
LinkAddr: nil,
PeerAddr: nil,
TransactionID: testTransactionID,
Options: layers.DHCPv6Options{
newOptClientDUID(tb, mac),
},
}
dhcp.Options = append(dhcp.Options, ianas...)
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
}
// newIPv6UDPLayer creates IPv6 and UDP layers for testing. Invalid src is
// replaced with an unspecified address and client DHCPv6 port, invalid dst is
// replaced with the broadcast address and server DHCPv6 port.

View file

@ -12,17 +12,14 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
)
// macKey contains hardware address as byte array of 6, 8, or 20 bytes.
//
// TODO(e.burkov): Move to aghnet or even to netutil.
//
// TODO(e.burkov): Identify the client by the hardware address and the client
// identifier from the DHCP messages.
//
// TODO(e.burkov): Identify IPv6 clients with DUID.
// TODO(e.burkov): Identify IPv4 clients by the hardware address and the client
// identifier option. Identify IPv6 clients with DUID.
type macKey any
// macToKey converts mac into macKey, which is used as the key for the lease
@ -42,14 +39,15 @@ func macToKey(mac net.HardwareAddr) (key macKey) {
}
// netInterface is a common part of any interface within the DHCP server.
//
// TODO(e.burkov): Add other methods as [DHCPServer] evolves.
type netInterface struct {
// logger logs the events related to the network interface.
//
// TODO(e.burkov): Consider removing it and using the value from context.
logger *slog.Logger
// addressChecker checks if an address is available for leasing.
addressChecker addressChecker
// indexMu protects the index, leases, and leasedOffsets.
indexMu *sync.RWMutex
@ -134,7 +132,7 @@ func (iface *netInterface) removeLease(l *Lease) (err error) {
func (iface *netInterface) blockLease(
ctx context.Context,
l *Lease,
clock timeutil.Clock,
now time.Time,
) (err error) {
err = iface.removeLease(l)
if err != nil {
@ -143,7 +141,7 @@ func (iface *netInterface) blockLease(
l.HWAddr = blockedHardwareAddr
l.Hostname = ""
l.Expiry = clock.Now().Add(iface.leaseTTL)
l.Expiry = now.Add(iface.leaseTTL)
l.IsStatic = false
err = iface.index.dbStore(ctx)
@ -187,22 +185,24 @@ func (iface *netInterface) findExpiredLease(now time.Time) (l *Lease) {
// [netutil.ValidateMAC].
//
// TODO(e.burkov): Pass the precalculated macKey.
//
// TODO(e.burkov): Support allocating the exact requested address if it is
// available.
func (iface *netInterface) allocateLease(
ctx context.Context,
mac net.HardwareAddr,
checker addressChecker,
clock timeutil.Clock,
now time.Time,
) (lease *Lease, err error) {
key := macToKey(mac)
for {
lease, err = iface.reserveLease(ctx, mac, clock)
lease, err = iface.reserveLease(ctx, mac, now)
if err != nil {
return nil, err
}
var ok bool
ok, err = checker.IsAvailable(lease.IP)
ok, err = iface.addressChecker.IsAvailable(lease.IP)
if err != nil {
return nil, fmt.Errorf("checking address availability: %w", err)
}
@ -218,7 +218,7 @@ func (iface *netInterface) allocateLease(
iface.logger.DebugContext(ctx, "address not available", "ip", lease.IP)
err = iface.blockLease(ctx, lease, clock)
err = iface.blockLease(ctx, lease, now)
if err != nil {
return nil, fmt.Errorf("blocking unavailable address: %w", err)
}
@ -228,12 +228,10 @@ func (iface *netInterface) allocateLease(
// 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.
//
// TODO(e.burkov): Pass the time moment instead of clock.
func (iface *netInterface) reserveLease(
ctx context.Context,
mac net.HardwareAddr,
clock timeutil.Clock,
now time.Time,
) (lease *Lease, err error) {
// TODO(e.burkov): Limit the number of attempts.
nextIP := iface.nextIP()
@ -241,13 +239,13 @@ func (iface *netInterface) reserveLease(
lease = &Lease{
HWAddr: slices.Clone(mac),
IP: nextIP,
Expiry: clock.Now().Add(iface.leaseTTL),
Expiry: now.Add(iface.leaseTTL),
}
return lease, nil
}
lease = iface.findExpiredLease(clock.Now())
lease = iface.findExpiredLease(now)
if lease == nil {
return nil, errors.Error("no addresses available to lease")
}
@ -263,7 +261,7 @@ func (iface *netInterface) reserveLease(
lease.HWAddr = slices.Clone(mac)
lease.Hostname = ""
lease.IsStatic = false
lease.updateExpiry(clock, iface.leaseTTL)
lease.updateExpiry(now, iface.leaseTTL)
iface.leases[macToKey(mac)] = lease

View file

@ -6,15 +6,10 @@ import (
"net/netip"
"slices"
"time"
"github.com/AdguardTeam/golibs/timeutil"
)
// 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.
//
// BUG(e.burkov): The implementation currently relies on the client's hardware
@ -66,14 +61,23 @@ func (l *Lease) IsBlocked() (blocked bool) {
return bytes.Equal(l.HWAddr, blockedHardwareAddr)
}
// isExpiredAt returns true if the lease is expired at now. For static leases,
// it always returns false.
func (l *Lease) isExpiredAt(now time.Time) (ok bool) {
if l.IsStatic {
return false
}
return l.Expiry.Before(now)
}
// updateExpiry updates the lease expiry time if the current time is past the
// expiry. For static leases, this operation is a no-op.
func (l *Lease) updateExpiry(clock timeutil.Clock, ttl time.Duration) {
func (l *Lease) updateExpiry(now time.Time, ttl time.Duration) {
if l.IsStatic {
return
}
now := clock.Now()
if now.Before(l.Expiry) {
return
}

View file

@ -422,6 +422,17 @@ func clientIdentifier4(msg *layers.DHCPv4) (id []byte) {
return nil
}
// message4 returns the optional message from the DHCPv4 message, if any.
func message4(msg *layers.DHCPv4) (res string, ok bool) {
for _, opt := range msg.Options {
if opt.Type == layers.DHCPOptMessage && len(opt.Data) > 0 {
return string(opt.Data), true
}
}
return "", false
}
// requestedOptions4 returns the list of options requested in DHCPv4 message, if
// any.
//

View file

@ -11,31 +11,51 @@ import (
"github.com/gopacket/gopacket/layers"
)
// newOptStatusCode creates a top-level DHCPv6 Status Code option.
func newOptStatusCode(tb testing.TB, status layers.DHCPv6StatusCode) (opt layers.DHCPv6Option) {
tb.Helper()
const (
statusCodeLen = 2
)
data := make([]byte, 0, statusCodeLen)
data = binary.BigEndian.AppendUint16(data, uint16(status))
return layers.NewDHCPv6Option(layers.DHCPv6OptStatusCode, data)
}
// newOptIANA creates a DHCPv6 Identity Association for Non-temporary Address
// (3) option containing an IA Address with the specified IAID and requested IP
// address. reqIP must be a valid IPv6 address. The option will have the T1
// and T2 values set to the recommended values based on the [testLeaseTTL]
// constant, see the RFC reference in the
// [dhcpsvc.DHCPServer.newDHCPInterfaceV6].
func newOptIANA(tb testing.TB, iaid uint32, reqIP netip.Addr) (opt layers.DHCPv6Option) {
// address. The option will have the T1 and T2 values set to the recommended
// values based on ttl, see the RFC reference in the
// [dhcpsvc.DHCPServer.newDHCPInterfaceV6]. reqIP must be a valid IPv6 address.
func newOptIANA(
tb testing.TB,
iaid uint32,
reqIP netip.Addr,
ttl time.Duration,
) (opt layers.DHCPv6Option) {
tb.Helper()
iana := &dhcpsvc.IANAOption{
ID: iaid,
Nested: []dhcpsvc.IAAddrOption{{
PreferredLifetime: testLeaseTTL,
ValidLifetime: testLeaseTTL,
PreferredLifetime: ttl,
ValidLifetime: ttl,
Addr: reqIP,
}},
T1: testLeaseTTL / 2,
T2: testLeaseTTL * 4 / 5,
T1: ttl / 2,
T2: ttl * 4 / 5,
}
return iana.Encode()
}
// newOptIANAStatus creates a DHCPv6 IA_NA (3) option carrying only a nested
// Status Code option.
// Status Code option. If status is [layers.DHCPv6StatusCodeSuccess], the
// returned option will not contain a nested Status Code option, as per RFC 8415
// section 21.13.
func newOptIANAStatus(
tb testing.TB,
iaid uint32,
@ -64,12 +84,14 @@ func newOptIANAStatus(
data = binary.BigEndian.AppendUint32(data, 0)
data = binary.BigEndian.AppendUint32(data, 0)
// Nested Status Code option.
data = binary.BigEndian.AppendUint16(data, uint16(layers.DHCPv6OptStatusCode))
if status != layers.DHCPv6StatusCodeSuccess {
// Nested Status Code option.
data = binary.BigEndian.AppendUint16(data, uint16(layers.DHCPv6OptStatusCode))
// The length of the Status Code option data is 2 bytes.
data = binary.BigEndian.AppendUint16(data, 2)
data = binary.BigEndian.AppendUint16(data, uint16(status))
// The length of the Status Code option data is 2 bytes.
data = binary.BigEndian.AppendUint16(data, 2)
data = binary.BigEndian.AppendUint16(data, uint16(status))
}
return layers.NewDHCPv6Option(layers.DHCPv6OptIANA, data)
}

View file

@ -150,15 +150,8 @@ type dhcpInterfaceV4 struct {
common *netInterface
// clock used to get current time.
//
// TODO(e.burkov): Move to [netInterface].
clock timeutil.Clock
// addrChecker checks addresses for availability.
//
// TODO(e.burkov): Move to [netInterface].
addrChecker addressChecker
// gateway is the IP address of the network gateway.
gateway netip.Addr
@ -200,20 +193,19 @@ func (srv *DHCPServer) newDHCPInterfaceV4(
addrSpace, _ := newIPRange(conf.RangeStart, conf.RangeEnd)
iface = &dhcpInterfaceV4{
// TODO(e.burkov): Use an ICMP implementation.
addrChecker: noopAddressChecker{},
gateway: conf.GatewayIP,
clock: conf.Clock,
subnet: netip.PrefixFrom(conf.GatewayIP, maskLen),
gateway: conf.GatewayIP,
clock: conf.Clock,
subnet: netip.PrefixFrom(conf.GatewayIP, maskLen),
common: &netInterface{
logger: baseLogger,
indexMu: srv.leasesMu,
index: srv.leases,
leases: map[macKey]*Lease{},
leasedOffsets: newBitSet(),
name: name,
addrSpace: addrSpace,
leaseTTL: conf.LeaseDuration,
logger: baseLogger,
addressChecker: noopAddressChecker{},
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, baseLogger)

View file

@ -35,8 +35,6 @@ const (
// uint16.
//
// See https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml#arp-parameters-2.
//
// TODO(e.burkov): Use.
var HardwareTypeEthernet = []byte{0x00, 0x01}
// DHCPv6 multicast addresses.
@ -54,15 +52,14 @@ var (
)
// v6PrefLen 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 v6PrefLen = netutil.IPv6BitLen - 8
// IPv6Config is the interface-specific configuration for DHCPv6.
//
// TODO(e.burkov): Add RangeEnd and SubnetPrefix fields, and validate them.
// 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,
// so add RangeEnd and SubnetPrefix fields, and validate them.
type IPv6Config struct {
// Clock is used to get the current time. It should not be nil.
Clock timeutil.Clock
@ -108,23 +105,11 @@ func (c *IPv6Config) Validate() (err error) {
validate.Positive("lease duration", c.LeaseDuration),
}
errs = c.validateSubnet(errs)
return errors.Join(errs...)
}
// validateSubnet validates the subnet configuration.
//
// TODO(e.burkov): Use [validate].
func (c *IPv6Config) validateSubnet(orig []error) (errs []error) {
errs = orig
if !c.RangeStart.Is6() {
err := newMustErr("range start", "be a valid ipv6", c.RangeStart)
errs = append(errs, err)
errs = append(errs, fmt.Errorf("range start: %s: must be a valid ipv6", c.RangeStart))
}
return errs
return errors.Join(errs...)
}
// dhcpInterfaceV6 is a DHCP interface for IPv6 address family.
@ -134,16 +119,8 @@ type dhcpInterfaceV6 struct {
common *netInterface
// clock is used to get the current time.
//
// TODO(e.burkov): Move to [netInterface].
clock timeutil.Clock
// addrChecker checks if an address is available for leasing in current
// network.
//
// TODO(e.burkov): Move to [netInterface].
addrChecker addressChecker
// subnetPrefix is the network prefix of the interface's IPv6 subnet. It is
// used for on-link address determination.
subnetPrefix netip.Prefix
@ -190,29 +167,24 @@ func (srv *DHCPServer) newDHCPInterfaceV6(
return nil
}
// TODO(e.burkov): Migrate the configuration to use proper range start,
// end, and subnet prefix.
rangeEndData := conf.RangeStart.As16()
rangeEndData[15] = 0xff
// TODO(e.burkov): Validate the range end and subnet prefix against the
// range start during configuration validation.
addrSpace, _ := newIPRange(conf.RangeStart, netip.AddrFrom16(rangeEndData))
iface = &dhcpInterfaceV6{
common: &netInterface{
logger: l,
leases: map[macKey]*Lease{},
indexMu: srv.leasesMu,
index: srv.leases,
name: name,
addrSpace: addrSpace,
leasedOffsets: newBitSet(),
leaseTTL: conf.LeaseDuration,
logger: l,
addressChecker: noopAddressChecker{},
leases: map[macKey]*Lease{},
indexMu: srv.leasesMu,
index: srv.leases,
name: name,
addrSpace: addrSpace,
leasedOffsets: newBitSet(),
leaseTTL: conf.LeaseDuration,
},
clock: conf.Clock,
// TODO(e.burkov): Use an ICMP implementation.
addrChecker: noopAddressChecker{},
clock: conf.Clock,
subnetPrefix: netip.PrefixFrom(conf.RangeStart, v6PrefLen),
// Recommended values for T1 and T2 are 0.5 and 0.8 times the shortest
// preferred lifetime of the addresses in the IA that the server is
@ -234,11 +206,11 @@ func (srv *DHCPServer) newDHCPInterfaceV6(
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
type dhcpInterfacesV6 []*dhcpInterfaceV6
// find returns the first network interface within ifaces whose subnet prefix
// find returns the first network interface within ifaces whose address space
// contains ip. It returns false if there is no such interface.
func (ifaces dhcpInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV6) (contains bool) {
return iface.subnetPrefix.Contains(ip)
return iface.common.addrSpace.contains(ip)
})
if i < 0 {
return nil, false
@ -424,9 +396,6 @@ func respond6(fd *frameData6, resp *layers.DHCPv6) (err error) {
// leasing. mac must be a valid MAC address according to [netutil.ValidateMAC],
// req must be a valid DHCPv6 message of SOLICIT type, iface.common.indexMu
// must be locked.
//
// TODO(e.burkov): Support allocating several leases at a time when the
// database will migrate, see the BUG at [Lease]'s documentation.
func (iface *dhcpInterfaceV6) allocateForSolicit(
ctx context.Context,
mac net.HardwareAddr,
@ -443,7 +412,6 @@ func (iface *dhcpInterfaceV6) allocateForSolicit(
var iana IANAOption
err := iana.UnmarshalBinary(reqOpt.Data)
if err != nil {
// TODO(e.burkov): Recheck the logic on malformed IA_NA options.
l.DebugContext(ctx, "malformed ia_na", "idx", i, slogutil.KeyError, err)
continue
@ -454,9 +422,7 @@ func (iface *dhcpInterfaceV6) allocateForSolicit(
return lease, iana.ID
}
// TODO(e.burkov): Support allocating the exact requested address if it
// is available.
lease, err = iface.common.allocateLease(ctx, mac, iface.addrChecker, iface.clock)
lease, err = iface.common.allocateLease(ctx, mac, iface.clock.Now())
if err != nil {
l.DebugContext(ctx, "no address available", "iaid", iana.ID, slogutil.KeyError, err)
@ -473,9 +439,6 @@ func (iface *dhcpInterfaceV6) allocateForSolicit(
// firstIANA returns the first valid IA_NA option in req. It returns false if
// there is no such option. req must not be nil.
//
// TODO(e.burkov): Support handling several IA_NA options at a time when the
// database will migrate, see the BUG at [Lease]'s documentation.
func (iface *dhcpInterfaceV6) firstIANA(
ctx context.Context,
req *layers.DHCPv6,
@ -501,6 +464,43 @@ func (iface *dhcpInterfaceV6) firstIANA(
return nil, false
}
// confirmAddrsOnLink checks whether every address in every IA_NA option of req
// is appropriate for the link, i.e., lies within iface.subnetPrefix. It
// returns true in hasAddrs if at least one address was found across all IA_NA
// options. If all addresses are on-link, allOnLink is true. req must be a
// valid DHCPv6 message of CONFIRM type.
//
// See RFC 9915 Section 18.3.3.
func (iface *dhcpInterfaceV6) confirmAddrsOnLink(
ctx context.Context,
req *layers.DHCPv6,
) (allOnLink, hasAddrs bool) {
logger := iface.common.logger
for i, reqOpt := range req.Options {
if reqOpt.Code != layers.DHCPv6OptIANA {
continue
}
var iana IANAOption
err := iana.UnmarshalBinary(reqOpt.Data)
if err != nil {
logger.DebugContext(ctx, "malformed ia_na", "idx", i, slogutil.KeyError, err)
continue
}
for _, addr := range iana.Nested {
hasAddrs = true
if !iface.common.addrSpace.contains(addr.Addr) {
return false, true
}
}
}
return true, hasAddrs
}
// newSolicitRespOpts returns the common option list for Advertise and
// rapid-commit Reply responses to a Solicit request. Zero iaid creates an
// option with Status Code NoAddrsAvail. rapidCommit defines whether the
@ -567,6 +567,34 @@ func (iface *dhcpInterfaceV6) newRequestRespOpts(
return iface.appendRequestedOptions(opts, req)
}
// newConfirmRespOpts returns the common option list for Reply responses to a
// Confirm message. fd and cliID must not be nil. If status is
// [layers.DHCPv6StatusCodeSuccess], the response will not include a Status Code
// option.
//
// See RFC 9915 Section 18.3.3.
func (iface *dhcpInterfaceV6) newConfirmRespOpts(
fd *frameData6,
cliID *layers.DHCPv6DUID,
status layers.DHCPv6StatusCode,
) (opts layers.DHCPv6Options) {
opts = append(
opts,
layers.NewDHCPv6Option(layers.DHCPv6OptServerID, fd.duidData),
layers.NewDHCPv6Option(layers.DHCPv6OptClientID, cliID.Encode()),
)
// If the Status Code option does not appear in a message in which the
// option could appear, the status of the message is assumed to be Success.
//
// See RFC 9915 Section 21.13.
if status != layers.DHCPv6StatusCodeSuccess {
opts = append(opts, newStatusCodeOption(status))
}
return opts
}
// iaNAFromLease returns an IA_NA option with a single IA Address sub-option
// corresponding to lease and with the given iaid. The T1 and T2 values are set
// according to iface.t1 and iface.t2. If lease is nil, it returns an IA_NA
@ -591,9 +619,6 @@ func (iface *dhcpInterfaceV6) iaNAFromLease(lease *Lease, iaid uint32) (iana lay
// leaseForRequest returns the committed lease for req. It reuses an already
// reserved lease for the client when possible, or allocates and commits the new
// address. iface.common.indexMu must be locked.
//
// TODO(e.burkov): Support committing several leases at a time when the
// database will migrate, see the BUG at [Lease]'s documentation.
func (iface *dhcpInterfaceV6) leaseForRequest(
ctx context.Context,
req *layers.DHCPv6,
@ -604,7 +629,7 @@ func (iface *dhcpInterfaceV6) leaseForRequest(
lease, ok := iface.common.leases[key]
if !ok {
lease, err = iface.common.allocateLease(ctx, mac, iface.addrChecker, iface.clock)
lease, err = iface.common.allocateLease(ctx, mac, iface.clock.Now())
if err != nil {
return nil, fmt.Errorf("allocating lease for mac %s: %w", mac, err)
}
@ -626,9 +651,6 @@ func (iface *dhcpInterfaceV6) leaseForRequest(
// deallocates the lease if the one fails to be committed. lease must be
// non-nil and allocated for the client corresponding to req,
// iface.common.indexMu mutex must be locked.
//
// TODO(e.burkov): Support committing several leases at a time when the
// database will migrate, see the BUG at [Lease]'s documentation.
func (iface *dhcpInterfaceV6) commit(
ctx context.Context,
req *layers.DHCPv6,
@ -648,9 +670,8 @@ func (iface *dhcpInterfaceV6) commit(
l.DebugContext(ctx, "updated lease hostname", "hostname", hostname, "ip", lease.IP)
}
// TODO(e.burkov): Add the Lease.isExpired. method.
if exp := lease.Expiry; !exp.IsZero() && exp.Before(iface.clock.Now()) {
lease.updateExpiry(iface.clock, iface.common.leaseTTL)
if now := iface.clock.Now(); lease.isExpiredAt(now) {
lease.updateExpiry(now, iface.common.leaseTTL)
l.DebugContext(ctx, "updated lease expiry", "expires", lease.Expiry, "ip", lease.IP)
}