diff --git a/internal/dhcpsvc/handler4.go b/internal/dhcpsvc/handler4.go new file mode 100644 index 00000000..9417f21f --- /dev/null +++ b/internal/dhcpsvc/handler4.go @@ -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) + } +} diff --git a/internal/dhcpsvc/handler6.go b/internal/dhcpsvc/handler6.go new file mode 100644 index 00000000..843f227a --- /dev/null +++ b/internal/dhcpsvc/handler6.go @@ -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 +} diff --git a/internal/dhcpsvc/serve.go b/internal/dhcpsvc/serve.go index 7bedaa78..fabfe632 100644 --- a/internal/dhcpsvc/serve.go +++ b/internal/dhcpsvc/serve.go @@ -2,15 +2,20 @@ 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" ) +// 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) @@ -23,11 +28,15 @@ func (srv *DHCPServer) serve(ctx context.Context) { } 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, pkt) + err = srv.serveV4(ctx, rw, pkt) case layers.EthernetTypeIPv6: - // TODO(e.burkov): Handle DHCPv6 as well. + err = srv.serveV6(ctx, rw, pkt) default: srv.logger.DebugContext(ctx, "skipping ethernet packet", "type", typ) @@ -39,64 +48,3 @@ func (srv *DHCPServer) serve(ctx context.Context) { } } } - -// 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 8ea5a921..1d7c0c51 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -28,7 +28,7 @@ type DHCPServer struct { // logger logs common DHCP events. logger *slog.Logger - // TODO(e.burkov): !! implement and set + // TODO(e.burkov): Implement and set. packetSource gopacket.PacketSource // localTLD is the top-level domain name to use for resolving DHCP clients' @@ -156,7 +156,7 @@ func newInterfaces( func (srv *DHCPServer) Start(ctx context.Context) (err error) { srv.logger.DebugContext(ctx, "starting dhcp server") - // TODO(e.burkov): !! listen to configured interfaces + // TODO(e.burkov): Listen to configured interfaces. go srv.serve(context.Background()) @@ -166,7 +166,7 @@ func (srv *DHCPServer) Start(ctx context.Context) (err error) { func (srv *DHCPServer) Shutdown(ctx context.Context) (err error) { srv.logger.DebugContext(ctx, "shutting down dhcp server") - // TODO(e.burkov): !! close the packet source + // TODO(e.burkov): Close the packet source. return nil } @@ -372,6 +372,8 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) { // 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") }() diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index 22749ef7..4cc9a70e 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -155,22 +155,6 @@ func newDHCPInterfaceV4( return iface, 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 -} - // options returns the implicit and explicit options for the interface. The two // lists are disjoint and the implicit options are initialized with default // values. @@ -362,8 +346,8 @@ 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) { +// 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 @@ -373,7 +357,9 @@ func msgType(msg *layers.DHCPv4) (typ layers.DHCPMsgType, ok bool) { return 0, false } -func requestedIP(msg *layers.DHCPv4) (ip netip.Addr, ok bool) { +// 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) @@ -383,10 +369,80 @@ func requestedIP(msg *layers.DHCPv4) (ip netip.Addr, ok bool) { return netip.Addr{}, false } -func (iface *dhcpInterfaceV4) handleDiscover(ctx context.Context, msg *layers.DHCPv4) { - // TODO(e.burkov): Implement. +// 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 } -func (iface *dhcpInterfaceV4) handleRequest(ctx context.Context, msg *layers.DHCPv4) { - // TODO(e.burkov): Implement. +// 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 }