From 78aae72e3a0d6a6d97ce9ec7724ef0fd4c3d0dd7 Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Fri, 14 Aug 2026 03:28:30 +0000 Subject: [PATCH] Pull request: AGDNS-3863-gopacket-dhcp-vol.35 Squashed commit of the following: commit 32cfedece7b37724d7b461c28da4a02804bf56ed Merge: 35b9f1137 0c3cc9070 Author: Dimitry Kolyshev Date: Fri Aug 14 09:45:40 2026 +0700 Merge remote-tracking branch 'origin/master' into AGDNS-3863-gopacket-dhcp-vol.35 commit 35b9f11374b57982ba3648340b0269926cebb375 Author: Dimitry Kolyshev Date: Wed Aug 12 10:38:46 2026 +0700 dhcpsvc: imp tests commit 61514b027ca408ece564fe1adf9848c4b842afb5 Author: Dimitry Kolyshev Date: Tue Aug 11 13:50:32 2026 +0700 dhcpsvc: imp code commit 0ef39a2f80778155631e7450279b1260a7ee4c9f Merge: efcde305e b2e25729e Author: Dimitry Kolyshev Date: Tue Aug 11 13:25:47 2026 +0700 Merge remote-tracking branch 'origin/master' into AGDNS-3863-gopacket-dhcp-vol.35 commit efcde305e776ba2836e4edca8e6bbc5a74d78334 Author: Eugene Burkov Date: Wed Aug 5 18:58:27 2026 +0300 dhcpsvc: impl decline handler --- internal/dhcpsvc/handler6.go | 61 ++- internal/dhcpsvc/handler6_test.go | 883 ++++++++++++++++-------------- internal/dhcpsvc/options6_test.go | 54 +- internal/dhcpsvc/v6.go | 34 +- 4 files changed, 582 insertions(+), 450 deletions(-) diff --git a/internal/dhcpsvc/handler6.go b/internal/dhcpsvc/handler6.go index e7639843cb3..9a62324215a 100644 --- a/internal/dhcpsvc/handler6.go +++ b/internal/dhcpsvc/handler6.go @@ -289,10 +289,9 @@ func (iface *dhcpInterfaceV6) handleRenew( // 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. // -// 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. +// TODO(e.burkov): Rebind all valid IA_NA options instead of only the first +// valid one. 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, fd *frameData6, @@ -336,8 +335,7 @@ func (iface *dhcpInterfaceV6) handleRebind( // and must be a valid DHCPv6 message of type INFORMATION-REQUEST. fd must be // valid. // -// TODO(e.burkov): The current implementation does not handle relay-forwarded -// INFORMATION-REQUEST messages. +// TODO(e.burkov): Handle relay-forwarded INFORMATION-REQUEST messages. func (iface *dhcpInterfaceV6) handleInfo( ctx context.Context, fd *frameData6, @@ -398,7 +396,7 @@ func (iface *dhcpInterfaceV6) handleRelease( TransactionID: req.TransactionID, } - iaid, ip := iface.ipForRelease(ctx, req) + iaid, ip := iface.firstIANAAddr(ctx, req) if ip == (netip.Addr{}) { resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{}) @@ -412,8 +410,7 @@ func (iface *dhcpInterfaceV6) handleRelease( lease, hasLease := iface.common.leases[key] if !hasLease || lease.IP != ip { - respIANA := newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoBinding) - resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA) + resp.Options = iface.newNoBindingRespOpts(fd, req, cliID, iaid) return respond6(fd, resp) } @@ -422,8 +419,7 @@ func (iface *dhcpInterfaceV6) handleRelease( if err != nil { l.ErrorContext(ctx, "removing lease", slogutil.KeyError, err) - respIANA := newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoBinding) - resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA) + resp.Options = iface.newNoBindingRespOpts(fd, req, cliID, iaid) } else { respIANA := &IANAOption{ID: iaid} resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA.Encode()) @@ -435,7 +431,10 @@ func (iface *dhcpInterfaceV6) handleRelease( // handleDecline handles messages of type DECLINE. req must not be nil and must // be a valid DHCPv6 message of type DECLINE. fd must be valid. // -// TODO(e.burkov): Implement. This is a stub for now. +// See RFC 9915 Section 18.3.8. +// +// TODO(e.burkov): Verify all IA_NA options instead of handling only the first +// one. func (iface *dhcpInterfaceV6) handleDecline( ctx context.Context, fd *frameData6, @@ -449,5 +448,41 @@ func (iface *dhcpInterfaceV6) handleDecline( l := iface.common.logger l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID) - return nil + resp := &layers.DHCPv6{ + MsgType: layers.DHCPv6MsgTypeReply, + TransactionID: req.TransactionID, + } + + iaid, ip := iface.firstIANAAddr(ctx, req) + if ip == (netip.Addr{}) { + resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{}) + + return respond6(fd, resp) + } + + key := macToKey(fd.ether.SrcMAC) + + iface.common.indexMu.Lock() + defer iface.common.indexMu.Unlock() + + lease, hasLease := iface.common.leases[key] + if !hasLease || lease.IP != ip { + resp.Options = iface.newNoBindingRespOpts(fd, req, cliID, iaid) + + return respond6(fd, resp) + } + + l.WarnContext(ctx, "lease is unavailable", "ip", lease.IP) + + err = iface.common.blockLease(ctx, lease, iface.clock.Now()) + if err != nil { + l.ErrorContext(ctx, "blocking lease", slogutil.KeyError, err) + + resp.Options = iface.newNoBindingRespOpts(fd, req, cliID, iaid) + } else { + respIANA := &IANAOption{ID: iaid} + resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA.Encode()) + } + + return respond6(fd, resp) } diff --git a/internal/dhcpsvc/handler6_test.go b/internal/dhcpsvc/handler6_test.go index 5076160ffb7..6a5449c894d 100644 --- a/internal/dhcpsvc/handler6_test.go +++ b/internal/dhcpsvc/handler6_test.go @@ -46,43 +46,43 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) { }{{ in: newDHCPv6Solicit(t, testHWUnknown, testIPv6Unknown, false), name: "new", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), - newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, + newDefaultOptIANA(t, testIPv6Conf.RangeStart), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Solicit(t, testHWStatic, testIPv6Static, false), name: "existing_static", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWStatic), - newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWStatic, + newDefaultOptIANA(t, testIPv6Static), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Solicit(t, testHWDynamic, testIPv6Dynamic, false), name: "existing_dynamic", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWDynamic), - newOptIANA(t, testIAID, testIPv6Dynamic, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, + newDefaultOptIANA(t, testIPv6Dynamic), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Solicit(t, testHWExpired, testIPv6Expired, false), name: "existing_expired", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWExpired), - newOptIANA(t, testIAID, testIPv6Expired, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWExpired, + newDefaultOptIANA(t, testIPv6Expired), + defaultOptPref, + defaultOptSolMaxRT, + ), }} for _, tc := range testCases { @@ -112,6 +112,8 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) { func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) { t.Parallel() + rapidCommitOpt := layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}) + testCases := []struct { in gopacket.Packet want *dhcpsvc.Lease @@ -127,38 +129,38 @@ func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) { IsStatic: false, }, name: "new", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), - newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, + newDefaultOptIANA(t, testIPv6Conf.RangeStart), + defaultOptPref, + defaultOptSolMaxRT, + rapidCommitOpt, + ), }, { in: newDHCPv6Solicit(t, testHWStatic, testIPv6Static, true), want: testLease6Static, name: "existing", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWStatic), - newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWStatic, + newDefaultOptIANA(t, testIPv6Static), + defaultOptPref, + defaultOptSolMaxRT, + rapidCommitOpt, + ), }, { in: newDHCPv6Solicit(t, testHWDynamic, testIPv6Dynamic, true), want: testLease6Dynamic, name: "existing_dynamic", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWDynamic), - newOptIANA(t, testIAID, testIPv6Dynamic, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, + newDefaultOptIANA(t, testIPv6Dynamic), + defaultOptPref, + defaultOptSolMaxRT, + rapidCommitOpt, + ), }, { in: newDHCPv6Solicit(t, testHWExpired, testIPv6Expired, true), want: &dhcpsvc.Lease{ @@ -169,14 +171,14 @@ func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) { IsStatic: false, }, name: "existing_expired", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWExpired), - newOptIANA(t, testIAID, testIPv6Expired, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWExpired, + newDefaultOptIANA(t, testIPv6Expired), + defaultOptPref, + defaultOptSolMaxRT, + rapidCommitOpt, + ), }} for _, tc := range testCases { @@ -233,45 +235,40 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) { IsStatic: false, }, name: "success", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), - newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, + newDefaultOptIANA(t, testIPv6Conf.RangeStart), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Request(t, testHWUnknown, testIPv6OtherSubnet), want: nil, name: "not_on_link", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNotOnLink), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Request(t, testHWStatic, testIPv6Static), want: testLease6Static, name: "existing_static", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWStatic), - newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWStatic, + newDefaultOptIANA(t, testIPv6Static), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { - in: newDHCPv6Request(t, testHWUnknown, netip.Addr{}), - want: nil, - name: "no_iana", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + in: newDHCPv6Request(t, testHWUnknown, netip.Addr{}), + want: nil, + name: "no_iana", + wantOpts: newWantDHCPv6Opts(t, testHWUnknown, defaultOptPref, defaultOptSolMaxRT), }} for _, tc := range testCases { @@ -311,12 +308,19 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) { func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) { t.Parallel() + wantOpts := newWantDHCPv6Opts( + t, + testHWUnknown, + newDefaultOptIANA(t, testIPv6Conf.RangeStart), + defaultOptPref, + defaultOptSolMaxRT, + ) + testCases := []struct { - in gopacket.Packet - solicit gopacket.Packet - want *dhcpsvc.Lease - name string - wantOpts layers.DHCPv6Options + in gopacket.Packet + solicit gopacket.Packet + want *dhcpsvc.Lease + name string }{{ in: newDHCPv6Request(t, testHWUnknown, testIPv6Unknown), solicit: newDHCPv6Solicit(t, testHWUnknown, testIPv6Unknown, false), @@ -328,13 +332,6 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) { IsStatic: false, }, name: "preceding_solicit", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), - newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, }, { in: newDHCPv6Request(t, testHWUnknown, testIPv6Unknown), solicit: newDHCPv6Solicit(t, testHWUnknown, testIPv6Unknown, true), @@ -346,13 +343,6 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) { IsStatic: false, }, name: "preceding_solicit_rapid_commit", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), - newOptIANA(t, testIAID, testIPv6Conf.RangeStart, testLeaseTTL), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, }} for _, tc := range testCases { @@ -389,7 +379,7 @@ func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) { testutil.RequireSend(t, inCh, tc.in, testTimeout) - assertValidResponse6(t, req, outCh, tc.wantOpts) + assertValidResponse6(t, req, outCh, wantOpts) }) } } @@ -407,11 +397,8 @@ func TestDHCPServer_ServeEther6_confirm(t *testing.T) { testHWUnknown, newOptIANA(t, testIAID, testIPv6Unknown, 0), ), - name: "success", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), - }, + name: "success", + wantOpts: newWantDHCPv6Opts(t, testHWUnknown), }, { in: newDHCPv6Confirm( t, @@ -419,11 +406,8 @@ func TestDHCPServer_ServeEther6_confirm(t *testing.T) { newOptIANA(t, testIAID, testIPv6Dynamic, 0), newOptIANA(t, testIAID+1, testIPv6Static, 0), ), - name: "success_multiple", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWDynamic), - }, + name: "success_multiple", + wantOpts: newWantDHCPv6Opts(t, testHWDynamic), }, { in: newDHCPv6Confirm( t, @@ -431,11 +415,11 @@ func TestDHCPServer_ServeEther6_confirm(t *testing.T) { newOptIANA(t, testIAID, testIPv6OtherSubnet, 0), ), name: "not_on_link", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, newOptStatusCode(t, layers.DHCPv6StatusCodeNotOnLink), - }, + ), }, { in: newDHCPv6Confirm( t, @@ -444,11 +428,11 @@ func TestDHCPServer_ServeEther6_confirm(t *testing.T) { newOptIANA(t, testIAID+1, testIPv6OtherSubnet, 0), ), name: "mixed", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, newOptStatusCode(t, layers.DHCPv6StatusCodeNotOnLink), - }, + ), }, { in: newDHCPv6Confirm(t, testHWUnknown), name: "no_iana", @@ -499,45 +483,40 @@ func TestDHCPServer_ServeEther6_renew(t *testing.T) { in: newDHCPv6Renew(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), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, + newDefaultOptIANA(t, testIPv6Dynamic), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Renew(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), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWStatic, + newDefaultOptIANA(t, testIPv6Static), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Renew(t, testHWUnknown, testIPv6Unknown), name: "no_binding", want: nil, - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + defaultOptPref, + defaultOptSolMaxRT, + ), }, { - in: newDHCPv6Renew(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), - }, + in: newDHCPv6Renew(t, testHWUnknown, netip.Addr{}), + name: "no_iana", + want: nil, + wantOpts: newWantDHCPv6Opts(t, testHWUnknown, defaultOptPref, defaultOptSolMaxRT), }} for _, tc := range testCases { @@ -586,45 +565,40 @@ func TestDHCPServer_ServeEther6_rebind(t *testing.T) { 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), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, + newDefaultOptIANA(t, testIPv6Dynamic), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { 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), - }, + wantOpts: newWantDHCPv6Opts( + t, + testHWStatic, + newDefaultOptIANA(t, testIPv6Static), + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Rebind(t, testHWUnknown, testIPv6Unknown), name: "no_binding", want: nil, - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + defaultOptPref, + defaultOptSolMaxRT, + ), }, { - 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), - }, + in: newDHCPv6Rebind(t, testHWUnknown, netip.Addr{}), + name: "no_iana", + want: nil, + wantOpts: newWantDHCPv6Opts(t, testHWUnknown, defaultOptPref, defaultOptSolMaxRT), }} for _, tc := range testCases { @@ -669,12 +643,9 @@ func TestDHCPServer_ServeEther6_info(t *testing.T) { 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, true, true), + name: "cli_and_srv", + wantOpts: newWantDHCPv6Opts(t, testHWUnknown), }, { in: newDHCPv6Info(t, testHWUnknown, false, true), name: "srv_only", @@ -682,12 +653,9 @@ func TestDHCPServer_ServeEther6_info(t *testing.T) { 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, true, false), + name: "cli_only", + wantOpts: newWantDHCPv6Opts(t, testHWUnknown), }, { in: newDHCPv6Info(t, testHWUnknown, false, false), name: "no_opts", @@ -732,56 +700,51 @@ func TestDHCPServer_ServeEther6_release(t *testing.T) { in: newDHCPv6Release(t, testHWDynamic, testIPv6Dynamic), want: testLease6Dynamic, name: "success", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWDynamic), + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeSuccess), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Release(t, testHWStatic, testIPv6Static), want: testLease6Static, name: "success_static", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWStatic), + wantOpts: newWantDHCPv6Opts( + t, + testHWStatic, newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeSuccess), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + defaultOptPref, + defaultOptSolMaxRT, + ), }, { in: newDHCPv6Release(t, testHWUnknown, testIPv6Unknown), want: nil, name: "no_binding", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWUnknown), + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + defaultOptPref, + defaultOptSolMaxRT, + ), }, { - in: newDHCPv6Release(t, testHWDynamic, netip.Addr{}), - want: nil, - name: "no_iana", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWDynamic), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + in: newDHCPv6Release(t, testHWDynamic, netip.Addr{}), + want: nil, + name: "no_iana", + wantOpts: newWantDHCPv6Opts(t, testHWDynamic, defaultOptPref, defaultOptSolMaxRT), }, { in: newDHCPv6Release(t, testHWDynamic, testIPv6Unknown), want: nil, name: "ip_mismatch", - wantOpts: layers.DHCPv6Options{ - newOptServerDUID(t, testIfaceHWAddr), - newOptClientDUID(t, testHWDynamic), + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding), - newOptPreference(t, 0), - newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT), - }, + defaultOptPref, + defaultOptSolMaxRT, + ), }} for _, tc := range testCases { @@ -818,156 +781,91 @@ func TestDHCPServer_ServeEther6_release(t *testing.T) { } } -// newDHCPv6Solicit creates a new DHCPv6 SOLICIT packet for testing. -func newDHCPv6Solicit( - tb testing.TB, - hwAddr net.HardwareAddr, - reqIP netip.Addr, - rapidCommit bool, -) (pkt gopacket.Packet) { - tb.Helper() - - eth := newEthernetLayer(tb, hwAddr, nil, layers.EthernetTypeIPv6) - ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) +func TestDHCPServer_ServeEther6_decline(t *testing.T) { + t.Parallel() - dhcp := &layers.DHCPv6{ - MsgType: layers.DHCPv6MsgTypeSolicit, - 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, hwAddr), + testCases := []struct { + in gopacket.Packet + want *dhcpsvc.Lease + name string + wantOpts layers.DHCPv6Options + }{{ + in: newDHCPv6Decline(t, testHWDynamic, testIPv6Dynamic), + want: &dhcpsvc.Lease{ + IP: testIPv6Dynamic, + Expiry: testExpiryDynamicLease, + Hostname: "", + HWAddr: dhcpsvc.BlockedHardwareAddr, + IsStatic: false, }, - } + name: "success", + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, + newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeSuccess), + defaultOptPref, + defaultOptSolMaxRT, + ), + }, { + in: newDHCPv6Decline(t, testHWUnknown, testIPv6Unknown), + want: nil, + name: "no_binding", + wantOpts: newWantDHCPv6Opts( + t, + testHWUnknown, + newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding), + defaultOptPref, + defaultOptSolMaxRT, + ), + }, { + in: newDHCPv6Decline(t, testHWDynamic, testIPv6Unknown), + want: nil, + name: "ip_mismatch", + wantOpts: newWantDHCPv6Opts( + t, + testHWDynamic, + newOptIANAStatus(t, testIAID, layers.DHCPv6StatusCodeNoBinding), + defaultOptPref, + defaultOptSolMaxRT, + ), + }, { + in: newDHCPv6Decline(t, testHWDynamic, netip.Addr{}), + want: nil, + name: "no_iana", + wantOpts: newWantDHCPv6Opts(t, testHWDynamic, defaultOptPref, defaultOptSolMaxRT), + }} - if reqIP.IsValid() && reqIP.Is6() { - dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) - } + for _, tc := range testCases { + req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6)) - if rapidCommit { - o := layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, nil) - dhcp.Options = append(dhcp.Options, o) - } + db := newTestDatabase(t) - return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp) -} + onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) { + assert.Contains(t, leases, tc.want) -// 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() + return nil + } - eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6) - ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) + if tc.want != nil { + db.onStore = onStore + } - dhcp := &layers.DHCPv6{ - MsgType: layers.DHCPv6MsgTypeRequest, - 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), - newOptServerDUID(tb, testIfaceHWAddr), - }, - } - - if reqIP.IsValid() && reqIP.Is6() { - 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. -func newIPv6UDPLayer(tb testing.TB, src, dst netip.AddrPort) (ip *layers.IPv6, udp *layers.UDP) { - tb.Helper() - - if !src.IsValid() { - src = netip.AddrPortFrom(netip.IPv6Unspecified(), uint16(dhcpsvc.ClientPortV6)) - } - - if !dst.IsValid() { - bcastAddr, ok := netip.AddrFromSlice(net.IPv6linklocalallnodes) - require.True(tb, ok) - - dst = netip.AddrPortFrom(bcastAddr, uint16(dhcpsvc.ServerPortV6)) - } - - ip = &layers.IPv6{ - Version: 6, - HopLimit: dhcpsvc.IPv6DefaultHopLimit, - SrcIP: src.Addr().AsSlice(), - DstIP: dst.Addr().AsSlice(), - NextHeader: layers.IPProtocolUDP, - } - udp = &layers.UDP{ - SrcPort: layers.UDPPort(src.Port()), - DstPort: layers.UDPPort(dst.Port()), - } - require.NoError(tb, udp.SetNetworkLayerForChecksum(ip)) - - return ip, udp -} + t.Run(tc.name, func(t *testing.T) { + t.Parallel() -// newEthernetLayer creates a new Ethernet layer for IP packets of the specified -// type. Nil src is replaced with an unspecified MAC address, nil dst is -// replaced with a broadcast MAC address, typ must be [layers.EthernetTypeIPv4] -// or [layers.EthernetTypeIPv6]. -func newEthernetLayer( - tb testing.TB, - src net.HardwareAddr, - dst net.HardwareAddr, - typ layers.EthernetType, -) (eth *layers.Ethernet) { - tb.Helper() + ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV6) + startTestDHCPServer(t, &dhcpsvc.Config{ + Database: db, + Interfaces: testIPv6InterfacesConf, + Logger: testLogger, + NetworkDeviceManager: ndMgr, + Enabled: true, + }) - if src == nil { - src = net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - } - if dst == nil { - dst = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} - } + testutil.RequireSend(t, inCh, tc.in, testTimeout) - return &layers.Ethernet{ - SrcMAC: src, - DstMAC: dst, - EthernetType: typ, + assertValidResponse6(t, req, outCh, tc.wantOpts) + }) } } @@ -1042,17 +940,20 @@ 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) { +// newDHCPv6Solicit creates a new DHCPv6 SOLICIT packet for testing. +func newDHCPv6Solicit( + tb testing.TB, + hwAddr net.HardwareAddr, + reqIP netip.Addr, + rapidCommit bool, +) (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) + eth := newEthernetLayer(tb, hwAddr, nil, layers.EthernetTypeIPv6) ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) dhcp := &layers.DHCPv6{ - MsgType: layers.DHCPv6MsgTypeRebind, + MsgType: layers.DHCPv6MsgTypeSolicit, HopCount: 0, // Don't specify link and peer addresses, as they are intended for relay // messages. @@ -1060,8 +961,7 @@ func newDHCPv6Rebind(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt PeerAddr: nil, TransactionID: testTransactionID, Options: layers.DHCPv6Options{ - newOptClientDUID(tb, mac), - // REBIND must not contain a Server ID option. + newOptClientDUID(tb, hwAddr), }, } @@ -1069,69 +969,137 @@ func newDHCPv6Rebind(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) } + if rapidCommit { + o := layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, nil) + dhcp.Options = append(dhcp.Options, o) + } + 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) { +// 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, nil, layers.EthernetTypeIPv6) + eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6) ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) dhcp := &layers.DHCPv6{ - MsgType: layers.DHCPv6MsgTypeInformationRequest, + MsgType: layers.DHCPv6MsgTypeRequest, 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)) + Options: layers.DHCPv6Options{ + newOptClientDUID(tb, mac), + newOptServerDUID(tb, testIfaceHWAddr), + }, } - if addServerID { - dhcp.Options = append(dhcp.Options, newOptServerDUID(tb, testIfaceHWAddr)) + if reqIP.IsValid() && reqIP.Is6() { + dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) } 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) { +// 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.DHCPv6MsgTypeRenew, - HopCount: 0, - // Don't specify link and peer addresses, as they are intended for relay - // messages. + MsgType: layers.DHCPv6MsgTypeConfirm, + HopCount: 0, LinkAddr: nil, PeerAddr: nil, TransactionID: testTransactionID, Options: layers.DHCPv6Options{ newOptClientDUID(tb, mac), - newOptServerDUID(tb, testIfaceHWAddr), }, } + dhcp.Options = append(dhcp.Options, ianas...) + + 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() + + opts := layers.DHCPv6Options{ + newOptClientDUID(tb, mac), + newOptServerDUID(tb, testIfaceHWAddr), + } + + if reqIP.Is6() { + opts = append(opts, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) + } + + eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6) + ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) + dhcp := newTestDHCPv6(tb, layers.DHCPv6MsgTypeRenew, opts...) + + return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp) +} + +// 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() + + opts := 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)) + opts = append(opts, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) } + // 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 := newTestDHCPv6(tb, layers.DHCPv6MsgTypeRebind, opts...) + + 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() + + var opts layers.DHCPv6Options + + if addClientID { + opts = append(opts, newOptClientDUID(tb, mac)) + } + + if addServerID { + opts = append(opts, newOptServerDUID(tb, testIfaceHWAddr)) + } + + eth := newEthernetLayer(tb, mac, nil, layers.EthernetTypeIPv6) + ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) + dhcp := newTestDHCPv6(tb, layers.DHCPv6MsgTypeInformationRequest, opts...) + return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp) } @@ -1139,26 +1107,119 @@ func newDHCPv6Renew(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt func newDHCPv6Release(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) { tb.Helper() + opts := layers.DHCPv6Options{ + newOptClientDUID(tb, mac), + newOptServerDUID(tb, testIfaceHWAddr), + } + + if reqIP.Is6() { + opts = append(opts, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) + } + eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6) ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) + dhcp := newTestDHCPv6(tb, layers.DHCPv6MsgTypeRelease, opts...) - dhcp := &layers.DHCPv6{ - MsgType: layers.DHCPv6MsgTypeRelease, + return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp) +} + +// newDHCPv6Decline creates a new DHCPv6 DECLINE packet for testing. +func newDHCPv6Decline(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) { + tb.Helper() + + opts := layers.DHCPv6Options{ + newOptClientDUID(tb, mac), + newOptServerDUID(tb, testIfaceHWAddr), + } + + if reqIP.Is6() { + opts = append(opts, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) + } + + eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6) + ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{}) + dhcp := newTestDHCPv6(tb, layers.DHCPv6MsgTypeDecline, opts...) + + return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp) +} + +// newTestDHCPv6 creates a new DHCPv6 message for testing with the specified +// type and options. The link and peer addresses are not set, as they are +// intended for relay messages. +func newTestDHCPv6( + tb testing.TB, + msgType layers.DHCPv6MsgType, + opts ...layers.DHCPv6Option, +) (dhcp *layers.DHCPv6) { + tb.Helper() + + return &layers.DHCPv6{ + MsgType: msgType, 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), - newOptServerDUID(tb, testIfaceHWAddr), - }, + Options: opts, } +} - if reqIP.IsValid() && reqIP.Is6() { - dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL)) +// 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. +func newIPv6UDPLayer(tb testing.TB, src, dst netip.AddrPort) (ip *layers.IPv6, udp *layers.UDP) { + tb.Helper() + + if !src.IsValid() { + src = netip.AddrPortFrom(netip.IPv6Unspecified(), uint16(dhcpsvc.ClientPortV6)) } - return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp) + if !dst.IsValid() { + bcastAddr, ok := netip.AddrFromSlice(net.IPv6linklocalallnodes) + require.True(tb, ok) + + dst = netip.AddrPortFrom(bcastAddr, uint16(dhcpsvc.ServerPortV6)) + } + + ip = &layers.IPv6{ + Version: 6, + HopLimit: dhcpsvc.IPv6DefaultHopLimit, + SrcIP: src.Addr().AsSlice(), + DstIP: dst.Addr().AsSlice(), + NextHeader: layers.IPProtocolUDP, + } + udp = &layers.UDP{ + SrcPort: layers.UDPPort(src.Port()), + DstPort: layers.UDPPort(dst.Port()), + } + require.NoError(tb, udp.SetNetworkLayerForChecksum(ip)) + + return ip, udp +} + +// newEthernetLayer creates a new Ethernet layer for IP packets of the specified +// type. Nil src is replaced with an unspecified MAC address, nil dst is +// replaced with a broadcast MAC address, typ must be [layers.EthernetTypeIPv4] +// or [layers.EthernetTypeIPv6]. +func newEthernetLayer( + tb testing.TB, + src net.HardwareAddr, + dst net.HardwareAddr, + typ layers.EthernetType, +) (eth *layers.Ethernet) { + tb.Helper() + + if src == nil { + src = net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + } + if dst == nil { + dst = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + } + + return &layers.Ethernet{ + SrcMAC: src, + DstMAC: dst, + EthernetType: typ, + } } diff --git a/internal/dhcpsvc/options6_test.go b/internal/dhcpsvc/options6_test.go index 6862c847dcc..d3d6c44066d 100644 --- a/internal/dhcpsvc/options6_test.go +++ b/internal/dhcpsvc/options6_test.go @@ -11,6 +11,17 @@ import ( "github.com/gopacket/gopacket/layers" ) +// defaultOptPref is a default DHCPv6 Preference (7) option with preference +// value set to 0. +var defaultOptPref = layers.NewDHCPv6Option(layers.DHCPv6OptPreference, []byte{0}) + +// defaultOptSolMaxRT creates a DHCPv6 Solicit Message Maximum Retransmission +// Time (80) option with maxRT value set to [dhcpsvc.DefaultSolMaxRT]. +var defaultOptSolMaxRT = layers.NewDHCPv6Option( + layers.DHCPv6OptSolMaxRt, + binary.BigEndian.AppendUint32(nil, uint32(dhcpsvc.DefaultSolMaxRT.Seconds())), +) + // newOptStatusCode creates a top-level DHCPv6 Status Code option. func newOptStatusCode(tb testing.TB, status layers.DHCPv6StatusCode) (opt layers.DHCPv6Option) { tb.Helper() @@ -52,6 +63,11 @@ func newOptIANA( return iana.Encode() } +// newDefaultOptIANA creates a default DHCPv6 IANA option for tests. +func newDefaultOptIANA(tb testing.TB, reqIP netip.Addr) (opt layers.DHCPv6Option) { + return newOptIANA(tb, testIAID, reqIP, testLeaseTTL) +} + // newOptIANAStatus creates a DHCPv6 IA_NA (3) option carrying only a nested // Status Code option. If status is [layers.DHCPv6StatusCodeSuccess], the // returned option will not contain a nested Status Code option, as per RFC 8415 @@ -96,25 +112,6 @@ func newOptIANAStatus( return layers.NewDHCPv6Option(layers.DHCPv6OptIANA, data) } -// newOptPreference creates a DHCPv6 Preference (7) option with the specified -// preference value. -func newOptPreference(tb testing.TB, pref uint8) (opt layers.DHCPv6Option) { - tb.Helper() - - return layers.NewDHCPv6Option(layers.DHCPv6OptPreference, []byte{pref}) -} - -// newOptSolMaxRT creates a DHCPv6 Solicit Message Maximum Retransmission Time -// (80) option with the specified maxRT value. -func newOptSolMaxRT(tb testing.TB, maxRT time.Duration) (opt layers.DHCPv6Option) { - tb.Helper() - - return layers.NewDHCPv6Option( - layers.DHCPv6OptSolMaxRt, - binary.BigEndian.AppendUint32(nil, uint32(maxRT.Seconds())), - ) -} - // newOptClientDUID creates a DHCPv6 Client Identifier (1) option containing a // DUID-LL made of cliHWAddr. func newOptClientDUID(tb testing.TB, cliHWAddr net.HardwareAddr) (opt layers.DHCPv6Option) { @@ -148,3 +145,22 @@ func newOptDUIDLL( return layers.NewDHCPv6Option(code, duid.Encode()) } + +// newWantDHCPv6Opts returns a slice of DHCPv6 options with the specified +// options and default server and client DUID options. +func newWantDHCPv6Opts( + tb testing.TB, + mac net.HardwareAddr, + opts ...layers.DHCPv6Option, +) (want layers.DHCPv6Options) { + tb.Helper() + + want = layers.DHCPv6Options{ + newOptServerDUID(tb, testIfaceHWAddr), + newOptClientDUID(tb, mac), + } + + want = append(want, opts...) + + return want +} diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go index 1a8efa769db..cd2d4e36d55 100644 --- a/internal/dhcpsvc/v6.go +++ b/internal/dhcpsvc/v6.go @@ -440,10 +440,12 @@ func (iface *dhcpInterfaceV6) allocateForSolicit( return nil, 0 } -// ipForRelease returns the IAID and IP address requested for release in req. -// It returns zero values if there is no IA_NA option or if all options are -// malformed. req must be a valid DHCPv6 message of RELEASE type. -func (iface *dhcpInterfaceV6) ipForRelease( +// firstIANAAddr returns the IAID and the first address of the first valid +// IA_NA option in req. It returns zero values if there is no IA_NA option or +// if all options are malformed. req must not be nil. +// +// See RFC 9915 Sections 18.3.7 and 18.3.8. +func (iface *dhcpInterfaceV6) firstIANAAddr( ctx context.Context, req *layers.DHCPv6, ) (iaid uint32, ip netip.Addr) { @@ -464,7 +466,7 @@ func (iface *dhcpInterfaceV6) ipForRelease( reqIP, hasReqIP := iana.requestedAddr() if !hasReqIP { - l.DebugContext(ctx, "no ip in ia_na for release", "iaid", iana.ID) + l.DebugContext(ctx, "no ip in ia_na", "iaid", iana.ID) continue } @@ -667,6 +669,22 @@ func (iface *dhcpInterfaceV6) newUpdateRespOpts( return iface.appendRequestedOptions(opts, req) } +// newNoBindingRespOpts returns the option list for a Reply to a RENEW or REBIND +// message when the server has no binding for the client. fd, req, and cliID +// must not be nil. iaid must not be zero. +// +// See RFC 9915 Section 18.3.4 and 18.3.5. +func (iface *dhcpInterfaceV6) newNoBindingRespOpts( + fd *frameData6, + req *layers.DHCPv6, + cliID *layers.DHCPv6DUID, + iaid uint32, +) (opts layers.DHCPv6Options) { + respIANA := newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoBinding) + + return iface.newUpdateRespOpts(fd, req, cliID, respIANA) +} + // 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. @@ -698,7 +716,7 @@ func (iface *dhcpInterfaceV6) iaNAFromLease(lease *Lease, iaid uint32) (iana lay return newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoAddrsAvail) } - return IANAOption{ + opt := IANAOption{ Nested: []IAAddrOption{{ Addr: lease.IP, PreferredLifetime: iface.common.leaseTTL, @@ -707,7 +725,9 @@ func (iface *dhcpInterfaceV6) iaNAFromLease(lease *Lease, iaid uint32) (iana lay ID: iaid, T1: iface.t1, T2: iface.t2, - }.Encode() + } + + return opt.Encode() } // ianaForRequest returns the IANA filled with committed lease data for req. It