diff --git a/internal/dhcpsvc/interface.go b/internal/dhcpsvc/interface.go index 87c3de4d..e0785c2a 100644 --- a/internal/dhcpsvc/interface.go +++ b/internal/dhcpsvc/interface.go @@ -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) diff --git a/internal/dhcpsvc/serve.go b/internal/dhcpsvc/serve.go new file mode 100644 index 00000000..7bedaa78 --- /dev/null +++ b/internal/dhcpsvc/serve.go @@ -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 +} diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index 194935c7..8ea5a921 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -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,48 @@ 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. +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) { diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index b5194a9f..22749ef7 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -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,15 +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 + return iface, nil } // 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) { 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. +} diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go index dd75184e..8a63443e 100644 --- a/internal/dhcpsvc/v6.go +++ b/internal/dhcpsvc/v6.go @@ -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.