Pull request 2539: 4923-gopacket-dhcp-vol.15

Squashed commit of the following:

commit 82c44225c770f7379ce0f11a80bc9f5ca6ffdc77
Merge: 4c8ea9b3c 9815e5386
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 4 22:03:50 2025 +0300

    Merge branch 'master' into 4923-gopacket-dhcp-vol.15

commit 4c8ea9b3cd8275c59dbf5a23c80e182f42089c5f
Merge: 96ef5241d 8dd4962d4
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 4 20:26:13 2025 +0300

    Merge branch 'master' into 4923-gopacket-dhcp-vol.15

commit 96ef5241ddfee692a1696545e822916e7bb62270
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 4 20:24:28 2025 +0300

    dhcpsvc: imp docs

commit 340037c2561f421f7ea04684a89dfd4ae8f952b7
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 3 14:54:20 2025 +0300

    dhcpsvc: imp code, docs

commit 94fa72beb9692f67dd7b8e2e82e871d3a2f810f4
Merge: ffad4c7c4 6404150ea
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 3 14:53:17 2025 +0300

    Merge branch 'master' into 4923-gopacket-dhcp-vol.15

commit ffad4c7c415c1313ec74e1a7597b50e69559bcb5
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Dec 2 15:20:47 2025 +0300

    dhcpsvc: refactor network devices
This commit is contained in:
Eugene Burkov committed 2025-12-05 15:11:40 +03:00
1 parent 9815e53863
commit 72cb04c51e
7 files changed
+444 -407

No files matched your search

+6
View File
@@ -20,6 +20,12 @@ type Config struct {
// contain valid interface names and configurations.
Interfaces map[string]*InterfaceConfig
// NetworkDeviceManager is the manager of network devices. It must not be
// nil.
//
// TODO(e.burkov): Set.
NetworkDeviceManager NetworkDeviceManager
// Logger will be used to log the DHCP events. It must not be nil.
Logger *slog.Logger
+21 -33
View File
@@ -4,28 +4,19 @@ import (
"context"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// responseWriter4 writes DHCPv4 response to the client.
type responseWriter4 interface {
// write writes the DHCPv4 response to the client.
write(ctx context.Context, pkt *layers.DHCPv4) (err error)
}
// responseWriter6 writes DHCPv6 response to the client.
type responseWriter6 interface {
// write writes the DHCPv6 response to the client.
write(ctx context.Context, pkt *layers.DHCPv6) (err error)
}
// serve handles the incoming packets and dispatches them to the appropriate
// handler based on the Ethernet type. It's used to run in a separate goroutine
// as it blocks until packets channel is closed.
func (srv *DHCPServer) serve(ctx context.Context) {
// serveEther4 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.
func (srv *DHCPServer) serveEther4(ctx context.Context, iface *dhcpInterfaceV4, nd NetworkDevice) {
defer slogutil.RecoverAndLog(ctx, srv.logger)
for pkt := range srv.packetSource.Packets() {
src := gopacket.NewPacketSource(nd, nd.LinkType())
for pkt := range src.Packets() {
etherLayer, ok := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
if !ok {
actual := pkt.Layers()
@@ -34,28 +25,25 @@ func (srv *DHCPServer) serve(ctx context.Context) {
continue
}
var err error
switch typ := etherLayer.EthernetType; typ {
case layers.EthernetTypeIPv4:
// TODO(e.burkov): Set the response writer.
var rw responseWriter4
err = srv.serveV4(ctx, rw, pkt)
case layers.EthernetTypeIPv6:
// TODO(e.burkov): Set the response writer.
var rw responseWriter6
err = srv.serveV6(ctx, rw, pkt)
default:
// TODO(e.burkov): It seems, there is another standard for Ethernet
// header, which uses the Length field instead of the EthernetType,
// so handle it properly.
srv.logger.DebugContext(ctx, "skipping ethernet packet", "type", typ)
ipLayer, ok := pkt.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
if !ok {
actual := pkt.Layers()
srv.logger.DebugContext(ctx, "skipping non-ipv4 packet", "layers", actual)
continue
}
fd := &frameData{
ether: etherLayer,
ip: ipLayer,
device: nd,
}
err := srv.serveV4(ctx, iface, pkt, fd)
if err != nil {
srv.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
}
}
}
// TODO(e.burkov): Add DHCPServer.serveEther6.
+290 -53
View File
@@ -6,16 +6,18 @@ import (
"net/netip"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// serveV4 handles the ethernet packet of IPv4 type. rw and pkt must not be
// nil.
// 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.
func (srv *DHCPServer) serveV4(
ctx context.Context,
rw responseWriter4,
iface *dhcpInterfaceV4,
pkt gopacket.Packet,
fd *frameData,
) (err error) {
defer func() { err = errors.Annotate(err, "serving dhcpv4: %w") }()
@@ -42,37 +44,36 @@ func (srv *DHCPServer) serveV4(
// message.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-3.
return fmt.Errorf("dhcpv4: message type: %w", errors.ErrNoValue)
return fmt.Errorf("message type: %w", errors.ErrNoValue)
}
return srv.handleDHCPv4(ctx, rw, typ, req)
return iface.handleDHCPv4(ctx, typ, req, fd)
}
// handleDHCPv4 handles the DHCPv4 message of the given type. The DHCPDISCOVER
// 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 server itself as it should pick the appropriate
// interface according to the client's choice. req must not be nil, typ should
// be one of:
// 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:
// - [layers.DHCPMsgTypeDiscover]
// - [layers.DHCPMsgTypeRequest]
// - [layers.DHCPMsgTypeRelease]
// - [layers.DHCPMsgTypeDecline]
func (srv *DHCPServer) handleDHCPv4(
func (iface *dhcpInterfaceV4) handleDHCPv4(
ctx context.Context,
rw responseWriter4,
typ layers.DHCPMsgType,
req *layers.DHCPv4,
fd *frameData,
) (err error) {
switch typ {
case layers.DHCPMsgTypeDiscover:
srv.handleDiscover(ctx, rw, req)
iface.handleDiscover(ctx, req, fd)
case layers.DHCPMsgTypeRequest:
srv.handleRequest(ctx, rw, req)
iface.handleRequest(ctx, req, fd)
case layers.DHCPMsgTypeRelease:
srv.handleRelease(ctx, req)
iface.handleRelease(ctx, req)
case layers.DHCPMsgTypeDecline:
srv.handleDecline(ctx, req)
iface.handleDecline(ctx, req)
default:
// TODO(e.burkov): Handle DHCPINFORM.
return fmt.Errorf("dhcpv4: request type: %w: %v", errors.ErrBadEnumValue, typ)
@@ -81,98 +82,334 @@ func (srv *DHCPServer) handleDHCPv4(
return nil
}
// handleDiscover handles the DHCPv4 message of DHCPDISCOVER type. rw must not
// be nil, req must be a DHCPDISCOVER message.
func (srv *DHCPServer) handleDiscover(ctx context.Context, rw responseWriter4, req *layers.DHCPv4) {
// TODO(e.burkov): Check existing leases, either allocated or offered.
for _, iface := range srv.interfaces4 {
go iface.handleDiscover(ctx, rw, req)
}
}
// handleRequest handles the DHCPv4 message of DHCPREQUEST type. rw must not be
// nil, req must be a DHCPREQUEST message.
// handleRequest handles the DHCPv4 message of DHCPREQUEST type. req must be a
// DHCPREQUEST message. req and fd must not be nil.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.
//
// TODO(e.burkov): Remove allocated leases after client have chosen one.
func (srv *DHCPServer) handleRequest(ctx context.Context, rw responseWriter4, req *layers.DHCPv4) {
func (iface *dhcpInterfaceV4) handleRequest(
ctx context.Context,
req *layers.DHCPv4,
fd *frameData,
) {
srvID, hasSrvID := serverID4(req)
reqIP, hasReqIP := requestedIPv4(req)
l := iface.common.logger
switch {
case hasSrvID && !srvID.IsUnspecified():
// If the DHCPREQUEST message contains a server identifier option, the
// message is in response to a DHCPOFFER message. Otherwise, the
// message is a request to verify or extend an existing lease.
iface, ok := srv.interfaces4.findInterface(srvID)
if !ok {
srv.logger.DebugContext(ctx, "skipping selecting request", "serverid", srvID)
if !iface.subnet.Contains(srvID) {
l.DebugContext(ctx, "skipping selecting request", "serverid", srvID)
return
}
iface.handleSelecting(ctx, rw, req, reqIP)
iface.handleSelecting(ctx, req, fd, reqIP)
case hasReqIP && !reqIP.IsUnspecified():
// Requested IP address option MUST be filled in with client's notion of
// its previously assigned address.
iface, ok := srv.interfaces4.findInterface(reqIP)
if !ok {
if !iface.subnet.Contains(reqIP) {
// If the DHCP server detects that the client is on the wrong net
// then the server SHOULD send a DHCPNAK message to the client.
srv.logger.DebugContext(ctx, "declining init-reboot request", "requestedip", reqIP)
iface.respondNAK(ctx, rw, req)
l.DebugContext(ctx, "declining init-reboot request", "requestedip", reqIP)
iface.respondNAK(ctx, req, fd)
return
}
iface.handleInitReboot(ctx, rw, req, reqIP)
iface.handleInitReboot(ctx, req, fd, reqIP)
default:
// Server identifier MUST NOT be filled in, requested IP address option
// MUST NOT be filled in.
ip, _ := netip.AddrFromSlice(req.ClientIP.To4())
iface, ok := srv.interfaces4.findInterface(ip)
if !ok {
srv.logger.DebugContext(ctx, "skipping renew request", "clientip", ip)
if !iface.subnet.Contains(ip) {
l.DebugContext(ctx, "skipping renew request", "clientip", ip)
return
}
iface.handleRenew(ctx, rw, req, ip)
iface.handleRenew(ctx, req, fd, ip)
}
}
// handleDecline handles the DHCPv4 message of DHCPDECLINE type. req must be a
// handleDiscover handles messages of type DHCPDISCOVER. req must be a
// DHCPDISCOVER message, fd must not be nil.
func (iface *dhcpInterfaceV4) handleDiscover(
ctx context.Context,
req *layers.DHCPv4,
fd *frameData,
) {
l := iface.common.logger
mac := req.ClientHWAddr
mk := macToKey(mac)
// Check if there's an existing lease for this MAC address.
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if hasLease {
reqIP, hasReqIP := requestedIPv4(req)
if hasReqIP && reqIP != lease.IP {
l.DebugContext(ctx, "different requested ip", "requested", reqIP, "lease", lease.IP)
}
iface.respondOffer(ctx, req, fd, lease)
return
}
// TODO(e.burkov): Allocate a new lease.
lease, err := iface.allocateLease(ctx, mac)
if err != nil {
l.ErrorContext(ctx, "allocating a lease", "error", err)
return
}
// Send DHCPOFFER with new lease.
iface.respondOffer(ctx, req, fd, lease)
}
// 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.
func (iface *dhcpInterfaceV4) handleSelecting(
ctx context.Context,
req *layers.DHCPv4,
fd *frameData,
reqIP netip.Addr,
) {
l := iface.common.logger
if !reqIP.Is4() {
l.DebugContext(ctx, "bad requested address", "requestedip", reqIP)
return
}
ciaddr, ok := netip.AddrFromSlice(req.ClientIP)
if ok && !ciaddr.IsUnspecified() {
l.DebugContext(ctx, "non-zero ciaddr in selecting request", "ciaddr", ciaddr)
return
}
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.DebugContext(ctx, "no reserved lease", "clienthwaddr", mac)
iface.respondNAK(ctx, req, fd)
return
}
if lease.IP != reqIP {
l.DebugContext(ctx, "selecting request mismatched", "requested", reqIP, "lease", lease.IP)
iface.respondNAK(ctx, req, fd)
return
}
// Commit the lease and send ACK.
iface.commitLease(ctx, lease, hostname4(req))
iface.respondACK(ctx, req, fd, lease)
}
// 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.
func (iface *dhcpInterfaceV4) handleInitReboot(
ctx context.Context,
req *layers.DHCPv4,
fd *frameData,
reqIP netip.Addr,
) {
l := iface.common.logger
if !reqIP.Is4() {
l.DebugContext(ctx, "bad requested address", "requestedip", reqIP)
return
}
// ciaddr must be zero. The client is seeking to verify a previously
// allocated, cached configuration.
ciaddr, _ := netip.AddrFromSlice(req.ClientIP)
if ciaddr.IsValid() && !ciaddr.IsUnspecified() {
l.DebugContext(ctx, "unexpected ciaddr in init-reboot request", "ciaddr", ciaddr)
return
}
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
// If the DHCP server has no record of this client, then it MUST remain
// silent, and MAY output a warning to the network administrator.
l.WarnContext(ctx, "no existing lease", "mac", mac)
return
}
if lease.IP != reqIP {
l.WarnContext(ctx, "init-reboot request mismatched", "requested", reqIP, "lease", lease.IP)
iface.respondNAK(ctx, req, fd)
return
}
// Commit the lease and send ACK.
iface.commitLease(ctx, lease, hostname4(req))
iface.respondACK(ctx, req, fd, lease)
}
// 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.
func (iface *dhcpInterfaceV4) handleRenew(
ctx context.Context,
req *layers.DHCPv4,
fd *frameData,
ip netip.Addr,
) {
l := iface.common.logger
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
// If the DHCP server has no record of this client, then it MUST remain
// silent, and MAY output a warning to the network administrator.
l.InfoContext(ctx, "no existing lease", "mac", mac)
// TODO(e.burkov): Investigate if we should respond with NAK.
return
}
if lease.IP != ip {
l.DebugContext(ctx, "renew request mismatched", "ciaddr", ip, "lease", lease.IP)
iface.respondNAK(ctx, req, fd)
return
}
// Commit the lease and send ACK.
iface.commitLease(ctx, lease, hostname4(req))
iface.respondACK(ctx, req, fd, lease)
}
// handleDecline handles messages of type DHCPDECLINE. req must be a
// DHCPDECLINE message.
func (srv *DHCPServer) handleDecline(ctx context.Context, req *layers.DHCPv4) {
//
// TODO(e.burkov): Log the message option, as the request should include one.
//
// TODO(e.burkov): Consider DRY'ing this with [dhcpInterfaceV4.handleRelease].
func (iface *dhcpInterfaceV4) handleDecline(ctx context.Context, req *layers.DHCPv4) {
l := iface.common.logger
reqIP, hasReqIP := requestedIPv4(req)
if !hasReqIP {
srv.logger.DebugContext(ctx, "skipping decline message without requested ip")
l.DebugContext(ctx, "skipping decline message without requested ip")
return
}
iface, ok := srv.interfaces4.findInterface(reqIP)
if !ok {
srv.logger.DebugContext(ctx, "skipping decline message", "requestedip", reqIP)
if !iface.subnet.Contains(reqIP) {
l.DebugContext(ctx, "skipping decline message", "requestedip", reqIP)
return
}
iface.handleDecline(ctx, reqIP, req)
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.ErrorContext(ctx, "decline message for non-existing lease", "mac", mac)
return
}
if lease.IP != reqIP {
l.ErrorContext(ctx, "decline mismatch", "ip", reqIP, "lease", lease.IP)
return
}
l.WarnContext(ctx, "lease reported to be unavailable", "ip", lease.IP)
err := iface.common.index.remove(ctx, l, lease, iface.common)
if err != nil {
l.ErrorContext(ctx, "removing lease", slogutil.KeyError, err)
return
}
iface.common.blockLease(lease, iface.clock)
}
// handleRelease handles the DHCPv4 message of DHCPRELEASE type. req must be a
// handleRelease handles messages of type DHCPRELEASE. req must be a
// DHCPRELEASE message.
func (srv *DHCPServer) handleRelease(ctx context.Context, req *layers.DHCPv4) {
func (iface *dhcpInterfaceV4) handleRelease(ctx context.Context, req *layers.DHCPv4) {
l := iface.common.logger
ip, _ := netip.AddrFromSlice(req.ClientIP.To4())
iface, ok := srv.interfaces4.findInterface(ip)
if !ok {
srv.logger.DebugContext(ctx, "skipping release message", "clientip", ip)
if !iface.subnet.Contains(ip) {
l.DebugContext(ctx, "skipping release message", "clientip", ip)
return
}
iface.handleRelease(ctx, ip, req)
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.WarnContext(ctx, "release message for non-existing lease", "mac", mac)
return
}
if lease.IP != ip {
l.WarnContext(ctx, "release mismatch", "ip", ip, "lease", lease.IP)
return
}
err := iface.common.index.remove(ctx, l, lease, iface.common)
if err != nil {
l.ErrorContext(ctx, "removing lease", slogutil.KeyError, err)
return
}
}
+6 -5
View File
@@ -9,11 +9,13 @@ import (
"github.com/google/gopacket/layers"
)
// serveV6 handles the ethernet packet of IPv6 type. rw and pkt must not be
// nil.
// serveV6 handles the ethernet packet of IPv6 type. iface and pkt must not be
// nil. ctx must contain a [frameData] accessible with [frameDataFromContext].
//
//lint:ignore U1000 TODO(e.burkov): Use.
func (srv *DHCPServer) serveV6(
ctx context.Context,
rw responseWriter6,
_ *dhcpInterfaceV6,
pkt gopacket.Packet,
) (err error) {
defer func() { err = errors.Annotate(err, "serving dhcpv6: %w") }()
@@ -29,13 +31,12 @@ func (srv *DHCPServer) serveV6(
// TODO(e.burkov): Handle duplicate TransactionID.
return srv.handleDHCPv6(ctx, rw, msg.MsgType, msg)
return srv.handleDHCPv6(ctx, msg.MsgType, msg)
}
// handleDHCPv6 handles the DHCPv6 message of the given type.
func (srv *DHCPServer) handleDHCPv6(
_ context.Context,
_ responseWriter6,
typ layers.DHCPv6MsgType,
_ *layers.DHCPv6,
) (err error) {
+54
View File
@@ -0,0 +1,54 @@
package dhcpsvc
import (
"context"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/validate"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// NetworkDeviceConfig is the configuration for a network device.
type NetworkDeviceConfig struct {
// Name is the name of the network device. It must be a valid interface
// name on the system.
Name string
}
// Validate implements the [validate.Interface] interface for
// *NetworkDeviceConfig.
func (conf *NetworkDeviceConfig) Validate() (err error) {
if conf == nil {
return errors.ErrNoValue
}
return validate.NotEmpty("Name", conf.Name)
}
// NetworkDeviceManager creates and manages network devices.
//
// TODO(e.burkov): Add device closing method.
type NetworkDeviceManager interface {
// Open opens a network device. conf must be valid.
Open(ctx context.Context, conf *NetworkDeviceConfig) (dev NetworkDevice, err error)
}
// NetworkDevice provides reading and writing packets to a network interface.
type NetworkDevice interface {
gopacket.PacketDataSource
// LinkType returns the link type of the network interface.
LinkType() (lt layers.LinkType)
// WritePacketData writes a serialized packet to the network interface.
WritePacketData(data []byte) (err error)
}
// 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
}
+26 -14
View File
@@ -14,7 +14,6 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket"
)
// DHCPServer is a DHCP server for both IPv4 and IPv6 address families.
@@ -28,10 +27,8 @@ type DHCPServer struct {
// logger logs common DHCP events.
logger *slog.Logger
// packetSource is the source of DHCP packets to process.
//
// TODO(e.burkov): Implement and set.
packetSource gopacket.PacketSource
// deviceManager is the manager of network devices.
deviceManager NetworkDeviceManager
// localTLD is the top-level domain name to use for resolving DHCP clients'
// hostnames.
@@ -70,12 +67,13 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
enabled.Store(conf.Enabled)
srv = &DHCPServer{
enabled: enabled,
logger: l,
localTLD: conf.LocalDomainName,
leasesMu: &sync.RWMutex{},
leases: newLeaseIndex(conf.DBFilePath),
icmpTimeout: conf.ICMPTimeout,
enabled: enabled,
logger: l,
deviceManager: conf.NetworkDeviceManager,
localTLD: conf.LocalDomainName,
leasesMu: &sync.RWMutex{},
leases: newLeaseIndex(conf.DBFilePath),
icmpTimeout: conf.ICMPTimeout,
}
srv.interfaces4, srv.interfaces6 = srv.newInterfaces(ctx, l, conf.Interfaces)
@@ -137,13 +135,27 @@ func (srv *DHCPServer) newInterfaces(
func (srv *DHCPServer) Start(ctx context.Context) (err error) {
srv.logger.DebugContext(ctx, "starting dhcp server")
// TODO(e.burkov): Listen to configured interfaces.
var errs []error
for _, iface := range srv.interfaces4 {
var nd NetworkDevice
nd, err = srv.deviceManager.Open(ctx, &NetworkDeviceConfig{
Name: iface.common.name,
})
if err != nil {
errs = append(errs, err)
go srv.serve(context.WithoutCancel(ctx))
continue
}
return nil
go srv.serveEther4(context.WithoutCancel(ctx), iface, nd)
}
// TODO(e.burkov): Serve EthernetTypeIPv6.
return errors.Join(errs...)
}
// Shutdown implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) Shutdown(ctx context.Context) (err error) {
srv.logger.DebugContext(ctx, "shutting down dhcp server")
+41 -302
View File
@@ -10,9 +10,9 @@ import (
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/AdguardTeam/golibs/validate"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
@@ -198,36 +198,37 @@ func (srv *DHCPServer) newDHCPInterfaceV4(
}
// commitLease updates the lease in database, using new hostname if it's valid.
func (iface *dhcpInterfaceV4) commitLease(ctx context.Context, l *Lease, hostname string) {
// TODO(e.burkov): Implement.
}
//
// TODO(e.burkov): Implement.
func (iface *dhcpInterfaceV4) commitLease(ctx context.Context, l *Lease, hostname string) {}
// respondOffer sends a DHCPOFFER message to the client. rw, req, and l must
// respondOffer sends a DHCPOFFER message to the client. req, fd, and l must
// not be nil.
func (iface *dhcpInterfaceV4) respondOffer(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
fd *frameData,
l *Lease,
) {
resp := iface.buildResponse(req, l, layers.DHCPMsgTypeOffer)
if err := rw.write(ctx, resp); err != nil {
if err := respond4(fd, resp); err != nil {
iface.common.logger.ErrorContext(ctx, "writing offer", "error", err)
}
}
// respondACK sends a DHCPACK message to the client.
// respondACK sends a DHCPACK message to the client. req, fd, and l must not be
// nil.
//
// TODO(e.burkov): Implement according to RFC, answer to DHCPINFORM
// differently, when it's supported.
func (iface *dhcpInterfaceV4) respondACK(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
fd *frameData,
l *Lease,
) {
resp := iface.buildResponse(req, l, layers.DHCPMsgTypeAck)
if err := rw.write(ctx, resp); err != nil {
if err := respond4(fd, resp); err != nil {
iface.common.logger.ErrorContext(ctx, "writing ack", "error", err)
}
}
@@ -238,14 +239,14 @@ var v4OptionMessageTypeNAK = layers.NewDHCPOption(
[]byte{byte(layers.DHCPMsgTypeNak)},
)
// respondNAK constructs and sends a DHCPNAK message to the client. rw and req
// must not be nil.
// respondNAK constructs and sends a DHCPNAK message to the client. req, fd,
// and resp must not be nil.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.
func (iface *dhcpInterfaceV4) respondNAK(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
fd *frameData,
) {
resp := &layers.DHCPv4{
Operation: layers.DHCPOpReply,
@@ -261,7 +262,7 @@ func (iface *dhcpInterfaceV4) respondNAK(
},
}
if err := rw.write(ctx, resp); err != nil {
if err := respond4(fd, resp); err != nil {
iface.common.logger.ErrorContext(ctx, "writing nak", "error", err)
}
}
@@ -307,281 +308,6 @@ func (iface *dhcpInterfaceV4) buildResponse(
return resp
}
// handleDiscover handles messages of type DHCPDISCOVER. rw and req must not be
// nil.
func (iface *dhcpInterfaceV4) handleDiscover(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
) {
l := iface.common.logger
mac := req.ClientHWAddr
mk := macToKey(mac)
// Check if there's an existing lease for this MAC address.
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if hasLease {
reqIP, hasReqIP := requestedIPv4(req)
if hasReqIP && reqIP != lease.IP {
l.DebugContext(ctx, "different requested ip", "requested", reqIP, "lease", lease.IP)
}
iface.respondOffer(ctx, rw, req, lease)
return
}
// TODO(e.burkov): Allocate a new lease.
lease, err := iface.allocateLease(ctx, mac)
if err != nil {
l.ErrorContext(ctx, "allocating a lease", "error", err)
return
}
// Send DHCPOFFER with new lease.
iface.respondOffer(ctx, rw, req, lease)
}
// handleSelecting handles messages of type DHCPREQUEST in SELECTING state. req
// must contain a server identifier option that matches the iface's subnet, and
// client IP address must be empty or unspecified, and requested IP address
// must be filled in with the yiaddr value from the chosen DHCPOFFER. rw must
// not be nil, reqIP must be a valid IPv4 address.
func (iface *dhcpInterfaceV4) handleSelecting(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
reqIP netip.Addr,
) {
l := iface.common.logger
if !reqIP.Is4() {
l.DebugContext(ctx, "bad requested address", "requestedip", reqIP)
return
}
ciaddr, ok := netip.AddrFromSlice(req.ClientIP)
if ok && !ciaddr.IsUnspecified() {
l.DebugContext(ctx, "non-zero ciaddr in selecting request", "ciaddr", ciaddr)
return
}
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.DebugContext(ctx, "no reserved lease", "clienthwaddr", mac)
iface.respondNAK(ctx, rw, req)
return
}
if lease.IP != reqIP {
l.DebugContext(ctx, "selecting request mismatched", "requested", reqIP, "lease", lease.IP)
iface.respondNAK(ctx, rw, req)
return
}
// Commit the lease and send ACK.
iface.commitLease(ctx, lease, hostname4(req))
iface.respondACK(ctx, rw, req, lease)
}
// handleInitReboot handles messages of type DHCPREQUEST in INIT-REBOOT state.
// req must contain a client IP address option that matches the iface's subnet,
// and requested IP address option must be filled in with the client's IP
// address. Also req must contain a valid chaddr according to
// [netutil.ValidateMAC]. rw must not be nil, reqIP must be a valid IPv4
// address.
func (iface *dhcpInterfaceV4) handleInitReboot(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
reqIP netip.Addr,
) {
l := iface.common.logger
if !reqIP.Is4() {
l.DebugContext(ctx, "bad requested address", "requestedip", reqIP)
return
}
// ciaddr must be zero. The client is seeking to verify a previously
// allocated, cached configuration.
ciaddr, _ := netip.AddrFromSlice(req.ClientIP)
if ciaddr.IsValid() && !ciaddr.IsUnspecified() {
l.DebugContext(ctx, "unexpected ciaddr in init-reboot request", "ciaddr", ciaddr)
return
}
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
// If the DHCP server has no record of this client, then it MUST remain
// silent, and MAY output a warning to the network administrator.
l.WarnContext(ctx, "no existing lease", "mac", mac)
return
}
if lease.IP != reqIP {
l.WarnContext(ctx, "init-reboot request mismatched", "requested", reqIP, "lease", lease.IP)
iface.respondNAK(ctx, rw, req)
return
}
// Commit the lease and send ACK.
iface.commitLease(ctx, lease, hostname4(req))
iface.respondACK(ctx, rw, req, lease)
}
// handleRenew handles messages of type DHCPREQUEST in RENEWING or REBINDING
// state. rw must not be nil, ip should be a previously leased address, req
// must contain a valid chaddr according to [netutil.ValidateMAC].
func (iface *dhcpInterfaceV4) handleRenew(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
ip netip.Addr,
) {
l := iface.common.logger
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
// If the DHCP server has no record of this client, then it MUST remain
// silent, and MAY output a warning to the network administrator.
l.InfoContext(ctx, "no existing lease", "mac", mac)
// TODO(e.burkov): Investigate if we should respond with NAK.
return
}
if lease.IP != ip {
l.DebugContext(ctx, "renew request mismatched", "ciaddr", ip, "lease", lease.IP)
iface.respondNAK(ctx, rw, req)
return
}
// Commit the lease and send ACK.
iface.commitLease(ctx, lease, hostname4(req))
iface.respondACK(ctx, rw, req, lease)
}
// handleDecline handles messages of type DHCPDECLINE. req should contain a
// requested IP address, as previously offered by the server. ip should be a
// previously leased address, req must contain a valid chaddr according to
// [netutil.ValidateMAC].
//
// TODO(e.burkov): Log the message option, as the request should include one.
//
// TODO(e.burkov): Consider DRY'ing this with [dhcpInterfaceV4.handleRelease].
func (iface *dhcpInterfaceV4) handleDecline(
ctx context.Context,
ip netip.Addr,
req *layers.DHCPv4,
) {
l := iface.common.logger
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.ErrorContext(ctx, "decline message for non-existing lease", "mac", mac)
return
}
if lease.IP != ip {
l.ErrorContext(ctx, "decline mismatch", "ip", ip, "lease", lease.IP)
return
}
l.WarnContext(ctx, "lease reported to be unavailable", "ip", lease.IP)
err := iface.common.index.remove(ctx, l, lease, iface.common)
if err != nil {
l.ErrorContext(ctx, "removing lease", slogutil.KeyError, err)
return
}
iface.common.blockLease(lease, iface.clock)
}
// handleRelease handles messages of type DHCPRELEASE. ip should be valid,
// previously allocated by the server and correspond to the client's lease. req
// must contain a valid chaddr according to [netutil.ValidateMAC].
//
// TODO(e.burkov): Consider DRY'ing this with [dhcpInterfaceV4.handleDecline].
func (iface *dhcpInterfaceV4) handleRelease(
ctx context.Context,
ip netip.Addr,
req *layers.DHCPv4,
) {
l := iface.common.logger
// Check if the lease exists and matches.
mac := req.ClientHWAddr
mk := macToKey(mac)
iface.common.indexMu.Lock()
defer iface.common.indexMu.Unlock()
lease, hasLease := iface.common.leases[mk]
if !hasLease {
l.ErrorContext(ctx, "release message for non-existing lease", "mac", mac)
return
}
if lease.IP != ip {
l.ErrorContext(ctx, "release mismatch", "ip", ip, "lease", lease.IP)
return
}
err := iface.common.index.remove(ctx, l, lease, iface.common)
if err != nil {
l.ErrorContext(ctx, "removing lease", slogutil.KeyError, err)
return
}
}
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
type dhcpInterfacesV4 []*dhcpInterfaceV4
@@ -598,19 +324,6 @@ func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok boo
return ifaces[i].common, true
}
// findInterface returns the first DHCPv4 interface within ifaces containing
// ip. It returns false if there is no such interface. ip must be valid.
func (ifaces dhcpInterfacesV4) findInterface(ip netip.Addr) (iface *dhcpInterfaceV4, ok bool) {
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
return iface.subnet.Contains(ip)
})
if i < 0 {
return nil, false
}
return ifaces[i], true
}
// allocateLease allocates a new lease for the MAC address. If there are no IP
// addresses left, both l and err are nil. mac must be a valid according to
// [netutil.ValidateMAC].
@@ -677,3 +390,29 @@ func (iface *dhcpInterfaceV4) reserveLease(
return l, nil
}
// respond4 sends a DHCPv4 response. fd and resp must not be nil.
func respond4(fd *frameData, resp *layers.DHCPv4) (err error) {
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
eth := &layers.Ethernet{
SrcMAC: fd.ether.SrcMAC,
DstMAC: fd.ether.DstMAC,
EthernetType: layers.EthernetTypeIPv4,
}
// TODO(e.burkov): Handle IP layer.
all := []gopacket.SerializableLayer{eth, resp}
err = gopacket.SerializeLayers(buf, opts, all...)
if err != nil {
return err
}
return fd.device.WritePacketData(buf.Bytes())
}