Instead of constantly making these updates, let's just remove the year since things are stored in git anyways, and this is not an actual modern legal risk anymore.
654 lines
20 KiB
Go
654 lines
20 KiB
Go
// Mgmt
|
|
// Copyright (C) James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// Additional permission under GNU GPL version 3 section 7
|
|
//
|
|
// If you modify this program, or any covered work, by linking or combining it
|
|
// with embedded mcl code and modules (and that the embedded mcl code and
|
|
// modules which link with this program, contain a copy of their source code in
|
|
// the authoritative form) containing parts covered by the terms of any other
|
|
// license, the licensors of this program grant you additional permission to
|
|
// convey the resulting work. Furthermore, the licensors of this program grant
|
|
// the original author, James Shubin, additional permission to update this
|
|
// additional permission if he deems it necessary to achieve the goals of this
|
|
// additional permission.
|
|
|
|
package resources
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/user"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/purpleidea/mgmt/engine"
|
|
"github.com/purpleidea/mgmt/engine/traits"
|
|
"github.com/purpleidea/mgmt/lang/funcs/vars"
|
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
|
"github.com/purpleidea/mgmt/lang/types"
|
|
"github.com/purpleidea/mgmt/util"
|
|
"github.com/purpleidea/mgmt/util/errwrap"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/google/nftables"
|
|
)
|
|
|
|
func init() {
|
|
// TODO: Should we split this into firewalld:service and firewalld:port?
|
|
engine.RegisterResource(KindFirewalld, func() engine.Res { return &FirewalldRes{} })
|
|
|
|
// const.res.firewalld.state.exists = "exists"
|
|
// const.res.firewalld.state.absent = "absent"
|
|
vars.RegisterResourceParams(KindFirewalld, map[string]map[string]func() interfaces.Var{
|
|
ParamFileState: {
|
|
FirewalldStateExists: func() interfaces.Var {
|
|
return &types.StrValue{
|
|
V: FirewalldStateExists,
|
|
}
|
|
},
|
|
FirewalldStateAbsent: func() interfaces.Var {
|
|
return &types.StrValue{
|
|
V: FirewalldStateAbsent,
|
|
}
|
|
},
|
|
// TODO: consider removing this field entirely
|
|
"undefined": func() interfaces.Var {
|
|
return &types.StrValue{
|
|
V: FirewalldStateUndefined, // empty string
|
|
}
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
const (
|
|
// KindFirewalld is the kind string used to identify this resource.
|
|
KindFirewalld = "firewalld"
|
|
|
|
// ParamFirewalldState is the name of the state field parameter.
|
|
ParamFirewalldState = "state"
|
|
|
|
// FirewalldStateExists is the string that represents that the service,
|
|
// or port, or other setting should be present.
|
|
FirewalldStateExists = "exists"
|
|
|
|
// FirewalldStateAbsent is the string that represents that the service,
|
|
// or port, or other setting should not exist.
|
|
FirewalldStateAbsent = "absent"
|
|
|
|
// FirewalldStateUndefined means the file state has not been specified.
|
|
// TODO: consider moving to *string and express this state as a nil.
|
|
FirewalldStateUndefined = ""
|
|
|
|
// ErrInvalidZone is a firewalld error from dbus.
|
|
ErrInvalidZone = engine.Error("invalid zone")
|
|
|
|
// ErrInvalidPort is a firewalld error from dbus.
|
|
ErrInvalidPort = engine.Error("invalid port")
|
|
|
|
// ErrInvalidService is a firewalld error from dbus.
|
|
ErrInvalidService = engine.Error("invalid service")
|
|
|
|
// ErrInvalidProtocol is a firewalld error from dbus.
|
|
ErrInvalidProtocol = engine.Error("invalid protocol")
|
|
|
|
// ErrInvalidCommand is a firewalld error from dbus.
|
|
ErrInvalidCommand = engine.Error("invalid command")
|
|
|
|
// ErrMissingProtocol is a firewalld error from dbus.
|
|
ErrMissingProtocol = engine.Error("missing protocol")
|
|
|
|
// ErrAlreadyEnabled is a firewalld error from dbus.
|
|
ErrAlreadyEnabled = engine.Error("already enabled")
|
|
|
|
// ErrNotEnabled is a firewalld error from dbus.
|
|
ErrNotEnabled = engine.Error("not enabled")
|
|
|
|
firewalld1Path = dbus.ObjectPath("/org/fedoraproject/FirewallD1")
|
|
firewalld1Iface = "org.fedoraproject.FirewallD1"
|
|
)
|
|
|
|
// FirewalldRes is a simple resource to interact with the firewalld service. It
|
|
// is not a replacement for a modern, robust tool like `shorewall`, but it has
|
|
// its uses such as for small, desktop use cases. The API of this resource might
|
|
// change to either add new features, split this into multiple resources, or to
|
|
// optimize the execution if it turns out to be too expensive to run large
|
|
// amounts of these as-is. The name variable currently has no useful purpose.
|
|
// Keep in mind that this resource requires root permissions to be able change
|
|
// the firewall settings and to monitor for changes. The change detection uses
|
|
// the nftables monitor facility.
|
|
type FirewalldRes struct {
|
|
traits.Base // add the base methods without re-implementation
|
|
|
|
// XXX: add traits.Reversible and make this undo itself on removal
|
|
|
|
init *engine.Init
|
|
|
|
// Zone is the name of the zone to manage. If unspecified, we will
|
|
// attempt to get the default zone automatically. In this situation, it
|
|
// is possible that this default changes over time if it is acted upon
|
|
// by external tools that use firewalld.
|
|
Zone string `lang:"zone" yaml:"zone"`
|
|
|
|
// State is the desired state.
|
|
State string `lang:"state" yaml:"state"`
|
|
|
|
// Services are the list of services to manage to the desired state.
|
|
// These are single lower case strings like `dhcp`, and `tftp`.
|
|
Services []string `lang:"services" yaml:"services"`
|
|
|
|
// Ports are the list of port/protocol combinations to manage to the
|
|
// desired state. These are strings of port number (slash) protocol like
|
|
// `4280/tcp` and `38/udp`.
|
|
Ports []string `lang:"ports" yaml:"ports"`
|
|
|
|
// TODO: add a boolean setting for persistence (across reboots)
|
|
// TODO: this is the equivalent of `firewall-cmd --permanent`
|
|
//Permanent bool `lang:"permanent" yaml:"permanent"`
|
|
|
|
zone string // cached name of the zone we're managing
|
|
call callFunc
|
|
wg *sync.WaitGroup
|
|
}
|
|
|
|
// Default returns some sensible defaults for this resource.
|
|
func (obj *FirewalldRes) Default() engine.Res {
|
|
return &FirewalldRes{}
|
|
}
|
|
|
|
// Validate if the params passed in are valid data.
|
|
func (obj *FirewalldRes) Validate() error {
|
|
// Check that we're running as root or with enough capabilities.
|
|
// XXX: check that we have enough capabilities if we're not root
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "error looking up current user")
|
|
}
|
|
if currentUser.Uid != "0" {
|
|
// TODO: Technically we only need perms to watch state and to
|
|
// change things, so without this we could just error entirely
|
|
// if this were only used as a change detection tool.
|
|
return fmt.Errorf("running as root is required for firewalld")
|
|
}
|
|
|
|
if obj.State != "" {
|
|
if obj.State != FirewalldStateExists && obj.State != FirewalldStateAbsent {
|
|
return fmt.Errorf("invalid state: %s", obj.State)
|
|
}
|
|
}
|
|
|
|
for _, x := range obj.Services {
|
|
// TODO: we could check services from a list
|
|
if x == "" {
|
|
return fmt.Errorf("service is empty")
|
|
}
|
|
}
|
|
|
|
for _, x := range obj.Ports {
|
|
split := strings.Split(x, "/")
|
|
if len(split) != 2 {
|
|
return fmt.Errorf("port/protocol was invalid: %s", x)
|
|
}
|
|
|
|
if num, err := strconv.Atoi(split[0]); err != nil {
|
|
return err
|
|
} else if num <= 0 {
|
|
return fmt.Errorf("invalid number: %d", num)
|
|
}
|
|
|
|
// TODO: we could check protocols from a list
|
|
if split[1] == "" {
|
|
return fmt.Errorf("protocol is empty")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Init runs some startup code for this resource.
|
|
func (obj *FirewalldRes) Init(init *engine.Init) error {
|
|
obj.init = init // save for later
|
|
|
|
obj.wg = &sync.WaitGroup{}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Cleanup is run by the engine to clean up after the resource is done.
|
|
func (obj *FirewalldRes) Cleanup() error {
|
|
return nil
|
|
}
|
|
|
|
// Watch is the primary listener for this resource and it outputs events. This
|
|
// could have been implemented with an exec of `/usr/sbin/nft --json monitor`,
|
|
// but the wrapped polkit and difficulty getting permissions using capabilities
|
|
// and without using full root, was challenging. This is cleaner too.
|
|
func (obj *FirewalldRes) Watch(ctx context.Context) error {
|
|
defer obj.wg.Wait()
|
|
|
|
// `sudo setcap CAP_NET_ADMIN=+eip mgmt` seems to let us avoid root
|
|
opts := []nftables.ConnOption{}
|
|
conn, err := nftables.New(opts...) // (*nftables.Conn, error)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// XXX: filter out events we don't care about using WithMonitorObject
|
|
//opt := nftables.WithMonitorObject(???)
|
|
mopts := []nftables.MonitorOption{}
|
|
//mopts = append(mopts, opt)
|
|
monitor := nftables.NewMonitor(mopts...) // *nftables.Monitor
|
|
defer monitor.Close()
|
|
events, err := conn.AddMonitor(monitor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
obj.init.Running() // when started, notify engine that we're running
|
|
|
|
var send = false // send event?
|
|
for {
|
|
select {
|
|
case event, ok := <-events: // &nftables.MonitorEvent
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if event.Error != nil {
|
|
return errwrap.Wrapf(err, "monitor err")
|
|
}
|
|
if obj.init.Debug {
|
|
//obj.init.Logf("event: %#v", event)
|
|
obj.init.Logf("event type: %v", event.Type)
|
|
//obj.init.Logf("event data: %+v", event.Data)
|
|
}
|
|
|
|
send = true
|
|
|
|
case <-ctx.Done(): // closed by the engine to signal shutdown
|
|
return nil
|
|
}
|
|
|
|
// do all our event sending all together to avoid duplicate msgs
|
|
if send {
|
|
send = false
|
|
obj.init.Event() // notify engine of an event (this can block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// CheckApply checks the resource state and applies the resource if the bool
|
|
// input is true. It returns error info and if the state check passed or not.
|
|
func (obj *FirewalldRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
|
|
conn, err := util.SystemBusPrivateUsable()
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "failed to connect to the private system bus")
|
|
}
|
|
defer conn.Close()
|
|
|
|
firewalldObject := conn.Object(firewalld1Iface, firewalld1Path) // TODO: can we reuse this?
|
|
var flags dbus.Flags // none set
|
|
obj.call = func(ctx context.Context, method string, args ...interface{}) *dbus.Call {
|
|
fullMethod := firewalld1Iface + method // eg: ".getDefaultZone"
|
|
return firewalldObject.CallWithContext(ctx, fullMethod, flags, args...)
|
|
}
|
|
|
|
// NOTE: The default zone might change over time by external users...
|
|
obj.zone = obj.Zone // default to this
|
|
if obj.Zone == "" {
|
|
if err := obj.call(ctx, ".getDefaultZone").Store(&obj.zone); err != nil {
|
|
return false, err
|
|
}
|
|
if obj.zone == "" {
|
|
return false, fmt.Errorf("unexpected empty zone")
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("zone: %s", obj.zone)
|
|
}
|
|
}
|
|
|
|
checkOK := true
|
|
|
|
// This ordering doesn't currently matter, but might change if we find
|
|
// any sort of relevant relationship.
|
|
for _, x := range obj.Services {
|
|
if c, err := obj.serviceCheckApply(ctx, apply, x); err != nil {
|
|
return false, err
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
}
|
|
|
|
for _, x := range obj.Ports {
|
|
if c, err := obj.portCheckApply(ctx, apply, x); err != nil {
|
|
return false, err
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
}
|
|
|
|
return checkOK, nil
|
|
}
|
|
|
|
// does the equivalent of: `firewall-cmd --zone=<zone> --list-services` and:
|
|
// `firewall-cmd --zone=<zone> --add-service=<service>` and: `firewall-cmd
|
|
// --zone=<zone> --remove-service=<service>`.
|
|
func (obj *FirewalldRes) serviceCheckApply(ctx context.Context, apply bool, service string) (bool, error) {
|
|
// .zone.getServices(s: zone) -> as
|
|
var services []string
|
|
args := []interface{}{obj.zone}
|
|
if err := obj.call(ctx, ".zone.getServices", args...).Store(&services); err != nil {
|
|
if parseError(err) == ErrInvalidZone {
|
|
obj.init.Logf("did the zone change?") // two managers!
|
|
}
|
|
return false, err
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("services: %+v", services)
|
|
}
|
|
|
|
found := util.StrInList(service, services)
|
|
exists := obj.State != FirewalldStateAbsent // other states mean "exists"
|
|
|
|
if exists && found || !exists && !found {
|
|
return true, nil
|
|
}
|
|
|
|
if !apply {
|
|
return false, nil
|
|
}
|
|
|
|
if !found {
|
|
// add it
|
|
// .zone.addService(s: zone, s: service, i: timeout) -> s
|
|
timeout := 0 // TODO: what should this be?
|
|
var output string
|
|
addArgs := []interface{}{obj.zone, service, timeout}
|
|
if err := obj.call(ctx, ".zone.addService", addArgs...).Store(&output); err != nil && parseError(err) != ErrAlreadyEnabled {
|
|
return false, err
|
|
}
|
|
obj.init.Logf("service added: %s", service)
|
|
if output != obj.zone {
|
|
// programming error
|
|
return false, fmt.Errorf("dbus API inconsistency")
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// remove it
|
|
// .zone.removeService(s: zone, s: service) -> s
|
|
//timeout := 0
|
|
var output string
|
|
removeArgs := []interface{}{obj.zone, service}
|
|
if err := obj.call(ctx, ".zone.removeService", removeArgs...).Store(&output); err != nil && parseError(err) != ErrNotEnabled {
|
|
return false, err
|
|
}
|
|
obj.init.Logf("service removed: %s", service)
|
|
if output != obj.zone {
|
|
// programming error
|
|
return false, fmt.Errorf("dbus API inconsistency")
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// does the equivalent of: `firewall-cmd --zone=<zone> --list-ports` and:
|
|
// `firewall-cmd --zone=<zone> --add-port=4280/tcp` and: `firewall-cmd
|
|
// --zone=<zone> --remove-port=4280/tcp`.
|
|
func (obj *FirewalldRes) portCheckApply(ctx context.Context, apply bool, pp string) (bool, error) {
|
|
split := strings.Split(pp, "/")
|
|
if len(split) != 2 { // should already be checked in Validate
|
|
return false, fmt.Errorf("port/protocol was invalid: %s", pp)
|
|
}
|
|
port, err := strconv.Atoi(split[0])
|
|
if err != nil {
|
|
return false, err
|
|
} else if port <= 0 {
|
|
return false, fmt.Errorf("invalid number: %d", port)
|
|
}
|
|
protocol := split[1]
|
|
|
|
// .zone.getPorts(s: zone) -> aas
|
|
var ports [][]string
|
|
args := []interface{}{obj.zone}
|
|
if err := obj.call(ctx, ".zone.getPorts", args...).Store(&ports); err != nil {
|
|
if parseError(err) == ErrInvalidZone {
|
|
obj.init.Logf("did the zone change?") // two managers!
|
|
}
|
|
return false, err
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("ports: %+v", ports)
|
|
}
|
|
|
|
found := false
|
|
for i, x := range ports {
|
|
// eg: ["1025-65535", "udp"]
|
|
if len(x) != 2 {
|
|
return false, fmt.Errorf("unexpected ports length (%d), got: %v", len(x), x)
|
|
}
|
|
|
|
// x[0] is range or single value, eg: "1025-65535" OR "42"
|
|
// x[1] is proto like "tcp" or "udp"
|
|
if obj.init.Debug {
|
|
obj.init.Logf("rule %d: %s/%s", i, x[0], x[1])
|
|
}
|
|
|
|
if protocol != x[1] {
|
|
continue // not found (not us)
|
|
}
|
|
|
|
// does the port number match?
|
|
split := strings.Split(x[0], "-") // split the range
|
|
if len(split) != 1 && len(split) != 2 {
|
|
return false, fmt.Errorf("unexpected ports format (%d), got: %v", len(split), split)
|
|
}
|
|
if len(split) == 1 { // standalone single value
|
|
if num, err := strconv.Atoi(split[0]); err != nil {
|
|
return false, err // programming error
|
|
} else if num != port {
|
|
continue // not found
|
|
}
|
|
found = true // yay!
|
|
break
|
|
}
|
|
//if len(split) == 2
|
|
lhs, err := strconv.Atoi(split[0])
|
|
if err != nil {
|
|
return false, err // programming error
|
|
}
|
|
rhs, err := strconv.Atoi(split[1])
|
|
if err != nil {
|
|
return false, err // programming error
|
|
}
|
|
|
|
if lhs <= port && port <= rhs { // ranges are inclusive on both boundss
|
|
found = true // yay!
|
|
break
|
|
}
|
|
}
|
|
|
|
exists := obj.State != FirewalldStateAbsent // other states mean "exists"
|
|
|
|
if exists && found || !exists && !found {
|
|
return true, nil
|
|
}
|
|
|
|
if !apply {
|
|
return false, nil
|
|
}
|
|
|
|
if !found {
|
|
// add it
|
|
// .zone.addPort(s: zone, s: port, s: protocol, i: timeout) -> s
|
|
timeout := 0 // TODO: what should this be?
|
|
var output string
|
|
addArgs := []interface{}{obj.zone, strconv.Itoa(port), protocol, timeout}
|
|
if err := obj.call(ctx, ".zone.addPort", addArgs...).Store(&output); err != nil && parseError(err) != ErrAlreadyEnabled {
|
|
return false, err
|
|
}
|
|
obj.init.Logf("port added: %s", pp)
|
|
if output != obj.zone {
|
|
// programming error
|
|
return false, fmt.Errorf("dbus API inconsistency")
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// remove it
|
|
// .zone.removePort(s: zone, s: port, s: protocol) -> s
|
|
//timeout := 0
|
|
var output string
|
|
removeArgs := []interface{}{obj.zone, strconv.Itoa(port), protocol}
|
|
if err := obj.call(ctx, ".zone.removePort", removeArgs...).Store(&output); err != nil && parseError(err) != ErrNotEnabled {
|
|
return false, err
|
|
}
|
|
obj.init.Logf("port removed: %s", pp)
|
|
if output != obj.zone {
|
|
// programming error
|
|
return false, fmt.Errorf("dbus API inconsistency")
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
|
func (obj *FirewalldRes) Cmp(r engine.Res) error {
|
|
// we can only compare FirewalldRes to others of the same resource kind
|
|
res, ok := r.(*FirewalldRes)
|
|
if !ok {
|
|
return fmt.Errorf("not a %s", obj.Kind())
|
|
}
|
|
|
|
if obj.Zone != res.Zone {
|
|
return fmt.Errorf("the Zone differs")
|
|
}
|
|
if obj.State != res.State {
|
|
return fmt.Errorf("the State differs")
|
|
}
|
|
|
|
if len(obj.Services) != len(res.Services) {
|
|
return fmt.Errorf("the Services differ")
|
|
}
|
|
for i, x := range obj.Services {
|
|
if x != res.Services[i] {
|
|
return fmt.Errorf("the service at index %d differs", i)
|
|
}
|
|
}
|
|
|
|
if len(obj.Ports) != len(res.Ports) {
|
|
return fmt.Errorf("the Ports differ")
|
|
}
|
|
for i, x := range obj.Ports {
|
|
if x != res.Ports[i] {
|
|
return fmt.Errorf("the port/protocol at index %d differs", i)
|
|
}
|
|
}
|
|
|
|
// TODO: consider adding this setting
|
|
//if obj.Permanent != res.Permanent {
|
|
// return fmt.Errorf("the Permanent differs")
|
|
//}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
|
// primarily useful for setting the defaults.
|
|
func (obj *FirewalldRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
type rawRes FirewalldRes // indirection to avoid infinite recursion
|
|
|
|
def := obj.Default() // get the default
|
|
res, ok := def.(*FirewalldRes) // put in the right format
|
|
if !ok {
|
|
return fmt.Errorf("could not convert to FirewalldRes")
|
|
}
|
|
raw := rawRes(*res) // convert; the defaults go here
|
|
|
|
if err := unmarshal(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
*obj = FirewalldRes(raw) // restore from indirection with type conversion!
|
|
return nil
|
|
}
|
|
|
|
// callFunc is a helper func for simplifying the making of dbus calls.
|
|
type callFunc func(ctx context.Context, method string, args ...interface{}) *dbus.Call
|
|
|
|
// parseError converts a returned error into one of our error constants if it
|
|
// matches. If it doesn't match, then it passes the data through. This is a
|
|
// useful alternative mechanism to inline string matching on the returned error.
|
|
func parseError(err error) error {
|
|
if err == nil {
|
|
return nil // passthrough
|
|
}
|
|
dbusError, ok := err.(dbus.Error)
|
|
if !ok {
|
|
return err // passthrough
|
|
}
|
|
|
|
if s := firewalld1Iface + ".Exception"; dbusError.Name != s {
|
|
return err // passthrough
|
|
}
|
|
|
|
// eg:
|
|
// dbus.Error{
|
|
// Name:"org.fedoraproject.FirewallD1",
|
|
// Body:[]interface {}{
|
|
// "NOT_ENABLED: '4280:tcp' not in 'FedoraWorkstation'",
|
|
// },
|
|
//}
|
|
if len(dbusError.Body) != 1 { // TODO: does this happen?
|
|
return err // passthrough
|
|
}
|
|
|
|
body, ok := (dbusError.Body[0]).(string)
|
|
if !ok {
|
|
return err // passthrough
|
|
}
|
|
|
|
// eg: "NOT_ENABLED: '4280:tcp' not in 'FedoraWorkstation'"
|
|
sep := ": " // the first colon space after the error name
|
|
split := strings.Split(body, sep)
|
|
//other := strings.Join(split[1:], sep) // remaining data (detailed reason)
|
|
switch split[0] {
|
|
case "INVALID_ZONE":
|
|
return ErrInvalidZone
|
|
case "INVALID_PORT":
|
|
return ErrInvalidPort
|
|
case "INVALID_SERVICE":
|
|
return ErrInvalidService
|
|
case "INVALID_PROTOCOL":
|
|
return ErrInvalidProtocol
|
|
case "INVALID_COMMAND":
|
|
return ErrInvalidCommand
|
|
case "MISSING_PROTOCOL":
|
|
return ErrMissingProtocol
|
|
case "ALREADY_ENABLED": // we tried to add it but it was already there
|
|
return ErrAlreadyEnabled
|
|
case "NOT_ENABLED": // we tried to remove it but it was already gone
|
|
return ErrNotEnabled
|
|
}
|
|
|
|
return err // passthrough
|
|
}
|