Skip to content

Commit b08e587

Browse files
committed
Pull request 2634: AGDNS-3863-gopacket-dhcp-vol.23
Updates AdguardTeam#4923. Squashed commit of the following: commit 305d3ba Merge: c7512c8 9e153fb 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 c7512c8 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 21 19:11:39 2026 +0300 dhcpsvc: fix doc commit f0fe3b6 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 21 15:10:16 2026 +0300 dhcpsvc: imp docs commit 8eeea47 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 21 14:53:56 2026 +0300 dhcpsvc: imp code, docs commit 96cf3b3 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Mon Apr 20 15:30:58 2026 +0300 dhcpsvc: add dhcpv6 handle, imp code commit e94cb9e Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed Apr 15 15:56:38 2026 +0300 dhcpsvc: add ipv6 consts
1 parent 9e153fb commit b08e587

14 files changed

Lines changed: 528 additions & 105 deletions

internal/dhcpsvc/config_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,15 @@ func TestIPv6Config_Validate(t *testing.T) {
140140
RangeStart: testIPv4Conf.GatewayIP,
141141
LeaseDuration: 1 * time.Hour,
142142
},
143-
wantErrMsg: "range start " + testGatewayIPv4Str + " should be a valid ipv6",
143+
wantErrMsg: "range start " + testGatewayIPv4Str + " must be a valid ipv6",
144144
}, {
145145
name: "bad_lease_duration",
146146
conf: &dhcpsvc.IPv6Config{
147147
Enabled: true,
148148
RangeStart: netip.MustParseAddr(testRangeStartV6Str),
149149
LeaseDuration: 0,
150150
},
151-
wantErrMsg: "lease duration 0s must be positive",
151+
wantErrMsg: "lease duration: not positive: 0s",
152152
}, {
153153
name: "valid",
154154
conf: &dhcpsvc.IPv6Config{

internal/dhcpsvc/db.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ type dataLeases struct {
3434
}
3535

3636
// dbLease is the structure of stored lease.
37+
//
38+
// TODO(e.burkov): Migrate to add DUID and IAID fields for DHCPv6 leases.
3739
type dbLease struct {
3840
Expiry string `json:"expires"`
3941
IP netip.Addr `json:"ip"`

internal/dhcpsvc/dhcpsvc_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ const (
109109
testAnotherRangeStartV6Str = "2001:db9::1"
110110
)
111111

112+
// testHWIface is the test MAC address of a test network interface.
113+
var testHWIface = net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
114+
112115
var (
113116
// testIPv4Conf is a common valid IPv4 part of the interface configuration
114117
// for tests.

internal/dhcpsvc/handle.go

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (srv *DHCPServer) serveEther4(ctx context.Context, iface *dhcpInterfaceV4,
2121
src := gopacket.NewPacketSource(nd, nd.LinkType())
2222

2323
for pkt := range src.Packets() {
24-
fd := newFrameData(ctx, srv.logger, pkt, nd)
24+
fd := newFrameData4(ctx, srv.logger, pkt, nd)
2525
if fd == nil {
2626
continue
2727
}
@@ -33,15 +33,40 @@ func (srv *DHCPServer) serveEther4(ctx context.Context, iface *dhcpInterfaceV4,
3333
}
3434
}
3535

36-
// newFrameData creates a new frameData with layers extracted from pkt. It
36+
// serveEther6 handles the incoming ethernet packets and dispatches them to the
37+
// appropriate handler. It's used to run in a separate goroutine as it blocks
38+
// until packets channel is closed. iface and nd must not be nil. nd must have
39+
// at least a single address returned by its Addresses method.
40+
//
41+
//lint:ignore U1000 TODO(e.burkov): Use.
42+
func (srv *DHCPServer) serveEther6(ctx context.Context, iface *dhcpInterfaceV6, nd NetworkDevice) {
43+
defer slogutil.RecoverAndLog(ctx, srv.logger)
44+
45+
src := gopacket.NewPacketSource(nd, nd.LinkType())
46+
srvDUID := newServerDUID(nd.HardwareAddr())
47+
48+
for pkt := range src.Packets() {
49+
fd := newFrameData6(ctx, srv.logger, pkt, nd, srvDUID)
50+
if fd == nil {
51+
continue
52+
}
53+
54+
err := srv.serveV6(ctx, iface, pkt, fd)
55+
if err != nil {
56+
srv.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
57+
}
58+
}
59+
}
60+
61+
// newFrameData4 creates a new [frameData4] with layers extracted from pkt. It
3762
// returns nil if the packet is not an Ethernet or IPv4 packet, or if the
3863
// network device has no addresses. logger, pkt, and dev must not be nil.
39-
func newFrameData(
64+
func newFrameData4(
4065
ctx context.Context,
4166
logger *slog.Logger,
4267
pkt gopacket.Packet,
4368
dev NetworkDevice,
44-
) (fd *frameData) {
69+
) (fd *frameData4) {
4570
addrs := dev.Addresses()
4671
if len(addrs) == 0 {
4772
logger.ErrorContext(ctx, "no addresses for network device")
@@ -70,12 +95,57 @@ func newFrameData(
7095
addr = addrs[0]
7196
}
7297

73-
return &frameData{
98+
return &frameData4{
7499
ether: etherLayer,
75100
ip: ipLayer,
76101
device: dev,
77102
localAddr: addr,
78103
}
79104
}
80105

81-
// TODO(e.burkov): Add DHCPServer.serveEther6.
106+
// newFrameData6 creates a new [frameData6] with layers extracted from pkt. It
107+
// returns nil if the packet is not an Ethernet or IPv6 packet, or if the
108+
// network device has no addresses. logger, pkt, and dev must not be nil.
109+
func newFrameData6(
110+
ctx context.Context,
111+
logger *slog.Logger,
112+
pkt gopacket.Packet,
113+
dev NetworkDevice,
114+
duid *layers.DHCPv6DUID,
115+
) (fd *frameData6) {
116+
addrs := dev.Addresses()
117+
if len(addrs) == 0 {
118+
logger.ErrorContext(ctx, "no addresses for network device")
119+
120+
return nil
121+
}
122+
123+
etherLayer, ok := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
124+
if !ok {
125+
actual := pkt.Layers()
126+
logger.DebugContext(ctx, "skipping non-ethernet packet", "layers", actual)
127+
128+
return nil
129+
}
130+
131+
ipLayer, ok := pkt.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
132+
if !ok {
133+
actual := pkt.Layers()
134+
logger.DebugContext(ctx, "skipping non-ipv6 packet", "layers", actual)
135+
136+
return nil
137+
}
138+
139+
addr, ok := netip.AddrFromSlice(ipLayer.DstIP)
140+
if !ok || !slices.Contains(addrs, addr) {
141+
addr = addrs[0]
142+
}
143+
144+
return &frameData6{
145+
ether: etherLayer,
146+
ip: ipLayer,
147+
duid: duid,
148+
device: dev,
149+
localAddr: addr,
150+
}
151+
}

internal/dhcpsvc/handler4.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import (
1212
"github.com/google/gopacket/layers"
1313
)
1414

15-
// serveV4 handles the ethernet packet of IPv4 type. iface and pkt must not be
16-
// nil. iface and fd must not be nil. pkt must be an IPv4 packet.
15+
// serveV4 handles the ethernet packet of IPv4 type. iface must not be nil, fd
16+
// must be valid, pkt must be an IPv4 packet.
1717
func (srv *DHCPServer) serveV4(
1818
ctx context.Context,
1919
iface *dhcpInterfaceV4,
2020
pkt gopacket.Packet,
21-
fd *frameData,
21+
fd *frameData4,
2222
) (err error) {
2323
defer func() { err = errors.Annotate(err, "serving dhcpv4: %w") }()
2424

@@ -55,7 +55,7 @@ func (srv *DHCPServer) serveV4(
5555
// messages are handled by all interfaces concurrently, as those offer addresses
5656
// for the independent networks. The DHCPREQUEST, DHCPRELEASE, and DHCPDECLINE
5757
// messages are handled by the appropriate interface according to the client's
58-
// choice. req and fd must not be nil, typ should be one of:
58+
// choice. req must not be nil, fd must be valid, typ should be one of:
5959
// - [layers.DHCPMsgTypeDiscover]
6060
// - [layers.DHCPMsgTypeRequest]
6161
// - [layers.DHCPMsgTypeRelease]
@@ -64,7 +64,7 @@ func (iface *dhcpInterfaceV4) handleDHCPv4(
6464
ctx context.Context,
6565
typ layers.DHCPMsgType,
6666
req *layers.DHCPv4,
67-
fd *frameData,
67+
fd *frameData4,
6868
) (err error) {
6969
switch typ {
7070
case layers.DHCPMsgTypeDiscover:
@@ -84,11 +84,11 @@ func (iface *dhcpInterfaceV4) handleDHCPv4(
8484
}
8585

8686
// handleDiscover handles messages of type DHCPDISCOVER. req must be a
87-
// DHCPDISCOVER message, fd must not be nil.
87+
// DHCPDISCOVER message, fd must be valid.
8888
func (iface *dhcpInterfaceV4) handleDiscover(
8989
ctx context.Context,
9090
req *layers.DHCPv4,
91-
fd *frameData,
91+
fd *frameData4,
9292
) {
9393
l := iface.common.logger
9494

@@ -125,15 +125,15 @@ func (iface *dhcpInterfaceV4) handleDiscover(
125125
}
126126

127127
// handleRequest handles the DHCPv4 message of DHCPREQUEST type. req must be a
128-
// DHCPREQUEST message. req and fd must not be nil.
128+
// DHCPREQUEST message. req must not be nil, fd must be valid.
129129
//
130130
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.
131131
//
132132
// TODO(e.burkov): Remove allocated leases after client have chosen one.
133133
func (iface *dhcpInterfaceV4) handleRequest(
134134
ctx context.Context,
135135
req *layers.DHCPv4,
136-
fd *frameData,
136+
fd *frameData4,
137137
) {
138138
srvID, hasSrvID := serverID4(req)
139139
reqIP, hasReqIP := requestedIPv4(req)
@@ -181,12 +181,12 @@ func (iface *dhcpInterfaceV4) handleRequest(
181181
}
182182

183183
// handleSelecting handles messages of type DHCPREQUEST in SELECTING state. req
184-
// must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd must
185-
// not be nil.
184+
// must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd must be
185+
// valid.
186186
func (iface *dhcpInterfaceV4) handleSelecting(
187187
ctx context.Context,
188188
req *layers.DHCPv4,
189-
fd *frameData,
189+
fd *frameData4,
190190
reqIP netip.Addr,
191191
) {
192192
l := iface.common.logger
@@ -235,11 +235,11 @@ func (iface *dhcpInterfaceV4) handleSelecting(
235235

236236
// handleInitReboot handles messages of type DHCPREQUEST in INIT-REBOOT state.
237237
// req must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd
238-
// must not be nil.
238+
// must be valid.
239239
func (iface *dhcpInterfaceV4) handleInitReboot(
240240
ctx context.Context,
241241
req *layers.DHCPv4,
242-
fd *frameData,
242+
fd *frameData4,
243243
reqIP netip.Addr,
244244
) {
245245
l := iface.common.logger
@@ -282,11 +282,11 @@ func (iface *dhcpInterfaceV4) handleInitReboot(
282282

283283
// handleRenew handles messages of type DHCPREQUEST in RENEWING or REBINDING
284284
// state. req must be a DHCPREQUEST message, ip should be a previously leased
285-
// address, fd must not be nil.
285+
// address, fd must be valid.
286286
func (iface *dhcpInterfaceV4) handleRenew(
287287
ctx context.Context,
288288
req *layers.DHCPv4,
289-
fd *frameData,
289+
fd *frameData4,
290290
ip netip.Addr,
291291
) {
292292
l := iface.common.logger

internal/dhcpsvc/handler6.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ import (
1010
)
1111

1212
// serveV6 handles the ethernet packet of IPv6 type. iface and pkt must not be
13-
// nil. ctx must contain a [frameData] accessible with [frameDataFromContext].
13+
// nil. iface and fd must be valid. pkt must be an IPv6 packet.
1414
//
1515
//lint:ignore U1000 TODO(e.burkov): Use.
1616
func (srv *DHCPServer) serveV6(
1717
ctx context.Context,
18-
_ *dhcpInterfaceV6,
18+
iface *dhcpInterfaceV6,
1919
pkt gopacket.Packet,
20+
fd *frameData6,
2021
) (err error) {
2122
defer func() { err = errors.Annotate(err, "serving dhcpv6: %w") }()
2223

24+
// TODO(e.burkov): Use the iface and fd parameters.
25+
_ = iface
26+
_ = fd
27+
2328
msg, ok := pkt.Layer(layers.LayerTypeDHCPv6).(*layers.DHCPv6)
2429
if !ok {
2530
// TODO(e.burkov): Consider adding some debug information about the

internal/dhcpsvc/interface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
//
2020
// TODO(e.burkov): Identify the client by the hardware address and the client
2121
// identifier from the DHCP messages.
22+
//
23+
// TODO(e.burkov): Identify IPv6 clients with DUID.
2224
type macKey any
2325

2426
// macToKey converts mac into macKey, which is used as the key for the lease

internal/dhcpsvc/lease.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
// [websvc].
1717
//
1818
// TODO(e.burkov): Add validation method.
19+
//
20+
// TODO(e.burkov): Migrate to add DUID and IAID fields for DHCPv6 leases.
1921
type Lease struct {
2022
// IP is the IP address leased to the client. It must not be empty.
2123
IP netip.Addr

internal/dhcpsvc/networkdevice.go

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dhcpsvc
33
import (
44
"context"
55
"io"
6+
"net"
67
"net/netip"
78

89
"github.com/AdguardTeam/golibs/errors"
@@ -63,10 +64,18 @@ type NetworkDevice interface {
6364
// No methods of a device should be called after Close.
6465
io.Closer
6566

66-
// Addresses returns all IP addresses assigned to the device.
67+
// Addresses returns all IP addresses assigned to the device. It must
68+
// return at least one valid address, unless the implementation documents
69+
// the opposite.
6770
Addresses() (ips []netip.Addr)
6871

69-
// LinkType returns the link type of the network interface.
72+
// HardwareAddr returns the hardware (MAC) address of the device. It must
73+
// return a valid hardware address, unless the implementation documents the
74+
// opposite.
75+
HardwareAddr() (hw net.HardwareAddr)
76+
77+
// LinkType returns the link type of the network interface. It must return
78+
// a valid link type, unless the implementation documents the opposite.
7079
LinkType() (lt layers.LinkType)
7180

7281
// WritePacketData writes a serialized packet to the network interface.
@@ -98,6 +107,12 @@ func (EmptyNetworkDevice) Addresses() (ips []netip.Addr) {
98107
return nil
99108
}
100109

110+
// HardwareAddr implements the [NetworkDevice] interface for
111+
// [EmptyNetworkDevice]. It always returns nil.
112+
func (EmptyNetworkDevice) HardwareAddr() (hw net.HardwareAddr) {
113+
return nil
114+
}
115+
101116
// LinkType implements the [NetworkDevice] interface for [EmptyNetworkDevice].
102117
// It always returns [layers.LinkTypeNull].
103118
func (EmptyNetworkDevice) LinkType() (lt layers.LinkType) {
@@ -110,11 +125,42 @@ func (EmptyNetworkDevice) WritePacketData(_ []byte) (err error) {
110125
return nil
111126
}
112127

113-
// frameData stores the Ethernet and IPv4 layers of the incoming packet, and
114-
// the network device that the packet was received from.
115-
type frameData struct {
116-
ether *layers.Ethernet
117-
ip *layers.IPv4
118-
device NetworkDevice
128+
// frameData4 stores the Ethernet and IPv4 layers of the incoming packet, as
129+
// well as the network device that the packet was received from and its address.
130+
type frameData4 struct {
131+
// ether is the Ethernet layer of the incoming packet. It must not be nil.
132+
ether *layers.Ethernet
133+
134+
// ip is the IPv4 layer of the incoming packet. It must not be nil.
135+
ip *layers.IPv4
136+
137+
// device is the network device that the packet was received from. It must
138+
// not be nil.
139+
device NetworkDevice
140+
141+
// localAddr is the local IP address that the packet was sent to. It must
142+
// be a valid IPv4 address assigned to the device.
143+
localAddr netip.Addr
144+
}
145+
146+
// frameData6 stores the Ethernet and IPv6 layers of the incoming packet, as
147+
// well as the network device that the packet was received from and its address.
148+
type frameData6 struct {
149+
// ether is the Ethernet layer of the incoming packet. It must not be nil.
150+
ether *layers.Ethernet
151+
152+
// ip is the IPv6 layer of the incoming packet. It must not be nil.
153+
ip *layers.IPv6
154+
155+
// duid is the DHCPv6 DUID constructed of the network device hardware
156+
// address. It must not be nil.
157+
duid *layers.DHCPv6DUID
158+
159+
// device is the network device that the packet was received from. It must
160+
// not be nil.
161+
device NetworkDevice
162+
163+
// localAddr is the local IP address that the packet was sent to. It must
164+
// be a valid IPv6 address assigned to the device.
119165
localAddr netip.Addr
120166
}

0 commit comments

Comments
 (0)