dhcpsvc: multiplex dhcpv4
This commit is contained in:
@@ -45,17 +45,6 @@ type netInterface struct {
|
|||||||
leaseTTL time.Duration
|
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.
|
// reset clears all the slices in iface for reuse.
|
||||||
func (iface *netInterface) reset() {
|
func (iface *netInterface) reset() {
|
||||||
clear(iface.leases)
|
clear(iface.leases)
|
||||||
|
|||||||
102
internal/dhcpsvc/serve.go
Normal file
102
internal/dhcpsvc/serve.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package dhcpsvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/google/gopacket"
|
||||||
|
"github.com/google/gopacket/layers"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
switch typ := etherLayer.EthernetType; typ {
|
||||||
|
case layers.EthernetTypeIPv4:
|
||||||
|
err = srv.serveV4(ctx, pkt)
|
||||||
|
case layers.EthernetTypeIPv6:
|
||||||
|
// TODO(e.burkov): Handle DHCPv6 as well.
|
||||||
|
default:
|
||||||
|
srv.logger.DebugContext(ctx, "skipping ethernet packet", "type", typ)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
srv.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveV4 handles the ethernet packet of IPv4 type.
|
||||||
|
func (srv *DHCPServer) serveV4(ctx context.Context, pkt gopacket.Packet) (err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "serving dhcpv4: %w") }()
|
||||||
|
|
||||||
|
msg, 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.
|
||||||
|
|
||||||
|
typ, ok := msgType(msg)
|
||||||
|
if !ok {
|
||||||
|
return errors.Error("no message type in the dhcpv4 message")
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.handleDHCPv4(ctx, typ, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDHCPv4 handles the DHCPv4 message of the given type.
|
||||||
|
func (srv *DHCPServer) handleDHCPv4(
|
||||||
|
ctx context.Context,
|
||||||
|
typ layers.DHCPMsgType,
|
||||||
|
msg *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:
|
||||||
|
for _, iface := range srv.interfaces4 {
|
||||||
|
go iface.handleDiscover(ctx, msg)
|
||||||
|
}
|
||||||
|
case layers.DHCPMsgTypeRequest:
|
||||||
|
for _, iface := range srv.interfaces4 {
|
||||||
|
go iface.handleRequest(ctx, msg)
|
||||||
|
}
|
||||||
|
case layers.DHCPMsgTypeRelease:
|
||||||
|
addr, ok := netip.AddrFromSlice(msg.ClientIP)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid client ip in the release message")
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.removeLeaseByAddr(ctx, addr)
|
||||||
|
case layers.DHCPMsgTypeDecline:
|
||||||
|
addr, ok := requestedIP(msg)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no requested ip in the decline message")
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.removeLeaseByAddr(ctx, addr)
|
||||||
|
default:
|
||||||
|
// TODO(e.burkov): Handle DHCPINFORM.
|
||||||
|
return fmt.Errorf("dhcpv4 message type: %w: %v", errors.ErrBadEnumValue, typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -13,9 +13,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"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.
|
// DHCPServer is a DHCP server for both IPv4 and IPv6 address families.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Rename to Default.
|
||||||
type DHCPServer struct {
|
type DHCPServer struct {
|
||||||
// enabled indicates whether the DHCP server is enabled and can provide
|
// enabled indicates whether the DHCP server is enabled and can provide
|
||||||
// information about its clients.
|
// information about its clients.
|
||||||
@@ -24,6 +28,9 @@ type DHCPServer struct {
|
|||||||
// logger logs common DHCP events.
|
// logger logs common DHCP events.
|
||||||
logger *slog.Logger
|
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'
|
// localTLD is the top-level domain name to use for resolving DHCP clients'
|
||||||
// hostnames.
|
// hostnames.
|
||||||
localTLD string
|
localTLD string
|
||||||
@@ -98,7 +105,7 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
|
|||||||
// their configurations.
|
// their configurations.
|
||||||
func newInterfaces(
|
func newInterfaces(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
l *slog.Logger,
|
baseLogger *slog.Logger,
|
||||||
ifaces map[string]*InterfaceConfig,
|
ifaces map[string]*InterfaceConfig,
|
||||||
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
|
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
|
||||||
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
|
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
|
||||||
@@ -110,18 +117,27 @@ func newInterfaces(
|
|||||||
var errs []error
|
var errs []error
|
||||||
for _, name := range slices.Sorted(maps.Keys(ifaces)) {
|
for _, name := range slices.Sorted(maps.Keys(ifaces)) {
|
||||||
iface := ifaces[name]
|
iface := ifaces[name]
|
||||||
var i4 *dhcpInterfaceV4
|
|
||||||
i4, err = newDHCPInterfaceV4(ctx, l, name, iface.IPv4)
|
iface4, v4Err := newDHCPInterfaceV4(
|
||||||
if err != nil {
|
ctx,
|
||||||
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
|
baseLogger.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv4),
|
||||||
} else if i4 != nil {
|
name,
|
||||||
v4 = append(v4, i4)
|
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)
|
iface6 := newDHCPInterfaceV6(
|
||||||
if i6 != nil {
|
ctx,
|
||||||
v6 = append(v6, i6)
|
baseLogger.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6),
|
||||||
}
|
name,
|
||||||
|
iface.IPv6,
|
||||||
|
)
|
||||||
|
v6 = append(v6, iface6)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = errors.Join(errs...); err != nil {
|
if err = errors.Join(errs...); err != nil {
|
||||||
@@ -136,6 +152,25 @@ func newInterfaces(
|
|||||||
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
|
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
|
||||||
// var _ Interface = (*DHCPServer)(nil)
|
// 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.
|
// Enabled implements the [Interface] interface for *DHCPServer.
|
||||||
func (srv *DHCPServer) Enabled() (ok bool) {
|
func (srv *DHCPServer) Enabled() (ok bool) {
|
||||||
return srv.enabled.Load()
|
return srv.enabled.Load()
|
||||||
@@ -335,6 +370,48 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
|
|||||||
return nil
|
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.
|
||||||
|
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,
|
// ifaceForAddr returns the handled network interface for the given IP address,
|
||||||
// or an error if no such interface exists.
|
// or an error if no such interface exists.
|
||||||
func (srv *DHCPServer) ifaceForAddr(addr netip.Addr) (iface *netInterface, err error) {
|
func (srv *DHCPServer) ifaceForAddr(addr netip.Addr) (iface *netInterface, err error) {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ type dhcpInterfaceV4 struct {
|
|||||||
// gateway is the IP address of the network gateway.
|
// gateway is the IP address of the network gateway.
|
||||||
gateway netip.Addr
|
gateway netip.Addr
|
||||||
|
|
||||||
// subnet is the network subnet.
|
// subnet is the network subnet of the interface.
|
||||||
subnet netip.Prefix
|
subnet netip.Prefix
|
||||||
|
|
||||||
// addrSpace is the IPv4 address space allocated for leasing.
|
// addrSpace is the IPv4 address space allocated for leasing.
|
||||||
@@ -115,12 +115,7 @@ func newDHCPInterfaceV4(
|
|||||||
l *slog.Logger,
|
l *slog.Logger,
|
||||||
name string,
|
name string,
|
||||||
conf *IPv4Config,
|
conf *IPv4Config,
|
||||||
) (i *dhcpInterfaceV4, err error) {
|
) (iface *dhcpInterfaceV4, err error) {
|
||||||
l = l.With(
|
|
||||||
keyInterface, name,
|
|
||||||
keyFamily, netutil.AddrFamilyIPv4,
|
|
||||||
)
|
|
||||||
|
|
||||||
if !conf.Enabled {
|
if !conf.Enabled {
|
||||||
l.DebugContext(ctx, "disabled")
|
l.DebugContext(ctx, "disabled")
|
||||||
|
|
||||||
@@ -144,15 +139,20 @@ func newDHCPInterfaceV4(
|
|||||||
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
i = &dhcpInterfaceV4{
|
iface = &dhcpInterfaceV4{
|
||||||
gateway: conf.GatewayIP,
|
gateway: conf.GatewayIP,
|
||||||
subnet: subnet,
|
subnet: subnet,
|
||||||
addrSpace: addrSpace,
|
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
|
return iface, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
||||||
@@ -361,3 +361,32 @@ func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp laye
|
|||||||
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
|
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
|
||||||
return int(a.Type) - int(b.Type)
|
return int(a.Type) - int(b.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// msgType returns the message type of msg, if it's present within the options.
|
||||||
|
func msgType(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestedIP(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iface *dhcpInterfaceV4) handleDiscover(ctx context.Context, msg *layers.DHCPv4) {
|
||||||
|
// TODO(e.burkov): Implement.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iface *dhcpInterfaceV4) handleRequest(ctx context.Context, msg *layers.DHCPv4) {
|
||||||
|
// TODO(e.burkov): Implement.
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,23 +97,27 @@ func newDHCPInterfaceV6(
|
|||||||
l *slog.Logger,
|
l *slog.Logger,
|
||||||
name string,
|
name string,
|
||||||
conf *IPv6Config,
|
conf *IPv6Config,
|
||||||
) (i *dhcpInterfaceV6) {
|
) (iface *dhcpInterfaceV6) {
|
||||||
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
|
|
||||||
if !conf.Enabled {
|
if !conf.Enabled {
|
||||||
l.DebugContext(ctx, "disabled")
|
l.DebugContext(ctx, "disabled")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
i = &dhcpInterfaceV6{
|
iface = &dhcpInterfaceV6{
|
||||||
rangeStart: conf.RangeStart,
|
rangeStart: conf.RangeStart,
|
||||||
common: newNetInterface(name, l, conf.LeaseDuration),
|
common: &netInterface{
|
||||||
|
logger: l,
|
||||||
|
leases: map[macKey]*Lease{},
|
||||||
|
name: name,
|
||||||
|
leaseTTL: conf.LeaseDuration,
|
||||||
|
},
|
||||||
raSLAACOnly: conf.RASLAACOnly,
|
raSLAACOnly: conf.RASLAACOnly,
|
||||||
raAllowSLAAC: conf.RAAllowSLAAC,
|
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.
|
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
|
||||||
|
|||||||
Reference in New Issue
Block a user