Pull request 2650: AGDNS-3863-gopacket-dhcp-vol.24

Updates #4923.

Squashed commit of the following:

commit 32b3580ed051c4d0b021fd0b141b2e25cd36b584
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue May 5 18:37:03 2026 +0300

    dhcpsvc: fix docs

commit cce786e4591d94373b6e2bcbeb51ee5ca1fa1764
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue May 5 15:44:53 2026 +0300

    dhcpsvc: validate v6 messages
This commit is contained in:
Eugene Burkov 2026-05-06 15:42:51 +00:00
parent 3eb42df776
commit b60af4bf31
8 changed files with 314 additions and 44 deletions

View file

@ -108,8 +108,10 @@ func (ic *InterfaceConfig) Validate() (err error) {
return errors.ErrNoValue
}
return errors.Join(
errors.Annotate(ic.IPv4.Validate(), "IPv4: %w"),
errors.Annotate(ic.IPv6.Validate(), "IPv6: %w"),
)
errs := []error{
errors.Annotate(ic.IPv4.Validate(), "ipv4: %w"),
errors.Annotate(ic.IPv6.Validate(), "ipv6: %w"),
}
return errors.Join(errs...)
}

View file

@ -109,9 +109,6 @@ const (
testAnotherRangeStartV6Str = "2001:db9::1"
)
// testHWIface is the test MAC address of a test network interface.
var testHWIface = net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
var (
// testIPv4Conf is a common valid IPv4 part of the interface configuration
// for tests.

View file

@ -145,6 +145,7 @@ func newFrameData6(
ether: etherLayer,
ip: ipLayer,
duid: duid,
duidData: duid.Encode(),
device: dev,
localAddr: addr,
}

View file

@ -1,6 +1,7 @@
package dhcpsvc
import (
"bytes"
"context"
"fmt"
@ -21,10 +22,6 @@ func (srv *DHCPServer) serveV6(
) (err error) {
defer func() { err = errors.Annotate(err, "serving dhcpv6: %w") }()
// TODO(e.burkov): Use the iface and fd parameters.
_ = iface
_ = fd
msg, ok := pkt.Layer(layers.LayerTypeDHCPv6).(*layers.DHCPv6)
if !ok {
// TODO(e.burkov): Consider adding some debug information about the
@ -36,29 +33,211 @@ func (srv *DHCPServer) serveV6(
// TODO(e.burkov): Handle duplicate TransactionID.
return srv.handleDHCPv6(ctx, msg.MsgType, msg)
return iface.handleDHCPv6(ctx, msg.MsgType, fd, msg)
}
// handleDHCPv6 handles the DHCPv6 message of the given type.
func (srv *DHCPServer) handleDHCPv6(
_ context.Context,
func (iface *dhcpInterfaceV6) handleDHCPv6(
ctx context.Context,
typ layers.DHCPv6MsgType,
_ *layers.DHCPv6,
fd *frameData6,
req *layers.DHCPv6,
) (err error) {
switch typ {
case
layers.DHCPv6MsgTypeSolicit,
layers.DHCPv6MsgTypeRequest,
layers.DHCPv6MsgTypeConfirm,
layers.DHCPv6MsgTypeRenew,
layers.DHCPv6MsgTypeRebind,
layers.DHCPv6MsgTypeInformationRequest,
layers.DHCPv6MsgTypeRelease,
layers.DHCPv6MsgTypeDecline:
// TODO(e.burkov): Handle messages.
case layers.DHCPv6MsgTypeSolicit:
return iface.handleSolicit(ctx, fd, req)
case layers.DHCPv6MsgTypeRequest:
return iface.handleRequest(ctx, fd, req)
case layers.DHCPv6MsgTypeConfirm:
return iface.handleConfirm(ctx, fd, req)
case layers.DHCPv6MsgTypeRenew:
return iface.handleRenew(ctx, fd, req)
case layers.DHCPv6MsgTypeRebind:
return iface.handleRebind(ctx, fd, req)
case layers.DHCPv6MsgTypeInformationRequest:
return iface.handleInfo(ctx, fd, req)
case layers.DHCPv6MsgTypeRelease:
return iface.handleRelease(ctx, fd, req)
case layers.DHCPv6MsgTypeDecline:
return iface.handleDecline(ctx, fd, req)
default:
return fmt.Errorf("dhcpv6: request type: %w: %v", errors.ErrBadEnumValue, typ)
return fmt.Errorf("dhcpv6: request type: %w: %d", errors.ErrBadEnumValue, typ)
}
}
// handleSolicit handles messages of type SOLICIT. req must not be nil and must
// be a valid DHCPv6 message of type SOLICIT. fd must be valid.
//
// TODO(e.burkov): Implement. This is a stub for now.
func (iface *dhcpInterfaceV6) handleSolicit(
ctx context.Context,
_ *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDNoServer(req.Options)
if err != nil {
return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
}
// handleRequest handles messages of type REQUEST. req must not be nil and must
// be a valid DHCPv6 message of type REQUEST. fd must be valid.
//
// TODO(e.burkov): Implement. This is a stub for now.
func (iface *dhcpInterfaceV6) handleRequest(
ctx context.Context,
fd *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
if err != nil {
return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
}
// 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,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDNoServer(req.Options)
if err != nil {
return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
}
// handleRenew handles messages of type RENEW. req must not be nil and must be
// a valid DHCPv6 message of type RENEW. fd must be valid.
//
// TODO(e.burkov): Implement. This is a stub for now.
func (iface *dhcpInterfaceV6) handleRenew(
ctx context.Context,
fd *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
if err != nil {
return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
}
// 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): Implement. This is a stub for now.
func (iface *dhcpInterfaceV6) handleRebind(
ctx context.Context,
_ *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDNoServer(req.Options)
if err != nil {
return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
}
// 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.
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,
)
}
}
_, ok := findOption6(req.Options, layers.DHCPv6OptIANA)
if ok {
return fmt.Errorf("dhcpv6: %s: ia option: %w", req.MsgType, errors.ErrUnexpectedValue)
}
_, ok = findOption6(req.Options, layers.DHCPv6OptIATA)
if ok {
return fmt.Errorf("dhcpv6: %s: ia option: %w", req.MsgType, errors.ErrUnexpectedValue)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType)
return nil
}
// handleRelease handles messages of type RELEASE. req must not be nil and must
// be a valid DHCPv6 message of type RELEASE. fd must be valid.
//
// TODO(e.burkov): Implement. This is a stub for now.
func (iface *dhcpInterfaceV6) handleRelease(
ctx context.Context,
fd *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
if err != nil {
return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
}
// 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.
func (iface *dhcpInterfaceV6) handleDecline(
ctx context.Context,
fd *frameData6,
req *layers.DHCPv6,
) (err error) {
cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
if err != nil {
return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
}
l := iface.common.logger
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
return nil
}

View file

@ -128,16 +128,16 @@ func (EmptyNetworkDevice) WritePacketData(_ []byte) (err error) {
// frameData4 stores the Ethernet and IPv4 layers of the incoming packet, as
// well as the network device that the packet was received from and its address.
type frameData4 struct {
// device is the network device that the packet was received from. It must
// not be nil.
device NetworkDevice
// ether is the Ethernet layer of the incoming packet. It must not be nil.
ether *layers.Ethernet
// ip is the IPv4 layer of the incoming packet. It must not be nil.
ip *layers.IPv4
// device is the network device that the packet was received from. It must
// not be nil.
device NetworkDevice
// localAddr is the local IP address that the packet was sent to. It must
// be a valid IPv4 address assigned to the device.
localAddr netip.Addr
@ -146,16 +146,6 @@ type frameData4 struct {
// frameData6 stores the Ethernet and IPv6 layers of the incoming packet, as
// well as the network device that the packet was received from and its address.
type frameData6 struct {
// ether is the Ethernet layer of the incoming packet. It must not be nil.
ether *layers.Ethernet
// ip is the IPv6 layer of the incoming packet. It must not be nil.
ip *layers.IPv6
// duid is the DHCPv6 DUID constructed of the network device hardware
// address. It must not be nil.
duid *layers.DHCPv6DUID
// device is the network device that the packet was received from. It must
// not be nil.
device NetworkDevice
@ -163,4 +153,19 @@ type frameData6 struct {
// localAddr is the local IP address that the packet was sent to. It must
// be a valid IPv6 address assigned to the device.
localAddr netip.Addr
// duid is the DHCPv6 DUID constructed of the network device hardware
// address. It must not be nil.
duid *layers.DHCPv6DUID
// ether is the Ethernet layer of the incoming packet. It must not be nil.
ether *layers.Ethernet
// ip is the IPv6 layer of the incoming packet. It must not be nil.
ip *layers.IPv6
// duidData is the pre-encoded DUID-LL for this server interface. It is
// used to match the Server Identifier option in incoming DHCPv6 messages.
// It must not be nil.
duidData []byte
}

View file

@ -91,7 +91,7 @@ func (nd *testNetworkDevice) LinkType() (lt layers.LinkType) {
// newTestNetworkDeviceManager creates a network device manager for testing. It
// requires that device opened have a deviceName. The device itself has a link
// type [layers.LinkTypeEthernet] and a hardware address [testHWIface].
// type [layers.LinkTypeEthernet] and a hardware address [testIfaceHWAddr].
// Incoming packets are received from inCh and outgoing packets are sent to
// outCh.
func newTestNetworkDeviceManager(
@ -125,7 +125,7 @@ func newTestNetworkDeviceManager(
}
// newTestNetworkDevice creates a network device for testing. It has a link
// type [layers.LinkTypeEthernet] and a hardware address [testHWIface].
// type [layers.LinkTypeEthernet] and a hardware address [testIfaceHWAddr].
// Incoming packets are received from inCh and outgoing packets are sent to
// outCh.
func newTestNetworkDevice(
@ -169,7 +169,7 @@ func newTestNetworkDevice(
}
onHardwareAddr := func() (hw net.HardwareAddr) {
return testHWIface
return testIfaceHWAddr
}
onLinkType := func() (lt layers.LinkType) {

View file

@ -208,3 +208,27 @@ func newServerDUID(mac net.HardwareAddr) (duid *layers.DHCPv6DUID) {
LinkLayerAddress: mac,
}
}
// findOption6 returns the data of the first option with the given code in
// opts. It returns nil and false if no such option is found.
func findOption6(opts layers.DHCPv6Options, code layers.DHCPv6Opt) (data []byte, ok bool) {
for _, opt := range opts {
if opt.Code == code {
return opt.Data, true
}
}
return nil, false
}
// clientDUID6 returns the data of the Client Identifier option (option 1) of
// msg.
func clientDUID6(opts layers.DHCPv6Options) (duid []byte, ok bool) {
return findOption6(opts, layers.DHCPv6OptClientID)
}
// serverDUID6 returns the data of the Server Identifier option (option 2) of
// msg.
func serverDUID6(opts layers.DHCPv6Options) (duid []byte, ok bool) {
return findOption6(opts, layers.DHCPv6OptServerID)
}

View file

@ -1,7 +1,9 @@
package dhcpsvc
import (
"bytes"
"context"
"fmt"
"log/slog"
"net/netip"
"slices"
@ -156,7 +158,7 @@ type dhcpInterfaceV6 struct {
// newDHCPInterfaceV6 creates a new DHCP interface for IPv6 address family with
// the given configuration. If the interface is disabled, it returns nil. conf
// must be valid.
// must be valid. hwAddr must not be empty.
func (srv *DHCPServer) newDHCPInterfaceV6(
ctx context.Context,
l *slog.Logger,
@ -245,3 +247,63 @@ func (c *IPv6Config) options(ctx context.Context, l *slog.Logger) (imp, exp laye
func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) {
return int(a.Code) - int(b.Code)
}
// clientIDNoServer extracts the client identifier from opts and checks that
// there is no server identifier. It returns an error if the client identifier
// is not found or if the server identifier is found.
func clientIDNoServer(opts layers.DHCPv6Options) (cliID *layers.DHCPv6DUID, err error) {
_, ok := serverDUID6(opts)
if ok {
return nil, fmt.Errorf("dhcpv6: server id: %w", errors.ErrUnexpectedValue)
}
cliIDData, ok := clientDUID6(opts)
if !ok {
return nil, fmt.Errorf("dhcpv6: client id: %w", errors.ErrNoValue)
}
cliID = &layers.DHCPv6DUID{}
err = cliID.DecodeFromBytes(cliIDData)
if err != nil {
return nil, fmt.Errorf("dhcpv6: client id: %w", err)
}
return cliID, nil
}
// clientIDMatchingServer extracts the client identifier from opts and checks
// that the server identifier matches serverDUID. It returns an error if the
// client identifier is not found, if the server identifier is not found, or if
// the server identifier does not match serverDUID.
func clientIDMatchingServer(
opts layers.DHCPv6Options,
serverDUID []byte,
) (cliID *layers.DHCPv6DUID, err error) {
srvID, ok := serverDUID6(opts)
if !ok {
return nil, fmt.Errorf("dhcpv6: server id: %w", errors.ErrNoValue)
}
// TODO(e.burkov): Add validate.EqualFunc.
if !bytes.Equal(srvID, serverDUID) {
return nil, fmt.Errorf(
"dhcpv6: server id: got %v, want %v: %w",
srvID,
serverDUID,
errors.ErrNotEqual,
)
}
cliIDData, ok := clientDUID6(opts)
if !ok {
return nil, fmt.Errorf("dhcpv6: client id: %w", errors.ErrNoValue)
}
cliID = &layers.DHCPv6DUID{}
err = cliID.DecodeFromBytes(cliIDData)
if err != nil {
return nil, fmt.Errorf("dhcpv6: client id: %w", err)
}
return cliID, nil
}