mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-06-28 03:41:19 +00:00
Pull request 2634: AGDNS-3863-gopacket-dhcp-vol.23
Updates #4923.
Squashed commit of the following:
commit 305d3ba116a6abcdb23805af8a5a7dfb82bdc580
Merge: c7512c829 9e153fbd9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Apr 21 19:30:31 2026 +0300
Merge branch 'master' into AGDNS-3863-gopacket-dhcp-vol.23
commit c7512c82921b2dbf0543e9c87120d2ce874729fd
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Apr 21 19:11:39 2026 +0300
dhcpsvc: fix doc
commit f0fe3b6bcf34672de9cbe94d73d7c3b3c3589368
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Apr 21 15:10:16 2026 +0300
dhcpsvc: imp docs
commit 8eeea478bff002a3683c02ba72861f3db9e550db
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Apr 21 14:53:56 2026 +0300
dhcpsvc: imp code, docs
commit 96cf3b34feb8a12c77a2c5e49c9c5a61537c95c4
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Mon Apr 20 15:30:58 2026 +0300
dhcpsvc: add dhcpv6 handle, imp code
commit e94cb9e1da8c117bc4c16634270230125f97677c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Wed Apr 15 15:56:38 2026 +0300
dhcpsvc: add ipv6 consts
This commit is contained in:
parent
9e153fbd99
commit
b08e587660
14 changed files with 529 additions and 106 deletions
|
|
@ -140,7 +140,7 @@ func TestIPv6Config_Validate(t *testing.T) {
|
|||
RangeStart: testIPv4Conf.GatewayIP,
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
},
|
||||
wantErrMsg: "range start " + testGatewayIPv4Str + " should be a valid ipv6",
|
||||
wantErrMsg: "range start " + testGatewayIPv4Str + " must be a valid ipv6",
|
||||
}, {
|
||||
name: "bad_lease_duration",
|
||||
conf: &dhcpsvc.IPv6Config{
|
||||
|
|
@ -148,7 +148,7 @@ func TestIPv6Config_Validate(t *testing.T) {
|
|||
RangeStart: netip.MustParseAddr(testRangeStartV6Str),
|
||||
LeaseDuration: 0,
|
||||
},
|
||||
wantErrMsg: "lease duration 0s must be positive",
|
||||
wantErrMsg: "lease duration: not positive: 0s",
|
||||
}, {
|
||||
name: "valid",
|
||||
conf: &dhcpsvc.IPv6Config{
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ type dataLeases struct {
|
|||
}
|
||||
|
||||
// dbLease is the structure of stored lease.
|
||||
//
|
||||
// TODO(e.burkov): Migrate to add DUID and IAID fields for DHCPv6 leases.
|
||||
type dbLease struct {
|
||||
Expiry string `json:"expires"`
|
||||
IP netip.Addr `json:"ip"`
|
||||
|
|
|
|||
|
|
@ -109,6 +109,9 @@ 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.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func (srv *DHCPServer) serveEther4(ctx context.Context, iface *dhcpInterfaceV4,
|
|||
src := gopacket.NewPacketSource(nd, nd.LinkType())
|
||||
|
||||
for pkt := range src.Packets() {
|
||||
fd := newFrameData(ctx, srv.logger, pkt, nd)
|
||||
fd := newFrameData4(ctx, srv.logger, pkt, nd)
|
||||
if fd == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -33,15 +33,40 @@ func (srv *DHCPServer) serveEther4(ctx context.Context, iface *dhcpInterfaceV4,
|
|||
}
|
||||
}
|
||||
|
||||
// newFrameData creates a new frameData with layers extracted from pkt. It
|
||||
// serveEther6 handles the incoming ethernet packets and dispatches them to the
|
||||
// appropriate handler. It's used to run in a separate goroutine as it blocks
|
||||
// until packets channel is closed. iface and nd must not be nil. nd must have
|
||||
// at least a single address returned by its Addresses method.
|
||||
//
|
||||
//lint:ignore U1000 TODO(e.burkov): Use.
|
||||
func (srv *DHCPServer) serveEther6(ctx context.Context, iface *dhcpInterfaceV6, nd NetworkDevice) {
|
||||
defer slogutil.RecoverAndLog(ctx, srv.logger)
|
||||
|
||||
src := gopacket.NewPacketSource(nd, nd.LinkType())
|
||||
srvDUID := newServerDUID(nd.HardwareAddr())
|
||||
|
||||
for pkt := range src.Packets() {
|
||||
fd := newFrameData6(ctx, srv.logger, pkt, nd, srvDUID)
|
||||
if fd == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err := srv.serveV6(ctx, iface, pkt, fd)
|
||||
if err != nil {
|
||||
srv.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newFrameData4 creates a new [frameData4] with layers extracted from pkt. It
|
||||
// returns nil if the packet is not an Ethernet or IPv4 packet, or if the
|
||||
// network device has no addresses. logger, pkt, and dev must not be nil.
|
||||
func newFrameData(
|
||||
func newFrameData4(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
pkt gopacket.Packet,
|
||||
dev NetworkDevice,
|
||||
) (fd *frameData) {
|
||||
) (fd *frameData4) {
|
||||
addrs := dev.Addresses()
|
||||
if len(addrs) == 0 {
|
||||
logger.ErrorContext(ctx, "no addresses for network device")
|
||||
|
|
@ -70,7 +95,7 @@ func newFrameData(
|
|||
addr = addrs[0]
|
||||
}
|
||||
|
||||
return &frameData{
|
||||
return &frameData4{
|
||||
ether: etherLayer,
|
||||
ip: ipLayer,
|
||||
device: dev,
|
||||
|
|
@ -78,4 +103,49 @@ func newFrameData(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Add DHCPServer.serveEther6.
|
||||
// newFrameData6 creates a new [frameData6] with layers extracted from pkt. It
|
||||
// returns nil if the packet is not an Ethernet or IPv6 packet, or if the
|
||||
// network device has no addresses. logger, pkt, and dev must not be nil.
|
||||
func newFrameData6(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
pkt gopacket.Packet,
|
||||
dev NetworkDevice,
|
||||
duid *layers.DHCPv6DUID,
|
||||
) (fd *frameData6) {
|
||||
addrs := dev.Addresses()
|
||||
if len(addrs) == 0 {
|
||||
logger.ErrorContext(ctx, "no addresses for network device")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
etherLayer, ok := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
|
||||
if !ok {
|
||||
actual := pkt.Layers()
|
||||
logger.DebugContext(ctx, "skipping non-ethernet packet", "layers", actual)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ipLayer, ok := pkt.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
|
||||
if !ok {
|
||||
actual := pkt.Layers()
|
||||
logger.DebugContext(ctx, "skipping non-ipv6 packet", "layers", actual)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
addr, ok := netip.AddrFromSlice(ipLayer.DstIP)
|
||||
if !ok || !slices.Contains(addrs, addr) {
|
||||
addr = addrs[0]
|
||||
}
|
||||
|
||||
return &frameData6{
|
||||
ether: etherLayer,
|
||||
ip: ipLayer,
|
||||
duid: duid,
|
||||
device: dev,
|
||||
localAddr: addr,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import (
|
|||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// serveV4 handles the ethernet packet of IPv4 type. iface and pkt must not be
|
||||
// nil. iface and fd must not be nil. pkt must be an IPv4 packet.
|
||||
// serveV4 handles the ethernet packet of IPv4 type. iface must not be nil, fd
|
||||
// must be valid, pkt must be an IPv4 packet.
|
||||
func (srv *DHCPServer) serveV4(
|
||||
ctx context.Context,
|
||||
iface *dhcpInterfaceV4,
|
||||
pkt gopacket.Packet,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "serving dhcpv4: %w") }()
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ func (srv *DHCPServer) serveV4(
|
|||
// messages are handled by all interfaces concurrently, as those offer addresses
|
||||
// for the independent networks. The DHCPREQUEST, DHCPRELEASE, and DHCPDECLINE
|
||||
// messages are handled by the appropriate interface according to the client's
|
||||
// choice. req and fd must not be nil, typ should be one of:
|
||||
// choice. req must not be nil, fd must be valid, typ should be one of:
|
||||
// - [layers.DHCPMsgTypeDiscover]
|
||||
// - [layers.DHCPMsgTypeRequest]
|
||||
// - [layers.DHCPMsgTypeRelease]
|
||||
|
|
@ -64,7 +64,7 @@ func (iface *dhcpInterfaceV4) handleDHCPv4(
|
|||
ctx context.Context,
|
||||
typ layers.DHCPMsgType,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
) (err error) {
|
||||
switch typ {
|
||||
case layers.DHCPMsgTypeDiscover:
|
||||
|
|
@ -84,11 +84,11 @@ func (iface *dhcpInterfaceV4) handleDHCPv4(
|
|||
}
|
||||
|
||||
// handleDiscover handles messages of type DHCPDISCOVER. req must be a
|
||||
// DHCPDISCOVER message, fd must not be nil.
|
||||
// DHCPDISCOVER message, fd must be valid.
|
||||
func (iface *dhcpInterfaceV4) handleDiscover(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
) {
|
||||
l := iface.common.logger
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ func (iface *dhcpInterfaceV4) handleDiscover(
|
|||
}
|
||||
|
||||
// handleRequest handles the DHCPv4 message of DHCPREQUEST type. req must be a
|
||||
// DHCPREQUEST message. req and fd must not be nil.
|
||||
// DHCPREQUEST message. req must not be nil, fd must be valid.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.
|
||||
//
|
||||
|
|
@ -133,7 +133,7 @@ func (iface *dhcpInterfaceV4) handleDiscover(
|
|||
func (iface *dhcpInterfaceV4) handleRequest(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
) {
|
||||
srvID, hasSrvID := serverID4(req)
|
||||
reqIP, hasReqIP := requestedIPv4(req)
|
||||
|
|
@ -181,12 +181,12 @@ func (iface *dhcpInterfaceV4) handleRequest(
|
|||
}
|
||||
|
||||
// handleSelecting handles messages of type DHCPREQUEST in SELECTING state. req
|
||||
// must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd must
|
||||
// not be nil.
|
||||
// must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd must be
|
||||
// valid.
|
||||
func (iface *dhcpInterfaceV4) handleSelecting(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
reqIP netip.Addr,
|
||||
) {
|
||||
l := iface.common.logger
|
||||
|
|
@ -235,11 +235,11 @@ func (iface *dhcpInterfaceV4) handleSelecting(
|
|||
|
||||
// handleInitReboot handles messages of type DHCPREQUEST in INIT-REBOOT state.
|
||||
// req must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd
|
||||
// must not be nil.
|
||||
// must be valid.
|
||||
func (iface *dhcpInterfaceV4) handleInitReboot(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
reqIP netip.Addr,
|
||||
) {
|
||||
l := iface.common.logger
|
||||
|
|
@ -282,11 +282,11 @@ func (iface *dhcpInterfaceV4) handleInitReboot(
|
|||
|
||||
// handleRenew handles messages of type DHCPREQUEST in RENEWING or REBINDING
|
||||
// state. req must be a DHCPREQUEST message, ip should be a previously leased
|
||||
// address, fd must not be nil.
|
||||
// address, fd must be valid.
|
||||
func (iface *dhcpInterfaceV4) handleRenew(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
ip netip.Addr,
|
||||
) {
|
||||
l := iface.common.logger
|
||||
|
|
|
|||
|
|
@ -10,16 +10,21 @@ import (
|
|||
)
|
||||
|
||||
// serveV6 handles the ethernet packet of IPv6 type. iface and pkt must not be
|
||||
// nil. ctx must contain a [frameData] accessible with [frameDataFromContext].
|
||||
// nil. iface and fd must be valid. pkt must be an IPv6 packet.
|
||||
//
|
||||
//lint:ignore U1000 TODO(e.burkov): Use.
|
||||
func (srv *DHCPServer) serveV6(
|
||||
ctx context.Context,
|
||||
_ *dhcpInterfaceV6,
|
||||
iface *dhcpInterfaceV6,
|
||||
pkt gopacket.Packet,
|
||||
fd *frameData6,
|
||||
) (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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import (
|
|||
//
|
||||
// TODO(e.burkov): Identify the client by the hardware address and the client
|
||||
// identifier from the DHCP messages.
|
||||
//
|
||||
// TODO(e.burkov): Identify IPv6 clients with DUID.
|
||||
type macKey any
|
||||
|
||||
// macToKey converts mac into macKey, which is used as the key for the lease
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import (
|
|||
// [websvc].
|
||||
//
|
||||
// TODO(e.burkov): Add validation method.
|
||||
//
|
||||
// TODO(e.burkov): Migrate to add DUID and IAID fields for DHCPv6 leases.
|
||||
type Lease struct {
|
||||
// IP is the IP address leased to the client. It must not be empty.
|
||||
IP netip.Addr
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package dhcpsvc
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
|
|
@ -63,10 +64,18 @@ type NetworkDevice interface {
|
|||
// No methods of a device should be called after Close.
|
||||
io.Closer
|
||||
|
||||
// Addresses returns all IP addresses assigned to the device.
|
||||
// Addresses returns all IP addresses assigned to the device. It must
|
||||
// return at least one valid address, unless the implementation documents
|
||||
// the opposite.
|
||||
Addresses() (ips []netip.Addr)
|
||||
|
||||
// LinkType returns the link type of the network interface.
|
||||
// HardwareAddr returns the hardware (MAC) address of the device. It must
|
||||
// return a valid hardware address, unless the implementation documents the
|
||||
// opposite.
|
||||
HardwareAddr() (hw net.HardwareAddr)
|
||||
|
||||
// LinkType returns the link type of the network interface. It must return
|
||||
// a valid link type, unless the implementation documents the opposite.
|
||||
LinkType() (lt layers.LinkType)
|
||||
|
||||
// WritePacketData writes a serialized packet to the network interface.
|
||||
|
|
@ -98,6 +107,12 @@ func (EmptyNetworkDevice) Addresses() (ips []netip.Addr) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// HardwareAddr implements the [NetworkDevice] interface for
|
||||
// [EmptyNetworkDevice]. It always returns nil.
|
||||
func (EmptyNetworkDevice) HardwareAddr() (hw net.HardwareAddr) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LinkType implements the [NetworkDevice] interface for [EmptyNetworkDevice].
|
||||
// It always returns [layers.LinkTypeNull].
|
||||
func (EmptyNetworkDevice) LinkType() (lt layers.LinkType) {
|
||||
|
|
@ -110,11 +125,42 @@ func (EmptyNetworkDevice) WritePacketData(_ []byte) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// frameData stores the Ethernet and IPv4 layers of the incoming packet, and
|
||||
// the network device that the packet was received from.
|
||||
type frameData struct {
|
||||
ether *layers.Ethernet
|
||||
ip *layers.IPv4
|
||||
device NetworkDevice
|
||||
// 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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package dhcpsvc_test
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
|
@ -45,6 +46,7 @@ type testNetworkDevice struct {
|
|||
onReadPacketData func() (data []byte, ci gopacket.CaptureInfo, err error)
|
||||
onClose func() (err error)
|
||||
onAddresses func() (ips []netip.Addr)
|
||||
onHardwareAddr func() (hw net.HardwareAddr)
|
||||
onLinkType func() (lt layers.LinkType)
|
||||
onWritePacketData func(data []byte) (err error)
|
||||
}
|
||||
|
|
@ -69,6 +71,12 @@ func (nd *testNetworkDevice) Addresses() (ips []netip.Addr) {
|
|||
return nd.onAddresses()
|
||||
}
|
||||
|
||||
// HardwareAddr implements the [dhcpsvc.NetworkDevice] interface for
|
||||
// *testNetworkDevice.
|
||||
func (nd *testNetworkDevice) HardwareAddr() (hw net.HardwareAddr) {
|
||||
return nd.onHardwareAddr()
|
||||
}
|
||||
|
||||
// WritePacketData implements the [dhcpsvc.NetworkDevice] interface for
|
||||
// *testNetworkDevice.
|
||||
func (nd *testNetworkDevice) WritePacketData(data []byte) (err error) {
|
||||
|
|
@ -83,8 +91,9 @@ 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]. Incoming packets are received from inCh and
|
||||
// outgoing packets are sent to outCh.
|
||||
// type [layers.LinkTypeEthernet] and a hardware address [testHWIface].
|
||||
// Incoming packets are received from inCh and outgoing packets are sent to
|
||||
// outCh.
|
||||
func newTestNetworkDeviceManager(
|
||||
tb testing.TB,
|
||||
deviceName string,
|
||||
|
|
@ -116,8 +125,9 @@ func newTestNetworkDeviceManager(
|
|||
}
|
||||
|
||||
// newTestNetworkDevice creates a network device for testing. It has a link
|
||||
// type [layers.LinkTypeEthernet]. Incoming packets are received from inCh and
|
||||
// outgoing packets are sent to outCh.
|
||||
// type [layers.LinkTypeEthernet] and a hardware address [testHWIface].
|
||||
// Incoming packets are received from inCh and outgoing packets are sent to
|
||||
// outCh.
|
||||
func newTestNetworkDevice(
|
||||
tb testing.TB,
|
||||
addr netip.Addr,
|
||||
|
|
@ -158,6 +168,10 @@ func newTestNetworkDevice(
|
|||
return []netip.Addr{addr}
|
||||
}
|
||||
|
||||
onHardwareAddr := func() (hw net.HardwareAddr) {
|
||||
return testHWIface
|
||||
}
|
||||
|
||||
onLinkType := func() (lt layers.LinkType) {
|
||||
return layers.LinkTypeEthernet
|
||||
}
|
||||
|
|
@ -172,6 +186,7 @@ func newTestNetworkDevice(
|
|||
onReadPacketData: onReadPacketData,
|
||||
onClose: onClose,
|
||||
onAddresses: onAddresses,
|
||||
onHardwareAddr: onHardwareAddr,
|
||||
onLinkType: onLinkType,
|
||||
onWritePacketData: onWritePacketData,
|
||||
}, in, out
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ func (iface *dhcpInterfaceV4) appendRequestedOptions(
|
|||
// newRespOptions creates the basic options for a DHCP response. fd must not be
|
||||
// nil, mt must be a valid DHCP response message type. idOpt is an optional
|
||||
// client identifier from the corresponding request.
|
||||
func newRespOptions(mt layers.DHCPMsgType, fd *frameData, idOpt []byte) (opts layers.DHCPOptions) {
|
||||
func newRespOptions(mt layers.DHCPMsgType, fd *frameData4, idOpt []byte) (opts layers.DHCPOptions) {
|
||||
opts = layers.DHCPOptions{
|
||||
msgTypeOptions4[mt],
|
||||
layers.NewDHCPOption(layers.DHCPOptServerID, fd.localAddr.AsSlice()),
|
||||
|
|
|
|||
210
internal/dhcpsvc/options6.go
Normal file
210
internal/dhcpsvc/options6.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/validate"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// iaNAMinLen is the minimum length of an IA_NA option data field, in bytes.
|
||||
//
|
||||
// See RFC 9915 Section 21.4.
|
||||
const iaNAMinLen = 12
|
||||
|
||||
// iaNAOption represents a parsed IA_NA (Identity Association for Non-temporary
|
||||
// Addresses) option.
|
||||
//
|
||||
// See RFC 9915 Section 21.4.
|
||||
type iaNAOption struct {
|
||||
// nested are the IA Address options nested within this IA_NA.
|
||||
nested []iaAddrOption
|
||||
|
||||
// iaid is the Identity Association IDentifier, a 4-octet value uniquely
|
||||
// identifying this IA within the client.
|
||||
iaid uint32
|
||||
|
||||
// t1 is the time after which the client must contact the same server to
|
||||
// extend the lifetimes of the addresses in this IA.
|
||||
t1 time.Duration
|
||||
|
||||
// t2 is the time after which the client may contact any available server to
|
||||
// extend the lifetimes.
|
||||
t2 time.Duration
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ encoding.BinaryUnmarshaler = (*iaNAOption)(nil)
|
||||
|
||||
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface for
|
||||
// *iaNAOption. data should have the following format:
|
||||
//
|
||||
// 0 1 2 3
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | IAID (4 octets) |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | T1 |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | T2 |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | |
|
||||
// . IA_NA-options .
|
||||
// . .
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
func (opt *iaNAOption) UnmarshalBinary(data []byte) (err error) {
|
||||
err = validate.NoLessThan("data length", len(data), iaNAMinLen)
|
||||
if err != nil {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
opt.iaid = binary.BigEndian.Uint32(data[0:4])
|
||||
opt.t1 = time.Duration(binary.BigEndian.Uint32(data[4:8])) * time.Second
|
||||
opt.t2 = time.Duration(binary.BigEndian.Uint32(data[8:12])) * time.Second
|
||||
|
||||
// Parse the nested options that follow the fixed fields.
|
||||
nested := data[iaNAMinLen:]
|
||||
for i := 0; len(nested) >= 4; i++ {
|
||||
code := layers.DHCPv6Opt(binary.BigEndian.Uint16(nested[0:2]))
|
||||
l := int(binary.BigEndian.Uint16(nested[2:4]))
|
||||
|
||||
err = validate.NoGreaterThan("nested option length", l, len(nested)-4)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nested option at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
if code == layers.DHCPv6OptIAAddr {
|
||||
addr := iaAddrOption{}
|
||||
err = addr.UnmarshalBinary(nested[4 : 4+l])
|
||||
if err != nil {
|
||||
return fmt.Errorf("nested ia_addr at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
opt.nested = append(opt.nested, addr)
|
||||
}
|
||||
|
||||
nested = nested[4+l:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode serializes ia into a DHCPv6 IA_NA option. Each contained
|
||||
// [iaAddrOption] is encoded as a nested IA Address option.
|
||||
//
|
||||
// TODO(e.burkov): Use.
|
||||
func (opt iaNAOption) Encode() (iaOpt layers.DHCPv6Option) {
|
||||
// Each nested IA Address option: code (2) + length (2) + data (24).
|
||||
const nestedAddrSize = 2 + 2 + iaAddrDataLen
|
||||
|
||||
data := make([]byte, 0, iaNAMinLen+len(opt.nested)*nestedAddrSize)
|
||||
|
||||
data = binary.BigEndian.AppendUint32(data, opt.iaid)
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(opt.t1.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(opt.t2.Seconds()))
|
||||
|
||||
for _, addr := range opt.nested {
|
||||
data = addr.append(data)
|
||||
}
|
||||
|
||||
return layers.NewDHCPv6Option(layers.DHCPv6OptIANA, data)
|
||||
}
|
||||
|
||||
// iaAddrDataLen is the minimum length of an IA Address option data field, which
|
||||
// is encoded [iaAddrOption], in bytes, excluding any nested options. It
|
||||
// consists of the IPv6 address (16 bytes) and the preferred and valid lifetimes
|
||||
// (4 bytes each).
|
||||
const iaAddrDataLen = 24
|
||||
|
||||
// iaAddrOption represents a parsed IA Address option.
|
||||
//
|
||||
// See RFC 9915 Section 21.6.
|
||||
type iaAddrOption struct {
|
||||
// addr is the IPv6 address.
|
||||
addr netip.Addr
|
||||
|
||||
// preferredLifetime is the preferred lifetime of the address. When it is
|
||||
// zero, the address is deprecated.
|
||||
preferredLifetime time.Duration
|
||||
|
||||
// validLifetime is the valid lifetime of the address. When it is zero, the
|
||||
// address is no longer valid.
|
||||
validLifetime time.Duration
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ encoding.BinaryUnmarshaler = (*iaAddrOption)(nil)
|
||||
|
||||
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface for
|
||||
// *iaAddrOption. Nested options within IA Address, if any, are
|
||||
// ignored. data should have the following format:
|
||||
//
|
||||
// 0 1 2 3
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | |
|
||||
// | IPv6-address |
|
||||
// | |
|
||||
// | |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | preferred-lifetime |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | valid-lifetime |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// . .
|
||||
// . IAaddr-options .
|
||||
// . .
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
func (ia *iaAddrOption) UnmarshalBinary(data []byte) (err error) {
|
||||
err = validate.NoLessThan("data length", len(data), iaAddrDataLen)
|
||||
if err != nil {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
ia.addr, ok = netip.AddrFromSlice(data[0:16])
|
||||
if !ok {
|
||||
return fmt.Errorf("ia_addr: invalid ipv6 address bytes")
|
||||
}
|
||||
|
||||
ia.preferredLifetime = time.Duration(binary.BigEndian.Uint32(data[16:20])) * time.Second
|
||||
ia.validLifetime = time.Duration(binary.BigEndian.Uint32(data[20:24])) * time.Second
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// append returns the data portion of the IA Address option encoding,
|
||||
// suitable for use as a nested option inside an IA_NA.
|
||||
func (ia iaAddrOption) append(orig []byte) (data []byte) {
|
||||
data = orig
|
||||
|
||||
data = binary.BigEndian.AppendUint16(data, uint16(layers.DHCPv6OptIAAddr))
|
||||
data = binary.BigEndian.AppendUint16(data, uint16(iaAddrDataLen))
|
||||
|
||||
// [netip.Addr.AppendBinary] never returns errors.
|
||||
data, _ = ia.addr.AppendBinary(data)
|
||||
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(ia.preferredLifetime.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(ia.validLifetime.Seconds()))
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// newServerDUID creates a DUID-LL (Link-Layer Address) from the given MAC
|
||||
// address per RFC 9915 §11.4. The result is deterministic: the same MAC
|
||||
// address always produces the same DUID, satisfying the stability requirement
|
||||
// of §11.
|
||||
func newServerDUID(mac net.HardwareAddr) (duid *layers.DHCPv6DUID) {
|
||||
return &layers.DHCPv6DUID{
|
||||
Type: layers.DHCPv6DUIDTypeLL,
|
||||
HardwareType: HardwareTypeEthernet,
|
||||
LinkLayerAddress: mac,
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,27 @@ import (
|
|||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// Port numbers for DHCPv4.
|
||||
//
|
||||
// See RFC 2131 Section 4.1.
|
||||
const (
|
||||
// ServerPortV4 is the standard DHCPv4 server port.
|
||||
ServerPortV4 layers.UDPPort = 67
|
||||
|
||||
// ClientPortV4 is the standard DHCPv4 client port.
|
||||
ClientPortV4 layers.UDPPort = 68
|
||||
)
|
||||
|
||||
const (
|
||||
// IPv4DefaultTTL is the default Time to Live value in seconds as
|
||||
// recommended by RFC 1700.
|
||||
IPv4DefaultTTL = 64
|
||||
|
||||
// IPProtoVersion is the IP internetwork general protocol version number as
|
||||
// defined by RFC 1700.
|
||||
IPProtoVersion = 4
|
||||
)
|
||||
|
||||
// IPv4Config is the interface-specific configuration for DHCPv4.
|
||||
type IPv4Config struct {
|
||||
// Clock is used to get current time. It should not be nil.
|
||||
|
|
@ -176,6 +197,8 @@ func (srv *DHCPServer) newDHCPInterfaceV4(
|
|||
// TODO(e.burkov): Add a helper for converting [netip.Addr] to subnet mask
|
||||
// to [netutil].
|
||||
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
||||
|
||||
// Ignore the error since it's already checked in [IPv4Config.Validate].
|
||||
addrSpace, _ := newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
|
||||
iface = &dhcpInterfaceV4{
|
||||
|
|
@ -207,14 +230,14 @@ func (iface *dhcpInterfaceV4) updateLease(ctx context.Context, lease *Lease) (er
|
|||
}
|
||||
|
||||
// respondOffer sends a DHCPOFFER message to the client. idOpt is expected to
|
||||
// be the value of the DHCP option Client Identifier, nil if not present. req,
|
||||
// fd, and lease must not be nil.
|
||||
// be the value of the DHCP option Client Identifier, nil if not present. req
|
||||
// and lease must not be nil, fd must be valid
|
||||
//
|
||||
// TODO(e.burkov): Consider merging with [respondACK].
|
||||
func (iface *dhcpInterfaceV4) respondOffer(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
lease *Lease,
|
||||
idOpt []byte,
|
||||
) {
|
||||
|
|
@ -231,15 +254,15 @@ func (iface *dhcpInterfaceV4) respondOffer(
|
|||
}
|
||||
|
||||
// respondACK sends a DHCPACK message to the client. idOpt is expected to be
|
||||
// the value of the DHCP option Client Identifier, nil if not present. req, fd,
|
||||
// and lease must not be nil.
|
||||
// the value of the DHCP option Client Identifier, nil if not present. req and
|
||||
// lease must not be nil, fd must be valid.
|
||||
//
|
||||
// TODO(e.burkov): Implement according to RFC, answer to DHCPINFORM
|
||||
// differently, when it's supported.
|
||||
func (iface *dhcpInterfaceV4) respondACK(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
lease *Lease,
|
||||
idOpt []byte,
|
||||
) {
|
||||
|
|
@ -257,13 +280,13 @@ func (iface *dhcpInterfaceV4) respondACK(
|
|||
|
||||
// respondNAK constructs and sends a DHCPNAK message to the client. idOpt is
|
||||
// expected to be the value of the DHCP option Client Identifier, nil if not
|
||||
// present. req, fd, and resp must not be nil.
|
||||
// present. req and resp must not be nil, fd must be valid.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.
|
||||
func (iface *dhcpInterfaceV4) respondNAK(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
idOpt []byte,
|
||||
) {
|
||||
// TODO(e.burkov): According to RFC 2131 we should add a message.
|
||||
|
|
@ -416,13 +439,13 @@ func (iface *dhcpInterfaceV4) reserveLease(
|
|||
// updateAndRespond updates the lease and sends a DHCPACK or DHCPNAK response to
|
||||
// the client according to the update result. idOpt is an expected to be the
|
||||
// value of the DHCP option Client Identifier, nil if not present. req must be
|
||||
// a DHCPREQUEST message, lease, l, and fd must not be nil.
|
||||
// a DHCPREQUEST message, lease, and l must not be nil, fd must be valid.
|
||||
func (iface *dhcpInterfaceV4) updateAndRespond(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
req *layers.DHCPv4,
|
||||
lease *Lease,
|
||||
fd *frameData,
|
||||
fd *frameData4,
|
||||
idOpt []byte,
|
||||
) {
|
||||
lease.Hostname = cmp.Or(hostname4(req), lease.Hostname)
|
||||
|
|
@ -438,32 +461,12 @@ func (iface *dhcpInterfaceV4) updateAndRespond(
|
|||
iface.respondACK(ctx, req, fd, lease, idOpt)
|
||||
}
|
||||
|
||||
const (
|
||||
// IPv4DefaultTTL is the default Time to Live value in seconds as
|
||||
// recommended by RFC 1700.
|
||||
IPv4DefaultTTL = 64
|
||||
|
||||
// IPProtoVersion is the IP internetwork general protocol version number as
|
||||
// defined by RFC 1700.
|
||||
IPProtoVersion = 4
|
||||
)
|
||||
|
||||
// Port numbers for DHCPv4.
|
||||
//
|
||||
// See RFC 2131 Section 4.1.
|
||||
const (
|
||||
// ServerPortV4 is the standard DHCPv4 server port.
|
||||
ServerPortV4 layers.UDPPort = 67
|
||||
|
||||
// ClientPortV4 is the standard DHCPv4 client port.
|
||||
ClientPortV4 layers.UDPPort = 68
|
||||
)
|
||||
|
||||
// FlagsBroadcast is the DHCPv4 message flags field with the broadcast bit set.
|
||||
const FlagsBroadcast uint16 = 1 << 15
|
||||
|
||||
// respond4 sends a DHCPv4 response. fd, req, and resp must not be nil.
|
||||
func respond4(fd *frameData, req, resp *layers.DHCPv4) (err error) {
|
||||
// respond4 sends a DHCPv4 response. req and resp must not be nil, fd must be
|
||||
// valid.
|
||||
func respond4(fd *frameData4, req, resp *layers.DHCPv4) (err error) {
|
||||
// TODO(e.burkov): Use pools for buffer and layers.
|
||||
buf := gopacket.NewSerializeBuffer()
|
||||
|
||||
|
|
@ -488,9 +491,9 @@ func respond4(fd *frameData, req, resp *layers.DHCPv4) (err error) {
|
|||
return fd.device.WritePacketData(buf.Bytes())
|
||||
}
|
||||
|
||||
// newIPv4UDPLayers creates new UDP and IP layers for DHCPv4 response. fd, req,
|
||||
// and resp must not be nil.
|
||||
func newIPv4UDPLayers(fd *frameData, req, resp *layers.DHCPv4) (ip *layers.IPv4, udp *layers.UDP) {
|
||||
// newIPv4UDPLayers creates new UDP and IP layers for DHCPv4 response. req and
|
||||
// resp must not be nil, fd must be valid.
|
||||
func newIPv4UDPLayers(fd *frameData4, req, resp *layers.DHCPv4) (ip *layers.IPv4, udp *layers.UDP) {
|
||||
var dstIP net.IP
|
||||
dstPort := ClientPortV4
|
||||
switch {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package dhcpsvc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
|
@ -14,7 +13,50 @@ import (
|
|||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// Port numbers for DHCPv6.
|
||||
//
|
||||
// See RFC 9915 Section 7.2.
|
||||
const (
|
||||
// ServerPortV6 is the standard DHCPv6 server port.
|
||||
ServerPortV6 layers.UDPPort = 547
|
||||
|
||||
// ClientPortV6 is the standard DHCPv6 client port.
|
||||
ClientPortV6 layers.UDPPort = 546
|
||||
)
|
||||
|
||||
// HardwareTypeEthernet is the IANA hardware type number for Ethernet, used in
|
||||
// DUID-LL and DUID-LLT construction. Its value is 1, encoded as a big-endian
|
||||
// uint16.
|
||||
//
|
||||
// See https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml#arp-parameters-2.
|
||||
//
|
||||
// TODO(e.burkov): Use.
|
||||
var HardwareTypeEthernet = []byte{0x00, 0x01}
|
||||
|
||||
// DHCPv6 multicast addresses.
|
||||
//
|
||||
// See RFC 9915 Section 7.1.
|
||||
var (
|
||||
// AllDHCPRelayAgentsAndServers is the well-known IPv6 multicast address
|
||||
// All_DHCP_Relay_Agents_and_Servers. Clients send messages to this address
|
||||
// to reach all servers on the local link.
|
||||
AllDHCPRelayAgentsAndServers = netip.MustParseAddr("ff02::1:2")
|
||||
|
||||
// AllDHCPServers is the well-known IPv6 multicast address All_DHCP_Servers.
|
||||
// Relay agents use this to reach all servers.
|
||||
AllDHCPServers = netip.MustParseAddr("ff05::1:3")
|
||||
)
|
||||
|
||||
// v6PrefLen is the length of prefix to match ip against.
|
||||
//
|
||||
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy implementation
|
||||
// where the allocated range constrained by the first address and the first
|
||||
// address with last byte set to 0xff. Proper prefixes should be used instead.
|
||||
const v6PrefLen = netutil.IPv6BitLen - 8
|
||||
|
||||
// IPv6Config is the interface-specific configuration for DHCPv6.
|
||||
//
|
||||
// TODO(e.burkov): Add RangeEnd and SubnetPrefix fields, and validate them.
|
||||
type IPv6Config struct {
|
||||
// RangeStart is the first address in the range to assign to DHCP clients.
|
||||
// It should be a valid IPv6 address.
|
||||
|
|
@ -52,29 +94,38 @@ func (c *IPv6Config) Validate() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if !c.RangeStart.Is6() {
|
||||
err = fmt.Errorf("range start %s should be a valid ipv6", c.RangeStart)
|
||||
errs = append(errs, err)
|
||||
errs := []error{
|
||||
validate.Positive("lease duration", c.LeaseDuration),
|
||||
}
|
||||
|
||||
if c.LeaseDuration <= 0 {
|
||||
err = fmt.Errorf("lease duration %s must be positive", c.LeaseDuration)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
errs = c.validateSubnet(errs)
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// validateSubnet validates the subnet configuration.
|
||||
//
|
||||
// TODO(e.burkov): Use [validate].
|
||||
func (c *IPv6Config) validateSubnet(orig []error) (errs []error) {
|
||||
errs = orig
|
||||
|
||||
if !c.RangeStart.Is6() {
|
||||
err := newMustErr("range start", "be a valid ipv6", c.RangeStart)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// dhcpInterfaceV6 is a DHCP interface for IPv6 address family.
|
||||
type dhcpInterfaceV6 struct {
|
||||
// common is the common part of any network interface within the DHCP
|
||||
// server.
|
||||
common *netInterface
|
||||
|
||||
// rangeStart is the first IP address in the range.
|
||||
rangeStart netip.Addr
|
||||
// subnetPrefix is the network prefix of the interface's IPv6 subnet. It is
|
||||
// used for on-link address determination.
|
||||
subnetPrefix netip.Prefix
|
||||
|
||||
// implicitOpts are the DHCPv6 options listed in RFC 8415 (and others) and
|
||||
// initialized with default values. It must not have intersections with
|
||||
|
|
@ -85,6 +136,16 @@ type dhcpInterfaceV6 struct {
|
|||
// intersections with implicitOpts.
|
||||
explicitOpts layers.DHCPv6Options
|
||||
|
||||
// t1 is the pre-computed T1 value (0.5 × LeaseDuration) per RFC 9915 §21.4.
|
||||
// It is the time after which the client should contact the same server to
|
||||
// extend the lease.
|
||||
t1 time.Duration
|
||||
|
||||
// t2 is the pre-computed T2 value (0.8 × LeaseDuration) per RFC 9915 §21.4.
|
||||
// It is the time after which the client may contact any server to extend
|
||||
// the lease.
|
||||
t2 time.Duration
|
||||
|
||||
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
|
||||
// flags.
|
||||
raSLAACOnly bool
|
||||
|
|
@ -108,16 +169,29 @@ func (srv *DHCPServer) newDHCPInterfaceV6(
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Migrate the configuration to use proper range start,
|
||||
// end, and subnet prefix.
|
||||
rangeEndData := conf.RangeStart.As16()
|
||||
rangeEndData[15] = 0xff
|
||||
|
||||
// TODO(e.burkov): Validate the range end and subnet prefix against the
|
||||
// range start during configuration validation.
|
||||
addrSpace, _ := newIPRange(conf.RangeStart, netip.AddrFrom16(rangeEndData))
|
||||
|
||||
iface = &dhcpInterfaceV6{
|
||||
rangeStart: conf.RangeStart,
|
||||
common: &netInterface{
|
||||
logger: l,
|
||||
leases: map[macKey]*Lease{},
|
||||
indexMu: srv.leasesMu,
|
||||
index: srv.leases,
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
logger: l,
|
||||
leases: map[macKey]*Lease{},
|
||||
indexMu: srv.leasesMu,
|
||||
index: srv.leases,
|
||||
name: name,
|
||||
addrSpace: addrSpace,
|
||||
leasedOffsets: newBitSet(),
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
subnetPrefix: netip.PrefixFrom(conf.RangeStart, v6PrefLen),
|
||||
t1: conf.LeaseDuration / 2,
|
||||
t2: conf.LeaseDuration * 4 / 5,
|
||||
raSLAACOnly: conf.RASLAACOnly,
|
||||
raAllowSLAAC: conf.RAAllowSLAAC,
|
||||
}
|
||||
|
|
@ -129,20 +203,11 @@ func (srv *DHCPServer) newDHCPInterfaceV6(
|
|||
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
|
||||
type dhcpInterfacesV6 []*dhcpInterfaceV6
|
||||
|
||||
// find returns the first network interface within ifaces containing ip. It
|
||||
// returns false if there is no such interface.
|
||||
// find returns the first network interface within ifaces whose subnet prefix
|
||||
// contains ip. It returns false if there is no such interface.
|
||||
func (ifaces dhcpInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
|
||||
// prefLen is the length of prefix to match ip against.
|
||||
//
|
||||
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
|
||||
// implementation where the allocated range constrained by the first address
|
||||
// and the first address with last byte set to 0xff. Proper prefixes should
|
||||
// be used instead.
|
||||
const prefLen = netutil.IPv6BitLen - 8
|
||||
|
||||
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV6) (contains bool) {
|
||||
return !ip.Less(iface.rangeStart) &&
|
||||
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
|
||||
return iface.subnetPrefix.Contains(ip)
|
||||
})
|
||||
if i < 0 {
|
||||
return nil, false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue