Reticulum-Go/pkg/common/errors.go

271 lines
12 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package common
import (
"errors"
"fmt"
"io"
"net"
"os"
"strings"
"syscall"
"time"
)
// Developer-facing setup and API error messages.
const (
MsgDestTransportRequiredForIn = "destination: transport is required when direction includes In"
MsgDestTransportNotSet = "destination: transport not set, pass a transport to destination.New"
MsgDestAnnounceNoInterfaces = "destination: announce sent on 0 interfaces (none registered with transport)"
MsgDestAnnounceNoWritable = "destination: announce sent on 0 interfaces (none online, enabled, and writable)"
MsgDestAnnounceRequiresIn = "destination: only IN destination types can be announced"
MsgDestAnnounceThrottled = "destination: announce throttled (wait before announcing again, do not loop Announce)"
MsgDestNoIncomingLinkHandler = `destination: no incoming link handler (import the link package, e.g. _ "quad4/reticulum-go/pkg/link")`
MsgDestAcceptsLinksFalseOnly = "destination: AcceptsLinks(false) clears the flag only and does not unregister from transport"
MsgDestNoPacketCallback = "destination: packet received but no packet callback set (call SetPacketCallback)"
MsgDestNoRequestHandler = "destination: no request handler registered for path (call RegisterRequestHandler)"
MsgLinkNilDestination = "link: NewLink called with nil destination"
MsgLinkNilTransport = "link: NewLink called with nil transport (Establish will fail to send)"
MsgLinkDestinationRequired = "link: destination is required"
MsgLinkTransportRequired = "link: transport is required for Establish"
MsgLinkNoPacketCallback = "link: packet queued with no packet callback (call SetPacketCallback)"
MsgLinkNoPacketCallbackDropped = "link: packet queued with no packet callback (call SetPacketCallback), prior queued packet dropped"
MsgLinkNotActive = "link not active (wait for the established callback before Send, Request, or Identify)"
MsgLinkRequestBusy = "link: too many in-flight requests (wait for receipts, do not loop Request)"
MsgLinkRequestDuplicate = "link: a request for this path is already in flight (wait for the receipt)"
MsgLinkAlreadySettled = "link already established or failed (wait for the established or closed callback, do not call Establish again on this link)"
MsgLinkEstablishBusy = "link: handshake already in progress to this destination (wait for the established callback, do not loop NewLink/Establish)"
MsgTransportNilDestination = "transport: cannot register nil destination"
MsgTransportEmptyDestinationHash = "transport: destination hash is empty"
MsgTransportNoDestForLinkRequest = "transport: no destination registered for hash (create destination with direction In or call RegisterDestination / AcceptsLinks(true))"
MsgTransportNoDestForData = "transport: data for unregistered destination (create destination with direction In or call RegisterDestination)"
MsgTransportNoPathForLinkRelay = "transport: no path to relay link request (call Transport.AwaitPath before Link.Establish)"
MsgTransportLinkRelayDisabled = "transport: link relay refused (enable_transport is off and packet is not from a shared-instance client)"
MsgTransportNoOutgoingForPR = "transport: path request not sent (no online outgoing interface with positive bitrate)"
MsgTransportIfaceNotReadyForPR = "transport: interface offline, receive-only, or has no bitrate"
MsgPathRequestThrottled = "path request throttled (use Transport.AwaitPath, do not loop RequestPath)"
MsgControlAPINoAcceptsLinks = "controlapi: destination registered without accepts_links, inbound link events will not be emitted"
// System and environment error messages.
MsgOOM = "out of memory"
MsgDisk = "disk unavailable or full"
MsgCPU = "cpu resource limit exceeded"
MsgCorruption = "data corruption detected"
MsgSandbox = "sandbox restriction denied the operation"
MsgPortConflict = "port already in use"
MsgConfig = "invalid configuration"
)
// Developer-facing sentinel errors. Prefer returning these (or wrapping them)
// so apps can use errors.Is.
var (
ErrDestTransportRequiredForIn = errors.New(MsgDestTransportRequiredForIn)
ErrDestTransportNotSet = errors.New(MsgDestTransportNotSet)
ErrDestAnnounceNoInterfaces = errors.New(MsgDestAnnounceNoInterfaces)
ErrDestAnnounceNoWritable = errors.New(MsgDestAnnounceNoWritable)
ErrDestAnnounceRequiresIn = errors.New(MsgDestAnnounceRequiresIn)
ErrDestAnnounceThrottled = errors.New(MsgDestAnnounceThrottled)
ErrDestNoIncomingLinkHandler = errors.New(MsgDestNoIncomingLinkHandler)
ErrDestNoPacketCallback = errors.New(MsgDestNoPacketCallback)
ErrDestNoRequestHandler = errors.New(MsgDestNoRequestHandler)
ErrLinkDestinationRequired = errors.New(MsgLinkDestinationRequired)
ErrLinkTransportRequired = errors.New(MsgLinkTransportRequired)
ErrLinkNoPath = errors.New("link: no path to destination")
ErrLinkNoPacketCallback = errors.New(MsgLinkNoPacketCallback)
ErrLinkNotActive = errors.New(MsgLinkNotActive)
ErrLinkRequestBusy = errors.New(MsgLinkRequestBusy)
ErrLinkRequestDuplicate = errors.New(MsgLinkRequestDuplicate)
ErrLinkAlreadySettled = errors.New(MsgLinkAlreadySettled)
ErrLinkEstablishBusy = errors.New(MsgLinkEstablishBusy)
ErrTransportNilDestination = errors.New(MsgTransportNilDestination)
ErrTransportEmptyDestinationHash = errors.New(MsgTransportEmptyDestinationHash)
ErrTransportNoDestForLinkRequest = errors.New(MsgTransportNoDestForLinkRequest)
ErrTransportNoDestForData = errors.New(MsgTransportNoDestForData)
ErrTransportNoPathForLinkRelay = errors.New(MsgTransportNoPathForLinkRelay)
ErrTransportLinkRelayDisabled = errors.New(MsgTransportLinkRelayDisabled)
ErrTransportNoOutgoingForPR = errors.New(MsgTransportNoOutgoingForPR)
ErrTransportIfaceNotReadyForPR = errors.New(MsgTransportIfaceNotReadyForPR)
ErrNoPathToDestination = errors.New("no path to destination")
ErrPathRequestThrottled = errors.New(MsgPathRequestThrottled)
ErrIdentityNotFound = errors.New("identity not found")
// System and environment sentinel errors.
ErrOOM = errors.New(MsgOOM)
ErrDisk = errors.New(MsgDisk)
ErrCPU = errors.New(MsgCPU)
ErrCorruption = errors.New(MsgCorruption)
ErrSandbox = errors.New(MsgSandbox)
ErrPortConflict = errors.New(MsgPortConflict)
ErrConfig = errors.New(MsgConfig)
)
// ErrLinkNoPathf returns a path-missing establish error for destHash.
func ErrLinkNoPathf(destHash []byte) error {
return fmt.Errorf("%w: %x (use Transport.AwaitPath, not a fixed 15 second wait)", ErrLinkNoPath, destHash)
}
// ErrNoPathToDestinationf returns a send/path error for destHash.
func ErrNoPathToDestinationf(destHash []byte) error {
return fmt.Errorf("%w %x (use Transport.AwaitPath, not a tight RequestPath loop)", ErrNoPathToDestination, destHash)
}
// ErrTransportIfaceNotReadyForPRf names the interface that cannot emit a path request.
func ErrTransportIfaceNotReadyForPRf(ifaceName string) error {
return fmt.Errorf("%w: %s (check enabled, online, outgoing, and bitrate)", ErrTransportIfaceNotReadyForPR, ifaceName)
}
// ErrTransportNoPathForLinkRelayf returns a link-relay miss for destHash.
func ErrTransportNoPathForLinkRelayf(destHash []byte) error {
return fmt.Errorf("%w: %x", ErrTransportNoPathForLinkRelay, destHash)
}
// ErrPathRequestThrottledf explains a PathRequestMI suppress with remaining wait.
func ErrPathRequestThrottledf(destHash []byte, wait time.Duration) error {
if wait < 0 {
wait = 0
}
return fmt.Errorf("%w: %x retry in %.1fs", ErrPathRequestThrottled, destHash, wait.Seconds())
}
// ErrIdentityNotFoundf returns a Recall miss for hash.
func ErrIdentityNotFoundf(hash []byte) error {
return fmt.Errorf("%w for hash %x (wait for an announce or call InitKnownDestinationsPersistence)", ErrIdentityNotFound, hash)
}
// ErrConfigf wraps ErrConfig with a detail message.
func ErrConfigf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrConfig, fmt.Sprintf(format, args...))
}
// ErrCorruptionf wraps ErrCorruption with a detail message.
func ErrCorruptionf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrCorruption, fmt.Sprintf(format, args...))
}
// ErrSandboxf wraps ErrSandbox with a detail message.
func ErrSandboxf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrSandbox, fmt.Sprintf(format, args...))
}
// ErrPortConflictf wraps ErrPortConflict with a detail message.
func ErrPortConflictf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrPortConflict, fmt.Sprintf(format, args...))
}
// ErrDiskf wraps ErrDisk with a detail message.
func ErrDiskf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrDisk, fmt.Sprintf(format, args...))
}
// ErrOOMf wraps ErrOOM with a detail message.
func ErrOOMf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrOOM, fmt.Sprintf(format, args...))
}
// ErrCPUf wraps ErrCPU with a detail message.
func ErrCPUf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrCPU, fmt.Sprintf(format, args...))
}
// ClassifyIOError maps common OS and net errors onto library sentinels.
// Unknown errors are returned unchanged.
func ClassifyIOError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, ErrOOM) || errors.Is(err, ErrDisk) || errors.Is(err, ErrCPU) ||
errors.Is(err, ErrCorruption) || errors.Is(err, ErrSandbox) ||
errors.Is(err, ErrPortConflict) || errors.Is(err, ErrConfig) {
return err
}
if IsPortConflict(err) {
return fmt.Errorf("%w: %w", ErrPortConflict, err)
}
if IsDiskFull(err) {
return fmt.Errorf("%w: %w", ErrDisk, err)
}
if IsOOM(err) {
return fmt.Errorf("%w: %w", ErrOOM, err)
}
return err
}
// IsPortConflict reports whether err indicates a bind/listen address conflict.
func IsPortConflict(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrPortConflict) {
return true
}
var opErr *net.OpError
if errors.As(err, &opErr) {
err = opErr.Err
}
var sysErr *os.SyscallError
if errors.As(err, &sysErr) {
err = sysErr.Err
}
if errors.Is(err, syscall.EADDRINUSE) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "address already in use") || strings.Contains(msg, "only one usage of each socket address")
}
// IsDiskFull reports whether err indicates insufficient disk space.
func IsDiskFull(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrDisk) {
return true
}
if errors.Is(err, syscall.ENOSPC) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "no space left") || strings.Contains(msg, "disk full")
}
// IsOOM reports whether err indicates an out-of-memory condition.
func IsOOM(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrOOM) || errors.Is(err, ErrMemoryBudgetExceeded) {
return true
}
if errors.Is(err, syscall.ENOMEM) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "out of memory") || strings.Contains(msg, "cannot allocate memory")
}
// WrapListenError classifies listen/bind failures, especially port conflicts.
func WrapListenError(err error) error {
if err == nil {
return nil
}
return ClassifyIOError(err)
}
// WrapWriteError classifies disk and memory failures from write paths.
func WrapWriteError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, io.ErrShortWrite) {
return err
}
return ClassifyIOError(err)
}