go-firewalld/errors.go
2026-07-02 14:08:37 -05:00

134 lines
5.2 KiB
Go

package firewalld
import (
"errors"
"strings"
"github.com/godbus/dbus/v5"
)
// exceptionName is the D-Bus error name firewalld raises for all of its domain
// errors. The specific condition (ALREADY_ENABLED, INVALID_ZONE, ...) arrives as
// the leading token of the error message, not as a distinct D-Bus name.
const exceptionName = "org.fedoraproject.FirewallD1.Exception"
// Sentinel errors mapped from firewalld exception messages. Callers match with
// errors.Is. ErrAlreadyEnabled/ErrNotEnabled are the idempotency signals: an
// add of something already present, or a remove of something absent.
var (
ErrAlreadyEnabled = errors.New("firewalld: already enabled")
ErrNotEnabled = errors.New("firewalld: not enabled")
ErrInvalidZone = errors.New("firewalld: invalid zone")
ErrInvalidPort = errors.New("firewalld: invalid port")
ErrInvalidProtocol = errors.New("firewalld: invalid protocol")
ErrInvalidService = errors.New("firewalld: invalid service")
ErrInvalidICMPType = errors.New("firewalld: invalid icmptype")
ErrInvalidIPSet = errors.New("firewalld: invalid ipset")
ErrInvalidInterface = errors.New("firewalld: invalid interface")
ErrInvalidSource = errors.New("firewalld: invalid source")
ErrInvalidPolicy = errors.New("firewalld: invalid policy")
ErrInvalidHelper = errors.New("firewalld: invalid helper")
ErrInvalidCommand = errors.New("firewalld: invalid command")
ErrNameConflict = errors.New("firewalld: name conflict")
ErrZoneConflict = errors.New("firewalld: zone conflict")
ErrBuiltinZone = errors.New("firewalld: builtin zone")
ErrAlreadySet = errors.New("firewalld: already set")
ErrMissingName = errors.New("firewalld: missing name")
ErrNotRunning = errors.New("firewalld: not running")
ErrNotApplicable = errors.New("firewalld: not applicable")
// ErrUnsupported is returned by this library (not firewalld) when a caller
// asks for a feature the connected server's version does not provide.
ErrUnsupported = errors.New("firewalld: operation not supported by this server version")
)
// exceptionMap keys the leading token of a firewalld exception message to a
// sentinel. Tokens are the firewalld INVALID_*/ALREADY_*/... error codes.
var exceptionMap = map[string]error{
"ALREADY_ENABLED": ErrAlreadyEnabled,
"NOT_ENABLED": ErrNotEnabled,
"ZONE_ALREADY_SET": ErrAlreadySet,
"ALREADY_SET": ErrAlreadySet,
"INVALID_ZONE": ErrInvalidZone,
"INVALID_PORT": ErrInvalidPort,
"INVALID_PROTOCOL": ErrInvalidProtocol,
"INVALID_SERVICE": ErrInvalidService,
"INVALID_ICMPTYPE": ErrInvalidICMPType,
"INVALID_IPSET": ErrInvalidIPSet,
"INVALID_INTERFACE": ErrInvalidInterface,
"INVALID_SOURCE": ErrInvalidSource,
"INVALID_POLICY": ErrInvalidPolicy,
"INVALID_HELPER": ErrInvalidHelper,
"INVALID_COMMAND": ErrInvalidCommand,
"NAME_CONFLICT": ErrNameConflict,
"ZONE_CONFLICT": ErrZoneConflict,
"BUILTIN_ZONE": ErrBuiltinZone,
"MISSING_NAME": ErrMissingName,
"NOT_RUNNING": ErrNotRunning,
"NOT_APPLICABLE": ErrNotApplicable,
}
// Error wraps a firewalld D-Bus exception, preserving the raw code and message
// while chaining to a sentinel (via Unwrap) so errors.Is works. Errors that are
// not firewalld exceptions are returned unchanged by mapError.
type Error struct {
Code string // firewalld token, e.g. "INVALID_ZONE"; empty if none parsed
Message string // full exception message from firewalld
sentinel error
}
// Error renders the underlying firewalld message.
func (e *Error) Error() string { return "firewalld: " + e.Message }
// Unwrap exposes the mapped sentinel so errors.Is matches (e.g. ErrInvalidZone).
func (e *Error) Unwrap() error { return e.sentinel }
// mapError converts a raw error from a D-Bus call into a typed *Error when it is a
// firewalld exception, chaining to the matching sentinel. Non-exception errors
// (transport failures, unknown names) are returned unchanged.
func mapError(err error) error {
if err == nil {
return nil
}
var derr dbus.Error
if !errors.As(err, &derr) || derr.Name != exceptionName {
return err
}
msg := dbusErrorMessage(derr)
code := leadingToken(msg)
return &Error{Code: code, Message: msg, sentinel: exceptionMap[code]}
}
// dbusErrorMessage extracts the string body of a firewalld D-Bus error, whose
// first body element is the human-readable message.
func dbusErrorMessage(derr dbus.Error) string {
if len(derr.Body) > 0 {
if s, ok := derr.Body[0].(string); ok {
return s
}
}
return derr.Error()
}
// leadingToken returns the leading UPPER_SNAKE code of a firewalld message, e.g.
// "INVALID_ZONE" from "INVALID_ZONE: 'bogus'". Returns "" when the message does
// not start with such a token.
func leadingToken(msg string) string {
msg = strings.TrimSpace(msg)
end := 0
for end < len(msg) {
c := msg[end]
if (c >= 'A' && c <= 'Z') || c == '_' || (c >= '0' && c <= '9') {
end++
continue
}
break
}
token := msg[:end]
// A real firewalld code is an all-caps run of two or more characters. This
// rejects a stray leading capital from a plain-sentence error message; an
// unmapped-but-well-formed token still round-trips harmlessly (sentinel nil).
if len(token) < 2 {
return ""
}
return token
}