Skip to content

Commit 369d476

Browse files
committed
Pull request 2741: AGDNS-3863-gopacket-dhcp-vol.34
Updates #4923. Squashed commit of the following: commit ac803e0 Merge: 7c42638 b0e930c Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Aug 4 19:35:57 2026 +0300 Merge branch 'master' into AGDNS-3863-gopacket-dhcp-vol.34 commit 7c42638 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Aug 4 19:09:44 2026 +0300 dhcpsvc: don't consider zero iaid invalid commit d883e64 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Aug 4 17:35:56 2026 +0300 dhcpsvc: imp maint commit 947a9b3 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Aug 4 15:35:14 2026 +0300 dhcpsvc: impl dhcpv6 release
1 parent b0e930c commit 369d476

7 files changed

Lines changed: 249 additions & 53 deletions

File tree

internal/dhcpsvc/dhcpsvc_test.go

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -393,27 +393,19 @@ func (db *testDatabase) Store(ctx context.Context, leases []*dhcpsvc.Lease) (err
393393
return db.onStore(ctx, leases)
394394
}
395395

396-
// newTestDatabase creates a new *testDatabase for testing. If initial is not
397-
// nil, db.Load is set to return it. By default, db.Store panics on any call.
398-
func newTestDatabase(tb testing.TB, initial []*dhcpsvc.Lease) (db *testDatabase) {
396+
// newTestDatabase creates a new *testDatabase for testing. The Load method
397+
// returns [testLeases], and the Store method panics.
398+
func newTestDatabase(tb testing.TB) (db *testDatabase) {
399399
tb.Helper()
400400

401-
db = &testDatabase{
402-
onLoad: func(ctx context.Context) (_ []*dhcpsvc.Lease, _ error) {
403-
panic(testutil.UnexpectedCall(ctx))
401+
return &testDatabase{
402+
onLoad: func(_ context.Context) (leases []*dhcpsvc.Lease, err error) {
403+
return testLeases, nil
404404
},
405405
onStore: func(ctx context.Context, leases []*dhcpsvc.Lease) (_ error) {
406406
panic(testutil.UnexpectedCall(ctx, leases))
407407
},
408408
}
409-
410-
if initial != nil {
411-
db.onLoad = func(ctx context.Context) (leases []*dhcpsvc.Lease, err error) {
412-
return initial, nil
413-
}
414-
}
415-
416-
return db
417409
}
418410

419411
// newTestDHCPServer creates a new DHCPServer for testing. It uses the default

internal/dhcpsvc/handler4_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func TestDHCPServer_ServeEther4_discover(t *testing.T) {
8383

8484
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV4)
8585
startTestDHCPServer(t, &dhcpsvc.Config{
86-
Database: newTestDatabase(t, testLeases),
86+
Database: newTestDatabase(t),
8787
Interfaces: testIPv4InterfacesConf,
8888
NetworkDeviceManager: ndMgr,
8989
Enabled: true,
@@ -105,7 +105,7 @@ func TestDHCPServer_ServeEther4_discoverExpired(t *testing.T) {
105105
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV4)
106106

107107
startTestDHCPServer(t, &dhcpsvc.Config{
108-
Database: newTestDatabase(t, testLeases),
108+
Database: newTestDatabase(t),
109109
Interfaces: testIPv4InterfacesConf,
110110
NetworkDeviceManager: ndMgr,
111111
Enabled: true,
@@ -156,7 +156,7 @@ func TestDHCPServer_ServeEther4_release(t *testing.T) {
156156
t.Run(tc.name, func(t *testing.T) {
157157
t.Parallel()
158158

159-
db := newTestDatabase(t, testLeases)
159+
db := newTestDatabase(t)
160160

161161
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
162162
assert.NotContains(testutil.NewPanicT(t), leases, tc.want)
@@ -262,7 +262,7 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
262262
t.Run(tc.name, func(t *testing.T) {
263263
t.Parallel()
264264

265-
db := newTestDatabase(t, testLeases)
265+
db := newTestDatabase(t)
266266

267267
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
268268
assert.Contains(t, leases, tc.want)
@@ -311,7 +311,7 @@ func TestDHCPServer_ServeEther4_requestSelectingNoLease(t *testing.T) {
311311
flags: dhcpsvc.FlagsBroadcast,
312312
})
313313

314-
db := newTestDatabase(t, testLeases)
314+
db := newTestDatabase(t)
315315

316316
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV4)
317317
startTestDHCPServer(t, &dhcpsvc.Config{
@@ -413,7 +413,7 @@ func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
413413
t.Run(tc.name, func(t *testing.T) {
414414
t.Parallel()
415415

416-
db := newTestDatabase(t, testLeases)
416+
db := newTestDatabase(t)
417417

418418
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
419419
assert.Contains(t, leases, tc.want)
@@ -513,7 +513,7 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
513513
t.Run(tc.name, func(t *testing.T) {
514514
t.Parallel()
515515

516-
db := newTestDatabase(t, testLeases)
516+
db := newTestDatabase(t)
517517

518518
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
519519
assert.Contains(t, leases, tc.want)
@@ -586,7 +586,7 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
586586
}
587587

588588
startTestDHCPServer(t, &dhcpsvc.Config{
589-
Database: newTestDatabase(t, testLeases),
589+
Database: newTestDatabase(t),
590590
Interfaces: testIPv4InterfacesConf,
591591
NetworkDeviceManager: ndMgr,
592592
Enabled: true,
@@ -641,7 +641,7 @@ func TestDHCPServer_ServeEther4_decline(t *testing.T) {
641641
t.Run(tc.name, func(t *testing.T) {
642642
t.Parallel()
643643

644-
db := newTestDatabase(t, testLeases)
644+
db := newTestDatabase(t)
645645

646646
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
647647
assert.Contains(t, leases, wantLease)

internal/dhcpsvc/handler6.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"net/netip"
78

89
"github.com/AdguardTeam/golibs/errors"
910
"github.com/AdguardTeam/golibs/logutil/slogutil"
@@ -377,7 +378,8 @@ func (iface *dhcpInterfaceV6) handleInfo(
377378
// handleRelease handles messages of type RELEASE. req must not be nil and must
378379
// be a valid DHCPv6 message of type RELEASE. fd must be valid.
379380
//
380-
// TODO(e.burkov): Implement. This is a stub for now.
381+
// TODO(e.burkov): Verify all IA_NA options instead of handling only the first
382+
// one.
381383
func (iface *dhcpInterfaceV6) handleRelease(
382384
ctx context.Context,
383385
fd *frameData6,
@@ -391,7 +393,43 @@ func (iface *dhcpInterfaceV6) handleRelease(
391393
l := iface.common.logger
392394
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
393395

394-
return nil
396+
resp := &layers.DHCPv6{
397+
MsgType: layers.DHCPv6MsgTypeReply,
398+
TransactionID: req.TransactionID,
399+
}
400+
401+
iaid, ip := iface.ipForRelease(ctx, req)
402+
if ip == (netip.Addr{}) {
403+
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{})
404+
405+
return respond6(fd, resp)
406+
}
407+
408+
key := macToKey(fd.ether.SrcMAC)
409+
410+
iface.common.indexMu.Lock()
411+
defer iface.common.indexMu.Unlock()
412+
413+
lease, hasLease := iface.common.leases[key]
414+
if !hasLease || lease.IP != ip {
415+
respIANA := newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoBinding)
416+
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA)
417+
418+
return respond6(fd, resp)
419+
}
420+
421+
err = iface.common.index.remove(ctx, lease, iface.common)
422+
if err != nil {
423+
l.ErrorContext(ctx, "removing lease", slogutil.KeyError, err)
424+
425+
respIANA := newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoBinding)
426+
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA)
427+
} else {
428+
respIANA := &IANAOption{ID: iaid}
429+
resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA.Encode())
430+
}
431+
432+
return respond6(fd, resp)
395433
}
396434

397435
// handleDecline handles messages of type DECLINE. req must not be nil and must

internal/dhcpsvc/handler6_test.go

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
9191
t.Run(tc.name, func(t *testing.T) {
9292
t.Parallel()
9393

94-
db := newTestDatabase(t, testLeases)
94+
db := newTestDatabase(t)
9595

9696
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV6)
9797
startTestDHCPServer(t, &dhcpsvc.Config{
@@ -185,7 +185,7 @@ func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) {
185185
t.Run(tc.name, func(t *testing.T) {
186186
t.Parallel()
187187

188-
db := newTestDatabase(t, testLeases)
188+
db := newTestDatabase(t)
189189

190190
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
191191
assert.Contains(t, leases, tc.want)
@@ -280,7 +280,7 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
280280
t.Run(tc.name, func(t *testing.T) {
281281
t.Parallel()
282282

283-
db := newTestDatabase(t, testLeases)
283+
db := newTestDatabase(t)
284284

285285
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
286286
assert.Contains(t, leases, tc.want)
@@ -361,7 +361,7 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) {
361361
t.Run(tc.name, func(t *testing.T) {
362362
t.Parallel()
363363

364-
db := newTestDatabase(t, testLeases)
364+
db := newTestDatabase(t)
365365

366366
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
367367
assert.Contains(t, leases, tc.want)
@@ -469,7 +469,7 @@ func TestDHCPServer_ServeEther6_confirm(t *testing.T) {
469469
t.Run(tc.name, func(t *testing.T) {
470470
t.Parallel()
471471

472-
db := newTestDatabase(t, testLeases)
472+
db := newTestDatabase(t)
473473

474474
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV6)
475475
startTestDHCPServer(t, &dhcpsvc.Config{
@@ -543,7 +543,7 @@ func TestDHCPServer_ServeEther6_renew(t *testing.T) {
543543
for _, tc := range testCases {
544544
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
545545

546-
db := newTestDatabase(t, testLeases)
546+
db := newTestDatabase(t)
547547

548548
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
549549
assert.Contains(t, leases, tc.want)
@@ -630,7 +630,7 @@ func TestDHCPServer_ServeEther6_rebind(t *testing.T) {
630630
for _, tc := range testCases {
631631
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
632632

633-
db := newTestDatabase(t, testLeases)
633+
db := newTestDatabase(t)
634634

635635
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
636636
assert.Contains(t, leases, tc.want)
@@ -699,10 +699,108 @@ func TestDHCPServer_ServeEther6_info(t *testing.T) {
699699
for _, tc := range testCases {
700700
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
701701

702+
db := newTestDatabase(t)
703+
702704
t.Run(tc.name, func(t *testing.T) {
703705
t.Parallel()
704706

705-
db := newTestDatabase(t, testLeases)
707+
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV6)
708+
startTestDHCPServer(t, &dhcpsvc.Config{
709+
Database: db,
710+
Interfaces: testIPv6InterfacesConf,
711+
Logger: testLogger,
712+
NetworkDeviceManager: ndMgr,
713+
Enabled: true,
714+
})
715+
716+
testutil.RequireSend(t, inCh, tc.in, testTimeout)
717+
718+
assertValidResponse6(t, req, outCh, tc.wantOpts)
719+
})
720+
}
721+
}
722+
723+
func TestDHCPServer_ServeEther6_release(t *testing.T) {
724+
t.Parallel()
725+
726+
testCases := []struct {
727+
in gopacket.Packet
728+
want *dhcpsvc.Lease
729+
name string
730+
wantOpts layers.DHCPv6Options
731+
}{{
732+
in: newDHCPv6Release(t, testHWDynamic, testIPv6Dynamic),
733+
want: testLease6Dynamic,
734+
name: "success",
735+
wantOpts: layers.DHCPv6Options{
736+
newOptServerDUID(t, testIfaceHWAddr),
737+
newOptClientDUID(t, testHWDynamic),
738+
newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeSuccess),
739+
newOptPreference(t, 0),
740+
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
741+
},
742+
}, {
743+
in: newDHCPv6Release(t, testHWStatic, testIPv6Static),
744+
want: testLease6Static,
745+
name: "success_static",
746+
wantOpts: layers.DHCPv6Options{
747+
newOptServerDUID(t, testIfaceHWAddr),
748+
newOptClientDUID(t, testHWStatic),
749+
newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeSuccess),
750+
newOptPreference(t, 0),
751+
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
752+
},
753+
}, {
754+
in: newDHCPv6Release(t, testHWUnknown, testIPv6Unknown),
755+
want: nil,
756+
name: "no_binding",
757+
wantOpts: layers.DHCPv6Options{
758+
newOptServerDUID(t, testIfaceHWAddr),
759+
newOptClientDUID(t, testHWUnknown),
760+
newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding),
761+
newOptPreference(t, 0),
762+
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
763+
},
764+
}, {
765+
in: newDHCPv6Release(t, testHWDynamic, netip.Addr{}),
766+
want: nil,
767+
name: "no_iana",
768+
wantOpts: layers.DHCPv6Options{
769+
newOptServerDUID(t, testIfaceHWAddr),
770+
newOptClientDUID(t, testHWDynamic),
771+
newOptPreference(t, 0),
772+
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
773+
},
774+
}, {
775+
in: newDHCPv6Release(t, testHWDynamic, testIPv6Unknown),
776+
want: nil,
777+
name: "ip_mismatch",
778+
wantOpts: layers.DHCPv6Options{
779+
newOptServerDUID(t, testIfaceHWAddr),
780+
newOptClientDUID(t, testHWDynamic),
781+
newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding),
782+
newOptPreference(t, 0),
783+
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
784+
},
785+
}}
786+
787+
for _, tc := range testCases {
788+
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
789+
790+
db := newTestDatabase(t)
791+
792+
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
793+
assert.NotContains(t, leases, tc.want)
794+
795+
return nil
796+
}
797+
798+
if tc.want != nil {
799+
db.onStore = onStore
800+
}
801+
802+
t.Run(tc.name, func(t *testing.T) {
803+
t.Parallel()
706804

707805
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV6)
708806
startTestDHCPServer(t, &dhcpsvc.Config{
@@ -1036,3 +1134,31 @@ func newDHCPv6Renew(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt
10361134

10371135
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
10381136
}
1137+
1138+
// newDHCPv6Release creates a new DHCPv6 RELEASE packet for testing.
1139+
func newDHCPv6Release(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) {
1140+
tb.Helper()
1141+
1142+
eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6)
1143+
ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
1144+
1145+
dhcp := &layers.DHCPv6{
1146+
MsgType: layers.DHCPv6MsgTypeRelease,
1147+
HopCount: 0,
1148+
// Don't specify link and peer addresses, as they are intended for relay
1149+
// messages.
1150+
LinkAddr: nil,
1151+
PeerAddr: nil,
1152+
TransactionID: testTransactionID,
1153+
Options: layers.DHCPv6Options{
1154+
newOptClientDUID(tb, mac),
1155+
newOptServerDUID(tb, testIfaceHWAddr),
1156+
},
1157+
}
1158+
1159+
if reqIP.IsValid() && reqIP.Is6() {
1160+
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL))
1161+
}
1162+
1163+
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
1164+
}

internal/dhcpsvc/leaseindex.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,7 @@ func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
9797
// remove removes l from idx and from iface. l must be valid, iface should
9898
// contain the same lease or the lease itself. It returns an error if the lease
9999
// not found.
100-
func (idx *leaseIndex) remove(
101-
ctx context.Context,
102-
l *Lease,
103-
iface *netInterface,
104-
) (err error) {
100+
func (idx *leaseIndex) remove(ctx context.Context, l *Lease, iface *netInterface) (err error) {
105101
loweredName := strings.ToLower(l.Hostname)
106102

107103
if _, ok := idx.byAddr[l.IP]; !ok {

0 commit comments

Comments
 (0)