Compare commits

...

2 Commits

Author SHA1 Message Date
Eugene Burkov
473fe15fbc dhcpsvc: add handlers 2025-04-30 15:57:53 +03:00
Eugene Burkov
1ccdfc0ac3 dhcpsvc: multiplex dhcpv4 2025-04-30 15:02:42 +03:00
7 changed files with 447 additions and 56 deletions

View File

@@ -0,0 +1,127 @@
package dhcpsvc
import (
"context"
"fmt"
"net/netip"
"github.com/AdguardTeam/golibs/errors"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// serveV4 handles the ethernet packet of IPv4 type.
func (srv *DHCPServer) serveV4(
ctx context.Context,
rw responseWriter4,
pkt gopacket.Packet,
) (err error) {
defer func() { err = errors.Annotate(err, "serving dhcpv4: %w") }()
req, ok := pkt.Layer(layers.LayerTypeDHCPv4).(*layers.DHCPv4)
if !ok {
srv.logger.DebugContext(ctx, "skipping non-dhcpv4 packet")
return nil
}
// TODO(e.burkov): Handle duplicate Xid.
if req.Operation != layers.DHCPOpRequest {
srv.logger.DebugContext(ctx, "skipping non-request dhcpv4 packet")
return nil
}
typ, ok := msg4Type(req)
if !ok {
// The "DHCP message type" option - must be included in every DHCP
// message.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-3.
return fmt.Errorf("dhcpv4: message type: %w", errors.ErrNoValue)
}
return srv.handleDHCPv4(ctx, rw, typ, req)
}
// handleDHCPv4 handles the DHCPv4 message of the given type.
func (srv *DHCPServer) handleDHCPv4(
ctx context.Context,
rw responseWriter4,
typ layers.DHCPMsgType,
req *layers.DHCPv4,
) (err error) {
// Each interface should handle the DISCOVER and REQUEST messages offer and
// allocate the available leases. The RELEASE and DECLINE messages should
// be handled by the server itself as it should remove the lease.
switch typ {
case layers.DHCPMsgTypeDiscover:
srv.handleDiscover(ctx, rw, req)
case layers.DHCPMsgTypeRequest:
srv.handleRequest(ctx, rw, req)
case layers.DHCPMsgTypeRelease:
// TODO(e.burkov): !! Remove the lease, either allocated or offered.
case layers.DHCPMsgTypeDecline:
// TODO(e.burkov): !! Remove the allocated lease. RFC tells it only
// possible if the client found the address already in use.
default:
// TODO(e.burkov): Handle DHCPINFORM.
return fmt.Errorf("dhcpv4: request type: %w: %v", errors.ErrBadEnumValue, typ)
}
return nil
}
// handleDiscover handles the DHCPv4 message of discover type.
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 request type.
func (srv *DHCPServer) handleRequest(ctx context.Context, rw responseWriter4, req *layers.DHCPv4) {
srvID, hasSrvID := serverID4(req)
reqIP, hasReqIP := requestedIPv4(req)
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, hasIface := srv.interfaces4.findInterface(srvID)
if !hasIface {
srv.logger.DebugContext(ctx, "skipping selecting request", "serverid", srvID)
return
}
iface.handleSelecting(ctx, rw, req, reqIP)
case hasReqIP && !reqIP.IsUnspecified():
// Requested IP address option MUST be filled in with client's notion of
// its previously assigned address.
iface, hasIface := srv.interfaces4.findInterface(reqIP)
if !hasIface {
srv.logger.DebugContext(ctx, "skipping init-reboot request", "requestedip", reqIP)
return
}
iface.handleInitReboot(ctx, rw, req, 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, hasIface := srv.interfaces4.findInterface(ip)
if !hasIface {
srv.logger.DebugContext(ctx, "skipping init-reboot request", "clientip", ip)
return
}
iface.handleRenew(ctx, rw, req)
}
}

View File

@@ -0,0 +1,57 @@
package dhcpsvc
import (
"context"
"fmt"
"github.com/AdguardTeam/golibs/errors"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// serveV6 handles the ethernet packet of IPv6 type.
func (srv *DHCPServer) serveV6(
ctx context.Context,
rw responseWriter4,
pkt gopacket.Packet,
) (err error) {
defer func() { err = errors.Annotate(err, "serving dhcpv6: %w") }()
msg, ok := pkt.Layer(layers.LayerTypeDHCPv6).(*layers.DHCPv6)
if !ok {
srv.logger.DebugContext(ctx, "skipping non-dhcpv6 packet")
return nil
}
// TODO(e.burkov): Handle duplicate TransactionID.
typ := msg.MsgType
return srv.handleDHCPv6(ctx, rw, typ, msg)
}
// handleDHCPv6 handles the DHCPv6 message of the given type.
func (srv *DHCPServer) handleDHCPv6(
_ context.Context,
_ responseWriter4,
typ layers.DHCPv6MsgType,
_ *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.
default:
return fmt.Errorf("dhcpv6: request type: %w: %v", errors.ErrBadEnumValue, typ)
}
return nil
}

View File

@@ -45,17 +45,6 @@ type netInterface struct {
leaseTTL time.Duration
}
// newNetInterface creates a new netInterface with the given name, leaseTTL, and
// logger.
func newNetInterface(name string, l *slog.Logger, leaseTTL time.Duration) (iface *netInterface) {
return &netInterface{
logger: l,
leases: map[macKey]*Lease{},
name: name,
leaseTTL: leaseTTL,
}
}
// reset clears all the slices in iface for reuse.
func (iface *netInterface) reset() {
clear(iface.leases)

50
internal/dhcpsvc/serve.go Normal file
View File

@@ -0,0 +1,50 @@
package dhcpsvc
import (
"context"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"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)
}
// 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) {
defer slogutil.RecoverAndLog(ctx, srv.logger)
for pkt := range srv.packetSource.Packets() {
etherLayer, ok := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
if !ok {
srv.logger.DebugContext(ctx, "skipping non-ethernet packet")
continue
}
var err error
// TODO(e.burkov): Set the response writer.
var rw responseWriter4
switch typ := etherLayer.EthernetType; typ {
case layers.EthernetTypeIPv4:
err = srv.serveV4(ctx, rw, pkt)
case layers.EthernetTypeIPv6:
err = srv.serveV6(ctx, rw, pkt)
default:
srv.logger.DebugContext(ctx, "skipping ethernet packet", "type", typ)
continue
}
if err != nil {
srv.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
}
}
}

View File

@@ -13,9 +13,13 @@ import (
"time"
"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.
//
// TODO(e.burkov): Rename to Default.
type DHCPServer struct {
// enabled indicates whether the DHCP server is enabled and can provide
// information about its clients.
@@ -24,6 +28,9 @@ type DHCPServer struct {
// logger logs common DHCP events.
logger *slog.Logger
// TODO(e.burkov): Implement and set.
packetSource gopacket.PacketSource
// localTLD is the top-level domain name to use for resolving DHCP clients'
// hostnames.
localTLD string
@@ -98,7 +105,7 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
// their configurations.
func newInterfaces(
ctx context.Context,
l *slog.Logger,
baseLogger *slog.Logger,
ifaces map[string]*InterfaceConfig,
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
@@ -110,18 +117,27 @@ func newInterfaces(
var errs []error
for _, name := range slices.Sorted(maps.Keys(ifaces)) {
iface := ifaces[name]
var i4 *dhcpInterfaceV4
i4, err = newDHCPInterfaceV4(ctx, l, name, iface.IPv4)
if err != nil {
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
} else if i4 != nil {
v4 = append(v4, i4)
iface4, v4Err := newDHCPInterfaceV4(
ctx,
baseLogger.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv4),
name,
iface.IPv4,
)
if v4Err != nil {
v4Err = fmt.Errorf("interface %q: %s: %w", name, netutil.AddrFamilyIPv4, v4Err)
errs = append(errs, v4Err)
} else {
v4 = append(v4, iface4)
}
i6 := newDHCPInterfaceV6(ctx, l, name, iface.IPv6)
if i6 != nil {
v6 = append(v6, i6)
}
iface6 := newDHCPInterfaceV6(
ctx,
baseLogger.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6),
name,
iface.IPv6,
)
v6 = append(v6, iface6)
}
if err = errors.Join(errs...); err != nil {
@@ -136,6 +152,25 @@ func newInterfaces(
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
// var _ Interface = (*DHCPServer)(nil)
// Start implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) Start(ctx context.Context) (err error) {
srv.logger.DebugContext(ctx, "starting dhcp server")
// TODO(e.burkov): Listen to configured interfaces.
go srv.serve(context.Background())
return nil
}
func (srv *DHCPServer) Shutdown(ctx context.Context) (err error) {
srv.logger.DebugContext(ctx, "shutting down dhcp server")
// TODO(e.burkov): Close the packet source.
return nil
}
// Enabled implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) Enabled() (ok bool) {
return srv.enabled.Load()
@@ -335,6 +370,50 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
return nil
}
// removeLeaseByAddr removes the lease with the given IP address from the
// server. It returns an error if the lease can't be removed.
//
// TODO(e.burkov): !! Use.
func (srv *DHCPServer) removeLeaseByAddr(ctx context.Context, addr netip.Addr) (err error) {
defer func() { err = errors.Annotate(err, "removing lease by address: %w") }()
iface, err := srv.ifaceForAddr(addr)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
srv.leasesMu.Lock()
defer srv.leasesMu.Unlock()
l, ok := srv.leases.leaseByAddr(addr)
if !ok {
return fmt.Errorf("no lease for ip %s", addr)
}
err = srv.leases.remove(l, iface)
if err != nil {
// Don't wrap the error since there is already an annotation deferred.
return err
}
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
iface.logger.DebugContext(
ctx, "removed lease",
"hostname", l.Hostname,
"ip", l.IP,
"mac", l.HWAddr,
"static", l.IsStatic,
)
return nil
}
// ifaceForAddr returns the handled network interface for the given IP address,
// or an error if no such interface exists.
func (srv *DHCPServer) ifaceForAddr(addr netip.Addr) (iface *netInterface, err error) {

View File

@@ -91,7 +91,7 @@ type dhcpInterfaceV4 struct {
// gateway is the IP address of the network gateway.
gateway netip.Addr
// subnet is the network subnet.
// subnet is the network subnet of the interface.
subnet netip.Prefix
// addrSpace is the IPv4 address space allocated for leasing.
@@ -115,12 +115,7 @@ func newDHCPInterfaceV4(
l *slog.Logger,
name string,
conf *IPv4Config,
) (i *dhcpInterfaceV4, err error) {
l = l.With(
keyInterface, name,
keyFamily, netutil.AddrFamilyIPv4,
)
) (iface *dhcpInterfaceV4, err error) {
if !conf.Enabled {
l.DebugContext(ctx, "disabled")
@@ -144,31 +139,20 @@ func newDHCPInterfaceV4(
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
}
i = &dhcpInterfaceV4{
iface = &dhcpInterfaceV4{
gateway: conf.GatewayIP,
subnet: subnet,
addrSpace: addrSpace,
common: newNetInterface(name, l, conf.LeaseDuration),
common: &netInterface{
logger: l,
leases: map[macKey]*Lease{},
name: name,
leaseTTL: conf.LeaseDuration,
},
}
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
iface.implicitOpts, iface.explicitOpts = conf.options(ctx, l)
return i, nil
}
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
type dhcpInterfacesV4 []*dhcpInterfaceV4
// find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface.
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, 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].common, true
return iface, nil
}
// options returns the implicit and explicit options for the interface. The two
@@ -361,3 +345,104 @@ func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp laye
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
return int(a.Type) - int(b.Type)
}
// msg4Type returns the message type of msg, if it's present within the options.
func msg4Type(msg *layers.DHCPv4) (typ layers.DHCPMsgType, ok bool) {
for _, opt := range msg.Options {
if opt.Type == layers.DHCPOptMessageType && len(opt.Data) > 0 {
return layers.DHCPMsgType(opt.Data[0]), true
}
}
return 0, false
}
// requestedIPv4 returns the IPv4 address, requested by client in the DHCP
// message, if any.
func requestedIPv4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {
for _, opt := range msg.Options {
if opt.Type == layers.DHCPOptRequestIP && len(opt.Data) == net.IPv4len {
return netip.AddrFromSlice(opt.Data)
}
}
return netip.Addr{}, false
}
// serverID4 returns the server ID of the DHCP message, if any.
func serverID4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {
for _, opt := range msg.Options {
if opt.Type == layers.DHCPOptServerID && len(opt.Data) == net.IPv4len {
return netip.AddrFromSlice(opt.Data)
}
}
return netip.Addr{}, false
}
// handleDiscover handles messages of type discover.
func (iface *dhcpInterfaceV4) handleDiscover(
ctx context.Context,
rw responseWriter4,
msg *layers.DHCPv4,
) {
// TODO(e.burkov): !! Implement.
}
// handleSelecting handles messages of type request in SELECTING state.
func (iface *dhcpInterfaceV4) handleSelecting(
ctx context.Context,
rw responseWriter4,
msg *layers.DHCPv4,
reqIP netip.Addr,
) {
// TODO(e.burkov): !! Implement.
}
// handleSelecting handles messages of type request in INIT-REBOOT state.
func (iface *dhcpInterfaceV4) handleInitReboot(
ctx context.Context,
rw responseWriter4,
msg *layers.DHCPv4,
reqIP netip.Addr,
) {
// TODO(e.burkov): !! Implement.
}
// handleRenew handles messages of type request in RENEWING or REBINDING state.
func (iface *dhcpInterfaceV4) handleRenew(
ctx context.Context,
rw responseWriter4,
req *layers.DHCPv4,
) {
// TODO(e.burkov): !! Implement.
}
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
type dhcpInterfacesV4 []*dhcpInterfaceV4
// find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface.
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, 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].common, true
}
// findInterface returns the first DHCPv4 interface within ifaces containing
// ip. It returns false if there is no such interface.
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
}

View File

@@ -97,23 +97,27 @@ func newDHCPInterfaceV6(
l *slog.Logger,
name string,
conf *IPv6Config,
) (i *dhcpInterfaceV6) {
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
) (iface *dhcpInterfaceV6) {
if !conf.Enabled {
l.DebugContext(ctx, "disabled")
return nil
}
i = &dhcpInterfaceV6{
rangeStart: conf.RangeStart,
common: newNetInterface(name, l, conf.LeaseDuration),
iface = &dhcpInterfaceV6{
rangeStart: conf.RangeStart,
common: &netInterface{
logger: l,
leases: map[macKey]*Lease{},
name: name,
leaseTTL: conf.LeaseDuration,
},
raSLAACOnly: conf.RASLAACOnly,
raAllowSLAAC: conf.RAAllowSLAAC,
}
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
iface.implicitOpts, iface.explicitOpts = conf.options(ctx, l)
return i
return iface
}
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.