mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-11-03 02:27:04 +09:00
Pull request 2739: AGDNS-3863-gopacket-dhcp-vol.33
Updates #4923. Squashed commit of the following: commit a0d7998b4bfa837a8d8c19e3ed7cfc3da16b7181 Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Aug 3 14:33:41 2026 +0300 dhcpsvc: imp code, docs commit 028c1ff83198e915fd320983eaea5cace5787325 Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Jul 31 18:57:36 2026 +0300 dhcpsvc: handle information request commit 0d3f2888508eb462ec5ec9f7a29c3a85c917d16d Author: Eugene Burkov <e.burkov@adguard.com> Date: Thu Jul 30 15:43:09 2026 +0300 dhcpsvc: handle rebind
This commit is contained in:
1 parent
5f6cb57df9
commit
091d9287fc
3 files changed
+297
-33
No files matched your search
@@ -274,24 +274,27 @@ func (iface *dhcpInterfaceV6) handleRenew(
|
||||
// handler does.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
resp.Options = iface.newRenewRespOpts(fd, req, cliID, layers.DHCPv6Option{})
|
||||
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{})
|
||||
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
ianaOpt := iface.ianaForRenew(ctx, req, iana, fd.ether.SrcMAC)
|
||||
resp.Options = iface.newRenewRespOpts(fd, req, cliID, ianaOpt)
|
||||
ianaOpt := iface.ianaForUpdate(ctx, req, iana, fd.ether.SrcMAC)
|
||||
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, ianaOpt)
|
||||
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
// handleRebind handles messages of type REBIND. req must not be nil and must
|
||||
// be a valid DHCPv6 message of type REBIND. fd must be valid.
|
||||
// be a valid DHCPv6 message of type REBIND, fd must be valid.
|
||||
//
|
||||
// TODO(e.burkov): Implement. This is a stub for now.
|
||||
// TODO(e.burkov): The current implementation rebinds only the first valid
|
||||
// IA_NA option. It does not verify that the addresses in the IA match the
|
||||
// stored lease, since clients are identified by MAC address rather than
|
||||
// DUID+IAID.
|
||||
func (iface *dhcpInterfaceV6) handleRebind(
|
||||
ctx context.Context,
|
||||
_ *frameData6,
|
||||
fd *frameData6,
|
||||
req *layers.DHCPv6,
|
||||
) (err error) {
|
||||
cliID, err := clientIDNoServer(req.Options)
|
||||
@@ -302,31 +305,54 @@ func (iface *dhcpInterfaceV6) handleRebind(
|
||||
l := iface.common.logger
|
||||
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
|
||||
|
||||
return nil
|
||||
iface.common.indexMu.Lock()
|
||||
defer iface.common.indexMu.Unlock()
|
||||
|
||||
resp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeReply,
|
||||
TransactionID: req.TransactionID,
|
||||
}
|
||||
|
||||
iana, ok := iface.firstIANA(ctx, req)
|
||||
if !ok {
|
||||
// With no IA_NA options and no requested addresses there's nothing to
|
||||
// rebind. Respond with no IA options similarly to how the Renew
|
||||
// handler does.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.5.
|
||||
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{})
|
||||
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
ianaOpt := iface.ianaForUpdate(ctx, req, iana, fd.ether.SrcMAC)
|
||||
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, ianaOpt)
|
||||
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
// handleInfo handles messages of type INFORMATION-REQUEST. req must not be nil
|
||||
// and must be a valid DHCPv6 message of type INFORMATION-REQUEST. fd must be
|
||||
// valid.
|
||||
//
|
||||
// TODO(e.burkov): Implement. This is a stub for now.
|
||||
// TODO(e.burkov): The current implementation does not handle relay-forwarded
|
||||
// INFORMATION-REQUEST messages.
|
||||
func (iface *dhcpInterfaceV6) handleInfo(
|
||||
ctx context.Context,
|
||||
fd *frameData6,
|
||||
req *layers.DHCPv6,
|
||||
) (err error) {
|
||||
if srvID, ok := findOption6(req.Options, layers.DHCPv6OptServerID); ok {
|
||||
if !bytes.Equal(srvID, fd.duidData) {
|
||||
return fmt.Errorf(
|
||||
"dhcpv6: server id: got %v, want %v: %w",
|
||||
srvID,
|
||||
fd.duidData,
|
||||
errors.ErrNotEqual,
|
||||
)
|
||||
}
|
||||
srvID, ok := findOption6(req.Options, layers.DHCPv6OptServerID)
|
||||
if ok && !bytes.Equal(srvID, fd.duidData) {
|
||||
return fmt.Errorf(
|
||||
"dhcpv6: server id: got %v, want %v: %w",
|
||||
srvID,
|
||||
fd.duidData,
|
||||
errors.ErrNotEqual,
|
||||
)
|
||||
}
|
||||
|
||||
_, ok := findOption6(req.Options, layers.DHCPv6OptIANA)
|
||||
_, ok = findOption6(req.Options, layers.DHCPv6OptIANA)
|
||||
if ok {
|
||||
return fmt.Errorf("dhcpv6: %s: ia option: %w", req.MsgType, errors.ErrUnexpectedValue)
|
||||
}
|
||||
@@ -339,7 +365,13 @@ func (iface *dhcpInterfaceV6) handleInfo(
|
||||
l := iface.common.logger
|
||||
l.DebugContext(ctx, "handling message", "type", req.MsgType)
|
||||
|
||||
return nil
|
||||
resp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeReply,
|
||||
TransactionID: req.TransactionID,
|
||||
Options: iface.newInfoRespOpts(fd, req),
|
||||
}
|
||||
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
// handleRelease handles messages of type RELEASE. req must not be nil and must
|
||||
|
||||
@@ -524,6 +524,7 @@ func TestDHCPServer_ServeEther6_renew(t *testing.T) {
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
@@ -573,6 +574,152 @@ func TestDHCPServer_ServeEther6_renew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHCPServer_ServeEther6_rebind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
in gopacket.Packet
|
||||
want *dhcpsvc.Lease
|
||||
name string
|
||||
wantOpts layers.DHCPv6Options
|
||||
}{{
|
||||
in: newDHCPv6Rebind(t, testHWDynamic, testIPv6Dynamic),
|
||||
name: "success",
|
||||
want: testLease6Dynamic,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWDynamic),
|
||||
newOptIANA(t, testIAID, testIPv6Dynamic, testLeaseTTL),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Rebind(t, testHWStatic, testIPv6Static),
|
||||
name: "success_static",
|
||||
want: testLease6Static,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWStatic),
|
||||
newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Rebind(t, testHWUnknown, testIPv6Unknown),
|
||||
name: "no_binding",
|
||||
want: nil,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Rebind(t, testHWUnknown, netip.Addr{}),
|
||||
name: "no_iana",
|
||||
want: nil,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
|
||||
|
||||
db := newTestDatabase(t, testLeases)
|
||||
|
||||
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
|
||||
assert.Contains(t, leases, tc.want)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if tc.want != nil {
|
||||
db.onStore = onStore
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHCPServer_ServeEther6_info(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
in gopacket.Packet
|
||||
name string
|
||||
wantOpts layers.DHCPv6Options
|
||||
}{{
|
||||
in: newDHCPv6Info(t, testHWUnknown, true, true),
|
||||
name: "cli_and_srv",
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Info(t, testHWUnknown, false, true),
|
||||
name: "srv_only",
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Info(t, testHWUnknown, true, false),
|
||||
name: "cli_only",
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Info(t, testHWUnknown, false, false),
|
||||
name: "no_opts",
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
},
|
||||
}}
|
||||
|
||||
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,
|
||||
@@ -797,6 +944,71 @@ func assertValidDHCPv6(
|
||||
}
|
||||
}
|
||||
|
||||
// newDHCPv6Rebind creates a new DHCPv6 REBIND packet for testing.
|
||||
func newDHCPv6Rebind(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
// REBIND is sent to any available server, so the destination is the
|
||||
// multicast address, not a specific server's unicast.
|
||||
eth := newEthernetLayer(tb, mac, nil, layers.EthernetTypeIPv6)
|
||||
ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
|
||||
|
||||
dhcp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeRebind,
|
||||
HopCount: 0,
|
||||
// Don't specify link and peer addresses, as they are intended for relay
|
||||
// messages.
|
||||
LinkAddr: nil,
|
||||
PeerAddr: nil,
|
||||
TransactionID: testTransactionID,
|
||||
Options: layers.DHCPv6Options{
|
||||
newOptClientDUID(tb, mac),
|
||||
// REBIND must not contain a Server ID option.
|
||||
},
|
||||
}
|
||||
|
||||
if reqIP.IsValid() && reqIP.Is6() {
|
||||
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL))
|
||||
}
|
||||
|
||||
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
|
||||
}
|
||||
|
||||
// newDHCPv6Info creates a new DHCPv6 INFORMATION-REQUEST packet for testing.
|
||||
// withClientID controls whether the packet includes a Client Identifier option.
|
||||
func newDHCPv6Info(
|
||||
tb testing.TB,
|
||||
mac net.HardwareAddr,
|
||||
addClientID bool,
|
||||
addServerID bool,
|
||||
) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
eth := newEthernetLayer(tb, mac, nil, layers.EthernetTypeIPv6)
|
||||
ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
|
||||
|
||||
dhcp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeInformationRequest,
|
||||
HopCount: 0,
|
||||
// Don't specify link and peer addresses, as they are intended for relay
|
||||
// messages.
|
||||
LinkAddr: nil,
|
||||
PeerAddr: nil,
|
||||
TransactionID: testTransactionID,
|
||||
Options: layers.DHCPv6Options{},
|
||||
}
|
||||
|
||||
if addClientID {
|
||||
dhcp.Options = append(dhcp.Options, newOptClientDUID(tb, mac))
|
||||
}
|
||||
|
||||
if addServerID {
|
||||
dhcp.Options = append(dhcp.Options, newOptServerDUID(tb, testIfaceHWAddr))
|
||||
}
|
||||
|
||||
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
|
||||
}
|
||||
|
||||
// newDHCPv6Renew creates a new DHCPv6 RENEW packet for testing.
|
||||
func newDHCPv6Renew(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
+34
-14
@@ -595,14 +595,12 @@ func (iface *dhcpInterfaceV6) newConfirmRespOpts(
|
||||
return iface.appendRequestedOptions(opts, req)
|
||||
}
|
||||
|
||||
// newRenewRespOpts returns the common option list for Reply responses to a
|
||||
// Renew message. fd, req, and cliID must not be nil. iana must be a valid
|
||||
// IA_NA option, or be empty if the response should not contain an IA_NA option.
|
||||
// newUpdateRespOpts returns the common option list for Reply responses to a
|
||||
// RENEW or REBIND messages. fd, req, and cliID must not be nil. iana must be
|
||||
// a valid IA_NA option, or empty if the response should not include one.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
//
|
||||
// TODO(e.burkov): DRY with other response option builders.
|
||||
func (iface *dhcpInterfaceV6) newRenewRespOpts(
|
||||
func (iface *dhcpInterfaceV6) newUpdateRespOpts(
|
||||
fd *frameData6,
|
||||
req *layers.DHCPv6,
|
||||
cliID *layers.DHCPv6DUID,
|
||||
@@ -625,6 +623,28 @@ func (iface *dhcpInterfaceV6) newRenewRespOpts(
|
||||
return iface.appendRequestedOptions(opts, req)
|
||||
}
|
||||
|
||||
// newInfoRespOpts returns the option list for a Reply to an INFORMATION-REQUEST
|
||||
// message. The Client Identifier option is echoed back only if the request
|
||||
// contained one. fd and req must not be nil.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.6.
|
||||
func (iface *dhcpInterfaceV6) newInfoRespOpts(
|
||||
fd *frameData6,
|
||||
req *layers.DHCPv6,
|
||||
) (opts layers.DHCPv6Options) {
|
||||
opts = append(opts, layers.NewDHCPv6Option(layers.DHCPv6OptServerID, fd.duidData))
|
||||
|
||||
// Client ID is optional in INFORMATION-REQUEST but must be echoed if
|
||||
// present.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.6.
|
||||
if cliIDData, ok := clientDUID6(req.Options); ok {
|
||||
opts = append(opts, layers.NewDHCPv6Option(layers.DHCPv6OptClientID, cliIDData))
|
||||
}
|
||||
|
||||
return iface.appendRequestedOptions(opts, req)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -679,12 +699,12 @@ func (iface *dhcpInterfaceV6) ianaForRequest(
|
||||
return iface.iaNAFromLease(lease, iaid)
|
||||
}
|
||||
|
||||
// ianaForRenew returns the IANA filled with committed lease data for req. It
|
||||
// ianaForUpdate returns the IANA filled with committed lease data for req. It
|
||||
// reuses an already reserved lease for the client, if it exists. req must be a
|
||||
// valid DHCPv6 message of type RENEW, iaid must not be zero, and mac must be a
|
||||
// valid MAC address according to [netutil.ValidateMAC]. iface.common.indexMu
|
||||
// must be locked.
|
||||
func (iface *dhcpInterfaceV6) ianaForRenew(
|
||||
// valid DHCPv6 message of type RENEW or REBIND, iaid must not be zero, and mac
|
||||
// must be a valid MAC address according to [netutil.ValidateMAC].
|
||||
// iface.common.indexMu must be locked.
|
||||
func (iface *dhcpInterfaceV6) ianaForUpdate(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv6,
|
||||
reqIANA *IANAOption,
|
||||
@@ -698,7 +718,7 @@ func (iface *dhcpInterfaceV6) ianaForRenew(
|
||||
// With no requested addresses there's nothing to renew. Respond with
|
||||
// no IA options similarly to how the Request handler does.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
// See RFC 9915 Section 18.3.4 and 18.3.5.
|
||||
return layers.DHCPv6Option{}
|
||||
}
|
||||
|
||||
@@ -707,8 +727,8 @@ func (iface *dhcpInterfaceV6) ianaForRenew(
|
||||
// No binding found for this client. The server returns the IA with a
|
||||
// NoBinding status code.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
return layers.DHCPv6Option{}
|
||||
// See RFC 9915 Section 18.3.4 and 18.3.5.
|
||||
return newIANAWithStatus(reqIANA.ID, layers.DHCPv6StatusCodeNoBinding)
|
||||
}
|
||||
|
||||
err := iface.commit(ctx, req, lease)
|
||||
|
||||
Reference in new issue
Block a user