mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-08-04 15:28:58 +00:00
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
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:8ae926b76254e17dabAuthor: 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 commit8ae926b76cMerge:55e177638336e9c9dfAuthor: 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 commit55e177638cAuthor: Eugene Burkov <e.burkov@adguard.com> Date: Mon Jul 27 16:38:51 2026 +0300 dhcspvc: imp docs commit9088177898Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Jul 27 15:57:27 2026 +0300 dhcpsvc: imp code commit94529fe836Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Jul 24 18:11:46 2026 +0300 dhcpsvc: imp code commitc0c26d4ce1Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Jul 24 18:00:58 2026 +0300 dhcpsvc: handle confirm
This commit is contained in:
parent
254e17dabf
commit
8e56afa92e
12 changed files with 426 additions and 228 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue