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

252 lines
7.9 KiB
Go

package firewalld
import (
"context"
"github.com/godbus/dbus/v5"
)
// PolicySettings is the transport-neutral representation of a zone-to-zone policy
// (firewalld >= 0.9). Policies have no v1 tuple form; they are exchanged only as
// an a{sv} dict. Ingress and egress zones define the traffic the policy governs;
// the remaining fields mirror a zone's filtering elements.
type PolicySettings struct {
Version string
Short string
Description string
Target string // CONTINUE, ACCEPT, DROP, REJECT
Priority int32
IngressZones []string
EgressZones []string
Services []string
Ports []Port
SourcePorts []Port
ICMPBlocks []string
Masquerade bool
ForwardPorts []ForwardPort
RichRules []string
Protocols []string
}
// toDict builds the a{sv} policy dict with concrete typed values so godbus emits
// the tuple signatures firewalld requires.
func (s *PolicySettings) toDict() map[string]dbus.Variant {
target := s.Target
if target == "" {
target = "CONTINUE"
}
return map[string]dbus.Variant{
"version": dbus.MakeVariant(s.Version),
"short": dbus.MakeVariant(s.Short),
"description": dbus.MakeVariant(s.Description),
"target": dbus.MakeVariant(target),
"priority": dbus.MakeVariant(s.Priority),
"ingress_zones": dbus.MakeVariant(nonNilStrings(s.IngressZones)),
"egress_zones": dbus.MakeVariant(nonNilStrings(s.EgressZones)),
"services": dbus.MakeVariant(nonNilStrings(s.Services)),
"ports": dbus.MakeVariant(nonNilPorts(s.Ports)),
"source_ports": dbus.MakeVariant(nonNilPorts(s.SourcePorts)),
"icmp_blocks": dbus.MakeVariant(nonNilStrings(s.ICMPBlocks)),
"masquerade": dbus.MakeVariant(s.Masquerade),
"forward_ports": dbus.MakeVariant(nonNilForwardPorts(s.ForwardPorts)),
// Policies name rich rules "rich_rules" in their dict (zones use
// "rules_str"); firewalld rejects the wrong key with INVALID_OPTION.
"rich_rules": dbus.MakeVariant(nonNilStrings(s.RichRules)),
"protocols": dbus.MakeVariant(nonNilStrings(s.Protocols)),
}
}
// policySettingsFromDict decodes an a{sv} policy dict, tolerating omitted keys.
func policySettingsFromDict(d map[string]dbus.Variant) PolicySettings {
var s PolicySettings
if v, ok := dictValue(d, "version"); ok {
s.Version = asString(v)
}
if v, ok := dictValue(d, "short"); ok {
s.Short = asString(v)
}
if v, ok := dictValue(d, "description"); ok {
s.Description = asString(v)
}
if v, ok := dictValue(d, "target"); ok {
s.Target = asString(v)
}
if v, ok := dictValue(d, "priority"); ok {
s.Priority = asInt32(v)
}
if v, ok := dictValue(d, "ingress_zones"); ok {
s.IngressZones = asStrings(v)
}
if v, ok := dictValue(d, "egress_zones"); ok {
s.EgressZones = asStrings(v)
}
if v, ok := dictValue(d, "services"); ok {
s.Services = asStrings(v)
}
if v, ok := dictValue(d, "ports"); ok {
s.Ports = asPorts(v)
}
if v, ok := dictValue(d, "source_ports"); ok {
s.SourcePorts = asPorts(v)
}
if v, ok := dictValue(d, "icmp_blocks"); ok {
s.ICMPBlocks = asStrings(v)
}
if v, ok := dictValue(d, "masquerade"); ok {
s.Masquerade = asBool(v)
}
if v, ok := dictValue(d, "forward_ports"); ok {
s.ForwardPorts = asForwardPorts(v)
}
if v, ok := dictValue(d, "rules_str"); ok {
s.RichRules = asStrings(v)
} else if v, ok := dictValue(d, "rich_rules"); ok {
s.RichRules = asStrings(v)
}
if v, ok := dictValue(d, "protocols"); ok {
s.Protocols = asStrings(v)
}
return s
}
// Policies lists the names of all policies known to the runtime. Returns
// ErrUnsupported on servers that predate policy support (firewalld < 0.9).
func (r *Runtime) Policies(ctx context.Context) ([]string, error) {
if !r.c.caps[Policies] {
return nil, ErrUnsupported
}
var names []string
err := r.c.call(ctx, basePath, ifacePolicy, "getPolicies", []any{&names})
return names, err
}
// ActivePolicies returns the policies that currently have zones bound.
func (r *Runtime) ActivePolicies(ctx context.Context) ([]string, error) {
if !r.c.caps[Policies] {
return nil, ErrUnsupported
}
var active map[string]map[string][]string
if err := r.c.call(ctx, basePath, ifacePolicy, "getActivePolicies", []any{&active}); err != nil {
return nil, err
}
names := make([]string, 0, len(active))
for name := range active {
names = append(names, name)
}
return names, nil
}
// PolicySettings reads a policy definition from the running firewall.
func (r *Runtime) PolicySettings(ctx context.Context, policy string) (PolicySettings, error) {
if !r.c.caps[Policies] {
return PolicySettings{}, ErrUnsupported
}
var d map[string]dbus.Variant
if err := r.c.call(ctx, basePath, ifacePolicy, "getPolicySettings", []any{&d}, policy); err != nil {
return PolicySettings{}, err
}
return policySettingsFromDict(d), nil
}
// SetPolicySettings replaces a policy's runtime settings wholesale.
func (r *Runtime) SetPolicySettings(ctx context.Context, policy string, settings PolicySettings) error {
if !r.c.caps[Policies] {
return ErrUnsupported
}
return r.c.call(ctx, basePath, ifacePolicy, "setPolicySettings", nil, policy, settings.toDict())
}
// PolicyNames lists the names of all permanent policies.
func (p *Permanent) PolicyNames(ctx context.Context) ([]string, error) {
if !p.c.caps[Policies] {
return nil, ErrUnsupported
}
var names []string
err := p.c.call(ctx, configPath, ifaceConfig, "getPolicyNames", []any{&names})
return names, err
}
// PolicyPaths lists the config object paths of all permanent policies.
func (p *Permanent) PolicyPaths(ctx context.Context) ([]dbus.ObjectPath, error) {
if !p.c.caps[Policies] {
return nil, ErrUnsupported
}
var paths []dbus.ObjectPath
err := p.c.call(ctx, configPath, ifaceConfig, "listPolicies", []any{&paths})
return paths, err
}
// AddPolicy creates a new permanent policy and returns its config object path.
func (p *Permanent) AddPolicy(ctx context.Context, name string, settings PolicySettings) (dbus.ObjectPath, error) {
if !p.c.caps[Policies] {
return "", ErrUnsupported
}
var path dbus.ObjectPath
err := p.c.call(ctx, configPath, ifaceConfig, "addPolicy", []any{&path}, name, settings.toDict())
return path, err
}
// Policy returns a handle for permanent operations on the named policy.
func (p *Permanent) Policy(name string) *PermPolicy {
return &PermPolicy{c: p.c, name: name}
}
// PermPolicy is a handle for permanent edits to a single policy, operating on the
// policy's config child object (/config/policy/N).
type PermPolicy struct {
c *Conn
name string
path dbus.ObjectPath
}
func (s *PermPolicy) resolve(ctx context.Context) (dbus.ObjectPath, error) {
if !s.c.caps[Policies] {
return "", ErrUnsupported
}
if s.path != "" {
return s.path, nil
}
var path dbus.ObjectPath
if err := s.c.call(ctx, configPath, ifaceConfig, "getPolicyByName", []any{&path}, s.name); err != nil {
return "", err
}
s.path = path
return path, nil
}
func (s *PermPolicy) callOn(ctx context.Context, method string, rets []any, args ...any) error {
path, err := s.resolve(ctx)
if err != nil {
return err
}
return s.c.call(ctx, path, ifaceConfigPolicy, method, rets, args...)
}
// Settings reads the policy's permanent settings.
func (s *PermPolicy) Settings(ctx context.Context) (PolicySettings, error) {
var d map[string]dbus.Variant
if err := s.callOn(ctx, "getSettings", []any{&d}); err != nil {
return PolicySettings{}, err
}
return policySettingsFromDict(d), nil
}
// Update replaces the policy's permanent settings wholesale.
func (s *PermPolicy) Update(ctx context.Context, settings PolicySettings) error {
return s.callOn(ctx, "update", nil, settings.toDict())
}
// Rename changes the policy's name and invalidates the cached path.
func (s *PermPolicy) Rename(ctx context.Context, newName string) error {
if err := s.callOn(ctx, "rename", nil, newName); err != nil {
return err
}
s.name = newName
s.path = ""
return nil
}
// Remove deletes the policy from the permanent configuration.
func (s *PermPolicy) Remove(ctx context.Context) error {
return s.callOn(ctx, "remove", nil)
}