Files
1Panel/agent/app/service/firewall_utils.go

4321 lines
146 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/netip"
"os"
"reflect"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld"
filteriptables "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/iptables"
filternftables "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/nftables"
filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
lifecycleproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
firewallTaskHost = "FirewallTaskHost"
firewallTaskForwarding = "FirewallTaskForwarding"
firewallTaskDocker = "FirewallTaskDocker"
)
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
type firewallLifecycleClient struct{ lifecycle.Client }
type managedMutationRequest struct {
Stored model.FirewallRule
Before filter.FirewallRule
After filter.FirewallRule
RuleSet filter.RuleSet
Locator filter.Locator
AdapterOperation filter.ChangeOperation
Runtime filter.Adapter
}
type firewallDockerRestartError struct {
Err error
}
type firewallCompletedOperationError struct {
Operation string
Err error
}
type firewallVerification struct {
RuleSet filter.RuleSet
Matched bool
}
type firewallRuleBatchItem struct {
snapshot filter.RuleSet
change filter.RuleChange
}
type firewallSyncRule struct {
dto.FirewallRuleSyncItem
desired filter.DesiredRule
observed *filter.ObservedRule
done bool
}
type forwardingRuleSyncCandidate struct {
rule forwarding.Rule
err error
}
type forwardingInventoryItem struct {
ID uint
Rule forwarding.Rule
IsDesired bool
IsRuntime bool
}
type observedInventoryCandidate struct {
rule filter.ObservedRule
ruleKey string
instanceKey string
claimed bool
}
type firewallRuleCollisionIndex map[string][]filter.Action
type firewallSyncDesired[T any] struct {
Value T
Payload dto.FirewallRuleSyncItem
Err error
}
func LoadPanelPort() string {
if !global.IsMaster {
return global.CONF.Base.Port
}
var portSetting model.Setting
_ = global.CoreDB.Where("key = ?", "ServerPort").First(&portSetting).Error
return portSetting.Value
}
func (s *FirewallService) syncPortWhitelist(ctx context.Context, provider filter.Provider, ports []firewall.PortWhitelist) ([]filter.FirewallRule, error) {
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return nil, err
}
rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
if provider != filter.ProviderIptables && provider != filter.ProviderNftables && len(rules) > 0 {
client, err := s.baseClient()
if err != nil {
return nil, err
}
active, err := client.Status()
if err != nil || !active {
return nil, err
}
}
client, err := s.firewallAdapter(provider)
if err != nil {
return nil, err
}
prepared := make([]dto.FirewallRuleCreateItem, 0, len(rules))
var failures []error
activeFamilies := make(map[filter.Family]bool)
for _, rule := range rules {
if err := ctx.Err(); err != nil {
return nil, err
}
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {
active, inspected := activeFamilies[rule.Scope.Family]
if !inspected {
initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), string(rule.Scope.Family))
if err != nil {
return nil, err
}
active = initialized && bound
activeFamilies[rule.Scope.Family] = active
}
if !active {
continue
}
}
port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress}
item, err := prepareFirewallCreateRule(ctx, client, dto.FirewallRuleCreateItem{
Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity, SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + firewall.SystemPortKey(firewall.SystemPort(port)),
})
if err != nil {
failures = append(failures, fmt.Errorf("prepare whitelist rule %s: %w", firewall.SystemPortKey(port), err))
continue
}
prepared = append(prepared, item)
}
if len(failures) > 0 {
return nil, errors.Join(failures...)
}
if len(prepared) == 0 {
return nil, nil
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
scopes := make([]filter.Scope, 0, len(prepared))
for _, item := range prepared {
scopes = append(scopes, item.Rule.Scope)
}
snapshots, err := readMutableFirewallRuleScopes(client, ctx, scopes)
if err != nil {
return nil, err
}
byScope := make(map[string]filter.RuleSet, len(snapshots))
byScopeIdentity := make(map[string]firewallRuleCollisionIndex, len(snapshots))
for _, snapshot := range snapshots {
identities, err := observedFirewallRuleCollisionIndex(snapshot)
if err != nil {
return nil, err
}
byScope[snapshot.Scope.Key()] = snapshot
byScopeIdentity[snapshot.Scope.Key()] = identities
}
var byMatch map[string][]filter.DesiredRule
type whitelistBatch struct {
snapshot filter.RuleSet
changes []filter.RuleChange
records []*model.FirewallRule
}
batches := make([]whitelistBatch, 0)
batchByScope := make(map[string]int)
for _, item := range prepared {
if err := ctx.Err(); err != nil {
return nil, err
}
rule := item.Rule
scope := rule.Scope
identities := byScopeIdentity[scope.Key()]
if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) {
continue
} else if err != nil {
return nil, err
}
if err := identities.Check(rule); err != nil {
return nil, err
}
key, err := filter.RuleMatchKey(rule)
if err != nil {
return nil, err
}
if byMatch == nil {
stored, err := s.rules.List(ctx)
if err != nil {
return nil, err
}
byMatch = make(map[string][]filter.DesiredRule)
for _, record := range stored {
rules, err := compileStoredFirewallRules(ctx, record, client)
if isFirewallPolicyIncompatible(err) {
continue
}
if err != nil {
return nil, err
}
for _, candidate := range rules {
key, err := filter.RuleMatchKey(candidate.Rule)
if err != nil {
return nil, err
}
byMatch[key] = append(byMatch[key], candidate)
}
}
}
var existing *filter.DesiredRule
for _, candidate := range byMatch[key] {
if collision := checkCollisionActions(rule.Action, candidate.Rule.Action); errors.Is(collision, filter.ErrRuleOperation) {
existing = &candidate
} else if collision != nil {
return nil, collision
}
}
var record *model.FirewallRule
if existing != nil && scope.Chain != filter.BasicBeforeChain {
rule = existing.Rule
} else {
rule.UUID = uuid.NewString()
if scope.Chain != filter.BasicBeforeChain {
created, err := firewallRuleModelForCreate(rule, item, constant.FirewallRuleOriginCreated)
if err != nil {
return nil, err
}
created.UUID = rule.UUID
record = &created
}
}
if provider != filter.ProviderFirewalld {
position := int64(1)
rule.OrderIndex = &position
}
index, exists := batchByScope[scope.Key()]
if !exists || provider != filter.ProviderIptables && provider != filter.ProviderNftables {
index = len(batches)
batchByScope[scope.Key()] = index
batches = append(batches, whitelistBatch{snapshot: byScope[scope.Key()]})
}
batches[index].changes = append(batches[index].changes, filter.RuleChange{Operation: filter.ChangeCreate, After: &rule, CommandOnly: true})
batches[index].records = append(batches[index].records, record)
if err := identities.Add(rule); err != nil {
return nil, err
}
}
saveRecords := func(batch whitelistBatch) {
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
for _, record := range batch.records {
if record == nil {
continue
}
if err := s.saveFirewallRule(saveCtx, record); err != nil {
failures = append(failures, err)
if global.LOG != nil {
global.LOG.Errorf("save firewall whitelist rule %s failed: %v", record.UUID, err)
}
}
}
cancel()
}
_, savesRules := client.(filter.RuleSaver)
plans := make([]filter.CommandBatch, 0, len(batches))
completed := make([]int, 0, len(batches))
for batchIndex, batch := range batches {
if err := ctx.Err(); err != nil {
failures = append(failures, err)
break
}
commands, err := client.BuildCommands(batch.snapshot, batch.changes)
commands.CommandOnly = true
if err == nil {
err = client.RunCommands(ctx, commands)
}
if err != nil {
failures = append(failures, err)
if global.LOG != nil {
global.LOG.Errorf("create firewall whitelist rules for %s failed: %v", batch.snapshot.Scope.Key(), err)
}
continue
}
verification := filter.CommandBatch{Provider: commands.Provider, Scope: commands.Scope}
for _, command := range commands.Rules {
expected := command.Expected
expected.Locator.Position = nil
verification.Rules = append(verification.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected})
}
plans = append(plans, verification)
completed = append(completed, batchIndex)
if !savesRules {
saveRecords(batch)
}
}
persistenceErrors := persistFirewallRuleBatches(ctx, client, plans)
for index, batchIndex := range completed {
if err := persistenceErrors[index]; err != nil {
failures = append(failures, err)
continue
}
if savesRules {
saveRecords(batches[batchIndex])
}
}
verification, err := verifyFirewallCommands(ctx, client, plans...)
if err != nil {
return nil, errors.Join(append(failures, err)...)
}
if !verification.Matched {
return nil, errors.Join(append(failures, filter.ErrVerificationFailed)...)
}
created := make([]filter.FirewallRule, 0)
for index, plan := range plans {
if persistenceErrors[index] != nil {
continue
}
for _, command := range plan.Rules {
created = append(created, command.Expected.Rule)
}
}
return created, errors.Join(failures...)
}
func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
if len(ports) == 0 {
return fmt.Errorf("firewall whitelist %s requires a port", serviceType)
}
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
entries, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
servicePorts := make([]firewall.PortWhitelist, 0)
for index := range entries {
if entries[index].Type != serviceType {
continue
}
entries[index].Port = ports[0]
servicePorts = append(servicePorts, entries[index])
}
if len(servicePorts) == 0 {
servicePorts = append(servicePorts, firewall.PortWhitelist{
Type: serviceType, Port: ports[0], Protocol: "tcp",
Sources: []string{"0.0.0.0/0", "::/0"},
})
entries = append(entries, servicePorts...)
}
entries, err = firewall.ValidatePortWhitelist(entries)
if err != nil {
return err
}
value, err := json.Marshal(entries)
if err != nil {
return err
}
client, err := s.baseClient()
if err != nil && !errors.Is(err, lifecycle.ErrNotInstalled) {
return err
}
if err == nil {
allowances := make([]firewall.PortWhitelist, 0, len(servicePorts)*len(ports))
for _, rule := range servicePorts {
for _, port := range ports {
rule.Port = port
allowances = append(allowances, rule)
}
}
if _, err := s.syncPortWhitelist(ctx, filter.Provider(client.Name()), allowances); err != nil {
return err
}
}
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
}
func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
var setting model.Setting
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return nil, err
}
var rules []firewall.PortWhitelist
err := json.Unmarshal([]byte(setting.Value), &rules)
return rules, err
}
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
provider = strings.TrimSpace(provider)
client, err := lifecycle.NewClient(provider)
if err != nil {
return nil, err
}
if provider == "" {
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
}
return client, nil
}
func loadFirewallInitStatus(provider, tab string) (bool, bool, error) {
switch provider {
case constant.FirewallProviderNftables:
return nftables_helper.LoadInitStatus(tab)
case constant.FirewallProviderIptables:
return iptables_helper.LoadInitStatus(tab)
default:
return false, false, fmt.Errorf("unsupported firewall provider: %s", provider)
}
}
func loadSystemFirewallOverview(provider, chainGroup string) (dto.FirewallSubsystemStatus, error) {
var status dto.FirewallSubsystemStatus
var ipv4Err, ipv6Err error
status.IPv4, ipv4Err = loadSystemFirewallFamilyInfo(provider, constant.FirewallFamilyIPv4)
status.IPv6, ipv6Err = loadSystemFirewallFamilyInfo(provider, constant.FirewallFamilyIPv6)
if chainGroup != "base" {
return status, nil
}
if ipv4Err != nil {
return status, ipv4Err
}
if provider == constant.FirewallProviderIptables {
status.IsInit, status.IsBind = status.IPv4.Initialized, status.IPv4.Bound
return status, nil
}
if !status.IPv4.Initialized {
return status, nil
}
if !status.IPv4.Bound {
status.IsInit = true
return status, nil
}
if ipv6Err != nil {
return status, ipv6Err
}
if status.IPv6.Initialized {
status.IsInit, status.IsBind = true, status.IPv6.Bound
}
return status, nil
}
func loadSystemFirewallFamilyInfo(provider, family string) (dto.FirewallBackendFamilyStatus, error) {
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil || !commands.IPv6Available() {
return dto.FirewallBackendFamilyStatus{Reason: dockerfirewall.ReasonCommandMissing}, nil
}
}
initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family)
return dto.FirewallBackendFamilyStatus{
Available: err == nil,
Initialized: initialized,
Bound: bound,
}, err
}
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
switch provider {
case constant.FirewallProviderIptables:
return iptables_helper.LoadFamilyInitStatus(family, "base")
case constant.FirewallProviderNftables:
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
default:
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
}
}
func (s *FirewallService) updateRuleOrder(ctx context.Context, ruleUUID string, targetPosition *int64, priority *int, description *string) error {
if (targetPosition == nil) == (priority == nil) {
return fmt.Errorf("%w: provide either position or priority", filter.ErrInvalidRule)
}
if ruleUUID == "" {
return fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid)
}
stored, before, runtime, err := s.loadManagedRule(ctx, ruleUUID)
if err != nil {
return err
}
var snapshot filter.RuleSet
var observed filter.ObservedRule
if runtime.Provider() == filter.ProviderNftables {
snapshot, err = readMutableFirewallRules(runtime, ctx, before.Rule.Scope)
if err != nil {
return err
}
observed, err = managedFirewallObserved(snapshot, before)
if err != nil {
return err
}
}
capabilities, err := runtime.Capabilities(ctx)
if err != nil {
return err
}
after := before.Rule
adapterOperation := filter.ChangeReorder
switch {
case capabilities.ExplicitPosition || capabilities.OwnedChains:
if targetPosition == nil || *targetPosition < 1 {
return fmt.Errorf("%w: target position is required", filter.ErrInvalidRule)
}
if runtime.Provider() == filter.ProviderNftables {
if err := validateFirewallRulePosition(snapshot, before.Rule, *targetPosition); err != nil {
return err
}
}
after.OrderIndex = targetPosition
case capabilities.ExplicitPriority:
if before.Rule.NativeKind != filter.NativeKindRichRule {
return fmt.Errorf("%w: only rich rules support explicit priority", filter.ErrUnsupportedScope)
}
if priority == nil {
return fmt.Errorf("%w: priority is required", filter.ErrInvalidRule)
}
after.Priority = priority
adapterOperation = filter.ChangeUpdate
default:
return fmt.Errorf("%w: provider does not support rule reordering", filter.ErrUnsupportedScope)
}
if description != nil {
after.Description = strings.TrimSpace(*description)
}
after, err = prepareFirewallBackendRule(ctx, runtime, after)
if err != nil {
return err
}
metadataOnly, err := isFirewallMetadataOnlyUpdate(before.Rule, after, observed.Locator)
if err != nil {
return err
}
if metadataOnly {
return s.updateRuleDescription(ctx, stored.UUID, after.Description)
}
if runtime.Provider() != filter.ProviderNftables {
return s.replaceManagedRule(ctx, preparedManagedUpdate{Stored: stored, Before: before, After: after, Runtime: runtime})
}
if err := filter.GuardMutation(observed); err != nil {
return err
}
return s.executeManagedMutation(ctx, managedMutationRequest{
Stored: stored, Before: before.Rule, After: after, RuleSet: snapshot, Locator: observed.Locator,
AdapterOperation: adapterOperation, Runtime: runtime,
})
}
func (s *FirewallService) loadManagedRule(ctx context.Context, ruleUUID string) (model.FirewallRule, filter.DesiredRule, filter.Adapter, error) {
stored, err := s.rules.GetByUUID(ctx, ruleUUID)
if err != nil {
return model.FirewallRule{}, filter.DesiredRule{}, nil, err
}
if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted {
return model.FirewallRule{}, filter.DesiredRule{}, nil,
fmt.Errorf("%w: only created or adopted rules can be changed", filter.ErrInvalidRule)
}
selected, err := s.selectedProviderForStoredRule(ctx, stored)
if err != nil {
return model.FirewallRule{}, filter.DesiredRule{}, nil, err
}
if err := checkFirewallRuleWhitelistProtection(selected, stored); err != nil {
return model.FirewallRule{}, filter.DesiredRule{}, nil, err
}
runtime, err := s.resolveRuntime(ctx, selected)
if err != nil {
return model.FirewallRule{}, filter.DesiredRule{}, nil, err
}
desiredRules, err := compileStoredFirewallRules(ctx, stored, runtime)
if err != nil {
return model.FirewallRule{}, filter.DesiredRule{}, nil, err
}
if len(desiredRules) != 1 {
return model.FirewallRule{}, filter.DesiredRule{}, nil,
fmt.Errorf("%w: policy %q expands to %d target rules and cannot be edited atomically", filter.ErrUnsupportedScope, ruleUUID, len(desiredRules))
}
return stored, desiredRules[0], runtime, nil
}
func (s *FirewallService) selectedProviderForStoredRule(ctx context.Context, _ model.FirewallRule) (filter.Provider, error) {
if s.selectedProvider != nil {
return s.selectedProvider(ctx)
}
if s.adapters != nil {
providers := make([]filter.Provider, 0, len(s.adapters))
for provider := range s.adapters {
providers = append(providers, provider)
}
if len(providers) == 1 {
return providers[0], nil
}
}
return "", fmt.Errorf("%w: selected provider is unavailable", filter.ErrProviderUnavailable)
}
func (s *FirewallService) resolveRuntime(ctx context.Context, provider filter.Provider) (filter.Adapter, error) {
if s.selectedProvider != nil {
selected, err := s.selectedProvider(ctx)
if err != nil {
return nil, err
}
if selected != provider {
return nil, fmt.Errorf("%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selected, provider)
}
}
return s.firewallAdapter(provider)
}
func (s *FirewallService) firewallAdapter(provider filter.Provider) (filter.Adapter, error) {
if s.adapters != nil {
if client := s.adapters[provider]; client != nil {
return client, nil
}
return nil, fmt.Errorf("%w: %s", filter.ErrAdapterUnavailable, provider)
}
switch provider {
case filter.ProviderUFW:
return filterufw.NewAdapter(), nil
case filter.ProviderFirewalld:
return filterfirewalld.NewAdapter(), nil
case filter.ProviderIptables:
return filteriptables.NewAdapter(), nil
case filter.ProviderNftables:
return filternftables.NewAdapter(), nil
default:
return nil, fmt.Errorf("%w: %s", filter.ErrAdapterUnavailable, provider)
}
}
func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
rules, err := expandStoredFirewallRule(record, provider)
if err != nil {
return err
}
whitelist := filter.NewPortWhitelistIndex(ports)
for _, rule := range rules {
if whitelist.Matches(rule) {
return filter.ErrProtectedRule
}
}
return nil
}
func loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
ports, err := loadPortWhitelistSetting(global.DB)
if err != nil {
return nil, err
}
return firewall.ValidatePortWhitelist(ports)
}
func expandStoredFirewallRule(rule model.FirewallRule, provider filter.Provider) ([]filter.FirewallRule, error) {
if rule.CompatibilityError != "" {
return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError)
}
connectionStates := make([]string, 0)
if rule.ConnectionStates != "" {
connectionStates = strings.Split(rule.ConnectionStates, ",")
}
base := filter.FirewallRule{
Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
Interface: rule.Interface, ConnectionStates: connectionStates,
Action: filter.Action(rule.Action), Description: rule.Description,
}
if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
base.Protocol = "tcp/udp"
}
if provider == filter.ProviderFirewalld {
base.Priority = rule.Priority
}
families := []filter.Family{filter.Family(rule.Family)}
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
hasIPv4, hasIPv6 := false, false
for _, address := range []string{base.SourceAddress, base.DestinationAddress} {
address = strings.TrimSpace(address)
if address == "" {
continue
}
if strings.Contains(address, ":") {
hasIPv6 = true
} else {
hasIPv4 = true
}
}
switch {
case hasIPv4 && hasIPv6:
return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope)
case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"):
families = []filter.Family{filter.FamilyIPv6}
case hasIPv4:
families = []filter.Family{filter.FamilyIPv4}
default:
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
}
}
result := make([]filter.FirewallRule, 0, len(families))
for _, family := range families {
compiled := base
compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
switch provider {
case filter.ProviderIptables, filter.ProviderNftables:
compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain
case filter.ProviderFirewalld:
compiled.Scope.Zone = filter.FirewalldInputZone
case filter.ProviderUFW:
compiled.Scope.Chain = filter.UFWInputChain
default:
return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider)
}
expanded, err := filter.ExpandAtomicRules(compiled)
if err != nil {
return nil, err
}
result = append(result, expanded...)
}
return result, nil
}
func compileStoredFirewallRules(ctx context.Context, stored model.FirewallRule, client filter.Adapter) ([]filter.DesiredRule, error) {
rules, err := expandStoredFirewallRule(stored, client.Provider())
if err != nil {
return nil, err
}
capabilities, err := client.Capabilities(ctx)
if err != nil {
return nil, err
}
result := make([]filter.DesiredRule, 0, len(rules))
for ordinal, rule := range rules {
prepared, err := prepareFirewallBackendRule(ctx, client, rule)
if err != nil {
return nil, err
}
ruleKey, err := filter.RuleKey(prepared)
if err != nil {
return nil, err
}
prepared.UUID = stored.UUID
if ordinal > 0 {
suffix := ruleKey
if len(suffix) > 12 {
suffix = suffix[:12]
}
prepared.UUID = fmt.Sprintf("%s-%d-%s", stored.UUID, ordinal+1, suffix)
}
desired := filter.DesiredRule{UUID: stored.UUID, Rule: prepared, RuleKey: ruleKey, Origin: filter.RuleOrigin(stored.Origin)}
if capabilities.Marker {
desired.Marker = "1panel-rule:" + prepared.UUID
}
result = append(result, desired)
}
return result, nil
}
func prepareFirewallBackendRule(ctx context.Context, client filter.Adapter, rule filter.FirewallRule) (filter.FirewallRule, error) {
if preparer, ok := client.(filter.RulePreparer); ok {
var err error
rule, err = preparer.PrepareRule(rule)
if err != nil {
return filter.FirewallRule{}, err
}
}
if checker, ok := client.(filter.RuleChecker); ok {
if err := checker.CheckRule(ctx, rule); err != nil {
return filter.FirewallRule{}, err
}
}
return rule, nil
}
func readMutableFirewallRules(client filter.Adapter, ctx context.Context, scope filter.Scope) (filter.RuleSet, error) {
snapshots, err := readMutableFirewallRuleScopes(client, ctx, []filter.Scope{scope})
if err != nil {
return filter.RuleSet{}, err
}
return snapshots[0], nil
}
func firewallScopeReadGroups(scopes []filter.Scope) [][]filter.Scope {
groups := make([][]filter.Scope, 0)
indexes := make(map[string]int)
for _, scope := range scopes {
scope = scope.Normalize()
key := scope.Key()
if scope.Provider == filter.ProviderUFW {
key = string(scope.Provider)
}
if scope.Provider == filter.ProviderIptables || scope.Provider == filter.ProviderNftables {
key = string(scope.Provider) + ":" + string(scope.Family) + ":" + scope.Table
}
if index, ok := indexes[key]; ok {
groups[index] = append(groups[index], scope)
} else {
indexes[key] = len(groups)
groups = append(groups, []filter.Scope{scope})
}
}
return groups
}
func listFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
unique := make([]filter.Scope, 0, len(scopes))
seen := make(map[string]bool)
for _, scope := range scopes {
scope = scope.Normalize()
if err := scope.ValidateMVP(); err != nil {
return nil, err
}
if !seen[scope.Key()] {
seen[scope.Key()] = true
unique = append(unique, scope)
}
}
if len(unique) == 0 {
return nil, nil
}
if reader, ok := client.(filter.MultiScopeReader); ok {
snapshots, err := reader.ListRuleScopes(ctx, unique)
if err != nil {
return nil, err
}
if len(snapshots) != len(unique) {
return nil, filter.ErrInventoryUnavailable
}
return snapshots, nil
}
snapshots := make([]filter.RuleSet, 0, len(unique))
for _, scope := range unique {
snapshot, err := client.ListRules(ctx, scope)
if err != nil {
return nil, err
}
snapshots = append(snapshots, snapshot)
}
return snapshots, nil
}
func readMutableFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
snapshots, err := readFirewallRuleScopes(client, ctx, scopes)
if err != nil {
return nil, err
}
for _, snapshot := range snapshots {
for _, notice := range snapshot.Notices {
if notice.Code == filter.ScopeNoticeManagedScopeInactive || notice.Code == filter.ScopeNoticeManagedScopeMissing {
return nil, fmt.Errorf("%w: managed firewall scope is unavailable", filter.ErrProviderUnavailable)
}
}
}
return snapshots, nil
}
func readFirewallRules(client filter.Adapter, ctx context.Context, scope filter.Scope) (filter.RuleSet, error) {
snapshot, err := client.ListRules(ctx, scope)
if err != nil {
return filter.RuleSet{}, err
}
ports, err := loadFirewallPortWhiteList()
if err != nil {
return filter.RuleSet{}, err
}
return filter.ProtectRuleSet(snapshot, ports)
}
func firewallRulesByMarker(rules []filter.ObservedRule) map[string][]filter.ObservedRule {
index := make(map[string][]filter.ObservedRule)
for _, rule := range rules {
if rule.Marker != "" {
index[rule.Marker] = append(index[rule.Marker], rule)
}
}
return index
}
func managedFirewallObserved(snapshot filter.RuleSet, desired filter.DesiredRule) (filter.ObservedRule, error) {
matches := make([]filter.ObservedRule, 0, 1)
for _, observed := range snapshot.Rules {
if desired.Marker != "" {
if observed.Marker != desired.Marker {
continue
}
} else if desired.ObservedInstanceKey != "" {
key, err := filter.InstanceKey(observed)
if err != nil || key != desired.ObservedInstanceKey {
continue
}
} else {
key, err := firewallInventoryRuleKey(observed.Rule)
wanted, wantErr := firewallInventoryRuleKey(desired.Rule)
if err != nil || wantErr != nil || key != wanted {
continue
}
}
matches = append(matches, observed)
}
if len(matches) != 1 {
return filter.ObservedRule{}, filter.ErrRuleStale
}
observed := matches[0]
if observed.Protected || desired.Protected {
return filter.ObservedRule{}, filter.ErrProtectedRule
}
if observed.Rule.Scope.Provider != filter.ProviderFirewalld && observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged {
return filter.ObservedRule{}, filter.ErrRuleStale
}
if desired.Marker != "" && observed.ParseStatus == filter.ParseStatusOpaque {
position := observed.Rule.OrderIndex
observed.Rule = desired.Rule
observed.Rule.OrderIndex = position
observed.ParseStatus = filter.ParseStatusSupported
observed.UncertainFields = nil
} else {
expected := desired.Rule
if expected.Scope.Provider == filter.ProviderFirewalld {
expected.Priority = observed.Rule.Priority
expected.NativeKind = observed.Rule.NativeKind
expected.OrderBucket = observed.Rule.OrderBucket
}
if !filter.ObservedRuleMatchesExpected(observed, expected) {
return filter.ObservedRule{}, filter.ErrRuleStale
}
}
return observed, nil
}
func (s *FirewallService) updateRuleDescription(ctx context.Context, ruleUUID, description string) error {
stored, err := s.rules.GetByUUID(ctx, ruleUUID)
if err != nil {
return err
}
selected, err := s.selectedProviderForStoredRule(ctx, stored)
if err != nil {
return err
}
if err := checkFirewallRuleWhitelistProtection(selected, stored); err != nil {
return err
}
if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted {
return fmt.Errorf("%w: only created or adopted rules can be changed", filter.ErrInvalidRule)
}
description = strings.TrimSpace(description)
if stored.Description == description {
return nil
}
return s.rules.UpdateWithRevision(ctx, stored.UUID, stored.Revision, map[string]interface{}{"description": description})
}
func (s *FirewallService) replaceManagedRule(ctx context.Context, prepared preparedManagedUpdate) error {
runtime := prepared.Runtime
snapshot := filter.RuleSet{Scope: prepared.Before.Rule.Scope}
remove, err := runtime.BuildCommands(snapshot, []filter.RuleChange{{
Operation: filter.ChangeDelete, Before: &prepared.Before.Rule, CommandOnly: true,
}})
if err != nil {
return err
}
create, err := runtime.BuildCommands(snapshot, []filter.RuleChange{{
Operation: filter.ChangeCreate, After: &prepared.After, CommandOnly: true, Append: prepared.After.OrderIndex == nil,
}})
if err != nil {
return err
}
updates, err := firewallRuleSemanticUpdates(prepared.After)
if err != nil {
return err
}
if runtime.Provider() == filter.ProviderFirewalld {
updates["priority"] = prepared.After.Priority
}
remove.CommandOnly, create.CommandOnly = true, true
if err := runtime.RunCommands(ctx, remove); err != nil {
return err
}
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
err = s.rules.UpdateWithRevision(saveCtx, prepared.Stored.UUID, prepared.Stored.Revision, updates)
cancel()
if err != nil {
return err
}
runErr := runtime.RunCommands(ctx, create)
if err := errors.Join(runErr, persistFirewallRules(ctx, runtime, create)); err != nil {
return buserr.WithDetail("ErrFirewallRuleSavedApplyFailed", err.Error(), err)
}
return nil
}
func (s *FirewallService) executeManagedMutation(ctx context.Context, request managedMutationRequest) (err error) {
defer func() {
if err != nil && global.LOG != nil {
global.LOG.Errorf("update firewall rule %s failed: %v", request.Stored.UUID, err)
}
}()
before, after := request.Before, request.After
appendRule := after.Scope.Provider == filter.ProviderUFW && after.OrderIndex != nil && *after.OrderIndex == maxObservedFirewallPosition(request.RuleSet)
backendPlan, err := request.Runtime.BuildCommands(request.RuleSet, []filter.RuleChange{{
Operation: request.AdapterOperation, Before: &before, After: &after,
Locator: &request.Locator, Append: appendRule, CommandOnly: true,
}})
if err != nil {
return err
}
backendPlan.CommandOnly = true
updates, err := firewallRuleSemanticUpdates(request.After)
if err != nil {
return err
}
if len(backendPlan.Rules) == 1 && len(backendPlan.Rules[0].Commands) == 2 {
commands := backendPlan.Rules[0]
backendPlan.Rules[0].Commands = commands.Commands[:1]
backendPlan.Rules[0].RollbackCommands = commands.RollbackCommands[:1]
if err := request.Runtime.RunCommands(ctx, backendPlan); err != nil {
return err
}
if after.Scope.Provider == filter.ProviderFirewalld {
updates["priority"] = after.Priority
}
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
err = s.rules.UpdateWithRevision(saveCtx, request.Stored.UUID, request.Stored.Revision, updates)
cancel()
if err != nil {
return err
}
backendPlan.Rules[0].Commands = commands.Commands[1:]
backendPlan.Rules[0].RollbackCommands = commands.RollbackCommands[1:]
runErr := request.Runtime.RunCommands(ctx, backendPlan)
if err := errors.Join(runErr, persistFirewallRules(ctx, request.Runtime, backendPlan)); err != nil {
return buserr.WithDetail("ErrFirewallRuleSavedApplyFailed", err.Error(), err)
}
return nil
}
runErr := request.Runtime.RunCommands(ctx, backendPlan)
if runErr != nil && request.Runtime.Provider() != filter.ProviderFirewalld {
return runErr
}
if err := errors.Join(runErr, persistFirewallRules(ctx, request.Runtime, backendPlan)); err != nil {
return err
}
sameContent, err := filter.SameRuleContent(request.Before, request.After)
if err != nil {
return err
}
if sameContent && request.Stored.Description == request.After.Description {
return nil
}
return s.rules.UpdateWithRevision(ctx, request.Stored.UUID, request.Stored.Revision, updates)
}
func isFirewallPolicyIncompatible(err error) bool {
return errors.Is(err, filter.ErrInvalidRule) || errors.Is(err, filter.ErrUnsupportedScope) ||
errors.Is(err, filter.ErrInvalidScope) || errors.Is(err, filter.ErrCompositeRule)
}
func maxObservedFirewallPosition(snapshot filter.RuleSet) int64 {
maximum := int64(snapshot.LastPosition)
for _, observed := range snapshot.Rules {
if observed.Locator.Position != nil && int64(*observed.Locator.Position) > maximum {
maximum = int64(*observed.Locator.Position)
}
}
return maximum
}
func applyFirewallChanges(client filter.Adapter, ctx context.Context, snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, firewallVerification, error) {
plan, err := client.BuildCommands(snapshot, changes)
if err != nil {
return filter.CommandBatch{}, firewallVerification{}, err
}
err = client.RunCommands(ctx, plan)
if err == nil {
err = persistFirewallRules(ctx, client, plan)
}
if err != nil {
return plan, firewallVerification{}, err
}
verification, err := verifyFirewallCommands(ctx, client, plan)
if plan.Provider == filter.ProviderUFW && len(plan.Rules) == 1 && plan.Rules[0].Operation == filter.ChangeAdopt && (err != nil || !verification.Matched) {
if err == nil {
err = filter.ErrVerificationFailed
}
err = buserr.WithDetail("ErrUFWRuleAdopt", err.Error(), err)
}
if err != nil {
if plan.CreatesOnly() {
return plan, verification, err
}
return plan, verification, rollbackFirewallPlan(ctx, client, plan, err)
}
if !verification.Matched && !plan.CreatesOnly() {
if rollbackErr := restoreFirewallCommands(client, ctx, plan); rollbackErr != nil {
return plan, verification, errors.Join(filter.ErrVerificationFailed, rollbackErr)
}
}
return plan, verification, nil
}
func persistFirewallRules(ctx context.Context, client filter.Adapter, commands filter.CommandBatch) error {
saver, ok := client.(filter.RuleSaver)
if !ok {
return nil
}
err := saver.SaveRules(ctx, commands.Scope)
if err == nil || commands.CreatesOnly() || commands.CommandOnly {
return err
}
if rollbackErr := restoreFirewallCommands(client, ctx, commands); rollbackErr != nil {
return errors.Join(err, rollbackErr)
}
return err
}
func persistFirewallRuleBatches(ctx context.Context, client filter.Adapter, plans []filter.CommandBatch) []error {
failures := make([]error, len(plans))
saver, ok := client.(filter.RuleSaver)
if !ok {
return failures
}
saved := make(map[string]error)
for index, plan := range plans {
key := plan.Scope.Key()
if plan.Provider == filter.ProviderNftables {
key = string(plan.Provider)
}
err, exists := saved[key]
if !exists {
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
err = saver.SaveRules(saveCtx, plan.Scope)
cancel()
saved[key] = err
}
failures[index] = err
}
return failures
}
func restoreFirewallCommands(client filter.Adapter, ctx context.Context, plan filter.CommandBatch) error {
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
return client.Rollback(ctx, plan)
}
func readFirewallCommandResults(ctx context.Context, client filter.Adapter, plans ...filter.CommandBatch) ([]filter.RuleSet, map[string]map[string][]filter.ObservedRule, error) {
scopes := make([]filter.Scope, 0, len(plans))
for _, plan := range plans {
scopes = append(scopes, plan.Scope)
if plan.Provider == filter.ProviderUFW {
related := plan.Scope
if related.Family == filter.FamilyIPv4 {
related.Family = filter.FamilyIPv6
} else {
related.Family = filter.FamilyIPv4
}
scopes = append(scopes, related)
}
}
snapshots, err := listFirewallRuleScopes(client, ctx, scopes)
if err != nil {
return nil, nil, err
}
byIdentity := make(map[string]map[string][]filter.ObservedRule, len(snapshots))
for _, snapshot := range snapshots {
if snapshot.Scope.Provider == filter.ProviderFirewalld {
canonical := make(map[string][]filter.ObservedRule)
for _, observed := range snapshot.Rules {
canonical[observed.Locator.Canonical] = append(canonical[observed.Locator.Canonical], observed)
}
byIdentity[snapshot.Scope.Key()] = canonical
} else {
byIdentity[snapshot.Scope.Key()] = firewallRulesByMarker(snapshot.Rules)
}
}
return snapshots, byIdentity, nil
}
func verifyFirewallCommands(ctx context.Context, client filter.Adapter, plans ...filter.CommandBatch) (firewallVerification, error) {
snapshots, byIdentity, err := readFirewallCommandResults(ctx, client, plans...)
if err != nil {
return firewallVerification{}, err
}
result := firewallVerification{Matched: true}
for _, plan := range plans {
result.Matched = false
for _, snapshot := range snapshots {
if snapshot.Scope.Key() != plan.Scope.Key() {
continue
}
result.RuleSet = snapshot
result.Matched = firewallCommandsMatch(plan, snapshot, snapshots, byIdentity)
break
}
if !result.Matched {
return result, nil
}
}
return result, nil
}
func firewallCommandsMatch(commands filter.CommandBatch, current filter.RuleSet, all []filter.RuleSet, byIdentity map[string]map[string][]filter.ObservedRule) bool {
for _, command := range commands.Rules {
if commands.Provider == filter.ProviderFirewalld {
previous, matched := 0, 0
canonical := byIdentity[current.Scope.Key()]
if command.Previous != nil {
previous = len(canonical[command.Previous.Locator.Canonical])
}
for _, observed := range canonical[command.Expected.Locator.Canonical] {
if observed.Persistence == filter.PersistenceStatusConverged {
want, wantErr := filter.RuleKey(command.Expected.Rule)
got, gotErr := filter.RuleKey(observed.Rule)
if wantErr == nil && gotErr == nil && want == got {
matched++
}
}
}
if command.Operation == filter.ChangeDelete {
if previous != 0 {
return false
}
continue
}
if command.Operation == filter.ChangeUpdate && command.Previous != nil && command.Previous.Locator.Canonical != command.Expected.Locator.Canonical && previous != 0 {
return false
}
if matched != 1 {
return false
}
continue
}
markerMatches, semanticMatches := 0, 0
positionMatches := true
requiresPosition := command.Expected.Locator.Position != nil && (commands.Provider == filter.ProviderUFW || command.Operation == filter.ChangeCreate || commands.Provider == filter.ProviderIptables && (command.Operation == filter.ChangeReorder || command.Operation == filter.ChangeUpdate && command.Expected.Rule.OrderIndex != nil))
if requiresPosition {
positionMatches = false
}
for _, rules := range all {
if commands.Provider != filter.ProviderUFW && rules.Scope.Key() != commands.Scope.Key() {
continue
}
candidates := byIdentity[rules.Scope.Key()][command.Expected.Marker]
if command.Expected.Marker == "" {
candidates = rules.Rules
}
for _, observed := range candidates {
if observed.Marker != command.Expected.Marker {
continue
}
markerMatches++
if rules.Scope.Key() != commands.Scope.Key() {
continue
}
same := false
if commands.Provider == filter.ProviderUFW {
same = observed.ParseStatus == filter.ParseStatusOpaque || filter.ObservedRuleMatchesExpected(observed, command.Expected.Rule)
} else {
want, wantErr := filter.RuleKey(command.Expected.Rule)
got, gotErr := filter.RuleKey(observed.Rule)
same = wantErr == nil && gotErr == nil && want == got
}
if same {
semanticMatches++
}
if observed.Locator.Position != nil && command.Expected.Locator.Position != nil && *observed.Locator.Position == *command.Expected.Locator.Position {
positionMatches = true
}
}
}
if command.Operation == filter.ChangeDelete {
if markerMatches != 0 {
return false
}
continue
}
if markerMatches != 1 || semanticMatches != 1 || !positionMatches {
return false
}
}
return true
}
func rollbackFirewallPlan(ctx context.Context, runtime filter.Adapter, plan filter.CommandBatch, cause error) error {
if runtime == nil {
return cause
}
if err := restoreFirewallCommands(runtime, ctx, plan); err != nil {
return errors.Join(cause, fmt.Errorf("rollback applied firewall plan: %w", err))
}
return cause
}
func firewallRuleSemanticUpdates(rule filter.FirewallRule) (map[string]interface{}, error) {
record, err := firewallRuleFromDomain(rule)
if err != nil {
return nil, err
}
return map[string]interface{}{
"family": record.Family, "protocol": record.Protocol,
"source_address": record.SourceAddress, "source_port": record.SourcePort,
"destination_address": record.DestinationAddress, "destination_port": record.DestinationPort,
"interface": record.Interface, "connection_states": record.ConnectionStates, "action": record.Action,
"description": record.Description, "compatibility_error": "",
}, nil
}
func validateFirewallRulePosition(snapshot filter.RuleSet, rule filter.FirewallRule, target int64) error {
if target < 1 {
return fmt.Errorf("%w: target position must be positive", filter.ErrInvalidRule)
}
if rule.Scope.Provider == filter.ProviderUFW {
minimum, maximum := positionBounds(snapshot)
if target < minimum || target > maximum {
return fmt.Errorf(
"%w: target position %d is outside the %s range %d-%d",
filter.ErrInvalidRule, target, rule.Scope.Family, minimum, maximum,
)
}
return nil
}
maximum := maxObservedFirewallPosition(snapshot)
if target > maximum {
return fmt.Errorf("%w: target position %d is out of range 1-%d", filter.ErrInvalidRule, target, maximum)
}
return nil
}
func positionBounds(snapshot filter.RuleSet) (int64, int64) {
minimum, maximum := int64(0), int64(0)
for _, observed := range snapshot.Rules {
if observed.Locator.Position == nil {
continue
}
position := int64(*observed.Locator.Position)
if minimum == 0 || position < minimum {
minimum = position
}
if position > maximum {
maximum = position
}
}
return minimum, maximum
}
func isFirewallMetadataOnlyUpdate(before, after filter.FirewallRule, locator filter.Locator) (bool, error) {
beforeKey, err := filter.RuleKey(before)
if err != nil {
return false, err
}
afterKey, err := filter.RuleKey(after)
if err != nil {
return false, err
}
if beforeKey != afterKey {
return false, nil
}
if after.Scope.Provider == filter.ProviderFirewalld {
return true, nil
}
return locator.Position != nil && after.OrderIndex != nil && *after.OrderIndex == int64(*locator.Position), nil
}
func (index firewallRuleCollisionIndex) Check(rule filter.FirewallRule) error {
key, err := filter.RuleMatchKey(rule)
if err != nil {
return err
}
for _, action := range index[key] {
if err := checkCollisionActions(rule.Action, action); err != nil {
return err
}
}
return nil
}
func checkCollisionActions(requested, existing filter.Action) error {
if requested == existing {
return fmt.Errorf("%w: equivalent rule already exists", filter.ErrRuleOperation)
}
if filter.OppositeActions(requested, existing) {
return filter.ErrRuleConflict
}
return nil
}
func firewallRuleCollisions(stored []model.FirewallRule, provider filter.Provider) (firewallRuleCollisionIndex, error) {
identities := make(firewallRuleCollisionIndex, len(stored))
for _, candidate := range stored {
rules, err := expandStoredFirewallRule(candidate, provider)
if err != nil {
continue
}
for _, rule := range rules {
if err := identities.Add(rule); err != nil {
return nil, err
}
}
}
return identities, nil
}
func (index firewallRuleCollisionIndex) Add(rule filter.FirewallRule) error {
key, err := filter.RuleMatchKey(rule)
if err != nil {
return err
}
index[key] = append(index[key], rule.Action)
return nil
}
func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider, t *task.Task) error {
result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, t)
if err != nil {
return fmt.Errorf("restore database firewall rules: %w", err)
}
failures := make([]error, 0, len(result.Errors))
for _, failure := range result.Errors {
failures = append(failures, fmt.Errorf("rule %s: %s", failure.SourceUUID, failure.Error))
}
return errors.Join(failures...)
}
func executeFirewallRuleBatches(ctx context.Context, runtime filter.Adapter, items []firewallRuleBatchItem, t *task.Task, record func(int, error)) {
groups := make([][]int, 0)
byScope := make(map[string]int)
for index, item := range items {
key := string(item.change.Operation) + ":" + item.snapshot.Scope.Key()
if runtime.Provider() == filter.ProviderUFW && item.change.Operation == filter.ChangeDelete {
key = string(item.change.Operation)
}
group, exists := byScope[key]
if !exists {
group = len(groups)
byScope[key] = group
groups = append(groups, nil)
}
groups[group] = append(groups[group], index)
}
var plans []filter.CommandBatch
var completed [][]int
_, savesRules := runtime.(filter.RuleSaver)
for _, group := range groups {
if items[group[0]].change.Operation == filter.ChangeDelete {
sort.SliceStable(group, func(i, j int) bool {
left, right := items[group[i]].change.Locator, items[group[j]].change.Locator
if left == nil || right == nil || left.Position == nil || right.Position == nil {
return false
}
return *left.Position > *right.Position
})
}
for start := 0; start < len(group); {
end := start + 1
if runtime.Provider() != filter.ProviderUFW {
end = min(start+filter.MaxAtomicExpansion, len(group))
}
batch := group[start:end]
start = end
changes := make([]filter.RuleChange, 0, len(batch))
for _, index := range batch {
change := items[index].change
change.CommandOnly = true
changes = append(changes, change)
}
err := ctx.Err()
if err == nil {
var plan filter.CommandBatch
plan, err = runtime.BuildCommands(items[batch[0]].snapshot, changes)
if err == nil {
if len(batch) > 1 && changes[0].Operation == filter.ChangeCreate && t != nil {
t.Log(i18n.GetMsgWithMap("FirewallCreateBatchStep", map[string]interface{}{"backend": runtime.Provider(), "count": len(batch)}))
}
plan.CommandOnly = true
err = runtime.RunCommands(ctx, plan)
if err == nil && savesRules {
plans = append(plans, plan)
completed = append(completed, batch)
continue
}
}
}
for _, index := range batch {
record(index, err)
}
}
}
failures := persistFirewallRuleBatches(ctx, runtime, plans)
for index, batch := range completed {
for _, item := range batch {
record(item, failures[index])
}
}
}
func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.FirewallRuleSyncRequest, t *task.Task) (result dto.FirewallRuleSyncResult, err error) {
if request.SourceProvider != "" || request.ResetSource {
return result, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule)
}
if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil {
return result, err
}
result = dto.FirewallRuleSyncResult{Subsystem: "system", TargetProvider: request.TargetProvider}
created, removed, unexecuted := 0, 0, 0
failedRemovals := make(map[string]error)
record := func(operation string, rule *firewallSyncRule, cause error, skipped bool) {
item := rule.FirewallRuleSyncItem
if operation == "TaskDelete" && rule.observed != nil {
item.Rule = &rule.observed.Rule
}
switch {
case skipped:
result.Skipped++
unexecuted++
rule.done = true
case cause != nil:
appendDatabaseSyncFailure(&result, item, cause)
rule.done = true
if operation == "TaskDelete" {
failedRemovals[rule.SourceUUID] = cause
}
case operation == "TaskDelete":
removed++
if rule.Status == firewallsync.StatusRemove {
result.Removed++
rule.done = true
}
case operation == "TaskCreate":
created++
result.Succeeded++
rule.done = true
default:
result.Skipped++
rule.done = true
}
if t == nil {
return
}
label := fmt.Sprintf("%s %s", i18n.GetMsgByKey(operation), item.SourceUUID)
if r := item.Rule; r != nil {
label += fmt.Sprintf(" [%s] %s %s:%s -> %s:%s %s", r.Scope.Key(), r.Protocol, r.SourceAddress, r.SourcePort, r.DestinationAddress, r.DestinationPort, r.Action)
}
if skipped {
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
} else if operation == task.TaskSync && cause == nil {
t.Logf("%s %s", label, i18n.GetMsgByKey("FirewallSyncRuleUnchanged"))
} else {
t.LogWithStatus(label, cause)
}
}
firewallWhitelistMu.Lock()
ports, whitelistErr := loadFirewallPortWhiteList()
var whitelistCreated []filter.FirewallRule
if whitelistErr == nil {
whitelistCreated, whitelistErr = s.syncPortWhitelist(ctx, request.TargetProvider, ports)
}
firewallWhitelistMu.Unlock()
createdWhitelistKeys := make(map[string]bool, len(whitelistCreated))
for _, rule := range whitelistCreated {
key, err := filter.RuleMatchKey(rule)
if err != nil {
return result, err
}
createdWhitelistKeys[key] = true
result.Total++
record("TaskCreate", &firewallSyncRule{FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: rule.UUID, Rule: &rule}}, nil, false)
}
if t != nil {
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
}
if whitelistErr != nil {
return result, whitelistErr
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return result, err
}
runtime, rules, snapshots, err := s.loadFirewallSyncRules(ctx, request)
if err != nil {
return result, err
}
defer func() {
if t != nil {
t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted, "unchanged": result.Skipped - unexecuted}))
}
}()
for _, rule := range rules {
if rule.Status == firewallsync.StatusExisting && rule.Rule != nil {
key, err := filter.RuleMatchKey(*rule.Rule)
if err != nil {
return result, err
}
if createdWhitelistKeys[key] {
rule.done = true
continue
}
}
if rule.Status != firewallsync.StatusRemove {
result.Total++
}
if rule.Status == firewallsync.StatusBlocked {
record("TaskSync", rule, errors.New(rule.Reason), false)
}
}
for _, rule := range rules {
if !rule.done && rule.Status == firewallsync.StatusExisting {
record("TaskSync", rule, nil, false)
}
}
byScope := make(map[string]filter.RuleSet, len(snapshots))
for _, snapshot := range snapshots {
byScope[snapshot.Scope.Key()] = snapshot
}
for _, operation := range []filter.ChangeOperation{filter.ChangeDelete, filter.ChangeCreate} {
name := "TaskCreate"
if operation == filter.ChangeDelete {
name = "TaskDelete"
}
pending := make([]*firewallSyncRule, 0)
items := make([]firewallRuleBatchItem, 0)
for _, rule := range rules {
if rule.done {
continue
}
if operation == filter.ChangeDelete && rule.observed == nil || operation == filter.ChangeCreate && rule.Status != firewallsync.StatusReady {
continue
}
if operation == filter.ChangeCreate {
var cause error
for id, err := range failedRemovals {
if id == rule.SourceUUID || strings.HasPrefix(id, rule.SourceUUID+"-") {
cause = err
break
}
}
if cause != nil {
record(name, rule, cause, true)
continue
}
}
item := firewallRuleBatchItem{snapshot: filter.RuleSet{Scope: rule.Rule.Scope}}
if operation == filter.ChangeDelete {
item.snapshot = byScope[rule.Rule.Scope.Key()]
item.change, err = firewallDeleteChange(*rule.observed, rule.desired)
if err != nil {
record(name, rule, err, false)
continue
}
if runtime.Provider() == filter.ProviderUFW {
item.snapshot.Rules = []filter.ObservedRule{*rule.observed}
}
} else {
after := *rule.Rule
after.OrderIndex = nil
item.change = filter.RuleChange{Operation: operation, After: &after, Append: true}
}
pending = append(pending, rule)
items = append(items, item)
}
executeFirewallRuleBatches(ctx, runtime, items, t, func(index int, failure error) {
record(name, pending[index], failure, false)
})
}
return result, nil
}
func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested filter.Provider) error {
selected, err := s.selectedProvider(ctx)
if err != nil {
return err
}
if selected != requested {
return fmt.Errorf("%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selected, requested)
}
return nil
}
func (s *FirewallService) saveFirewallRule(ctx context.Context, record *model.FirewallRule) error {
if err := s.rules.Create(ctx, record); err != nil {
message := "FirewallCreateRulePersistenceFailed"
if record.Origin == constant.FirewallRuleOriginAdopted {
message = "FirewallAdoptRulePersistenceFailed"
}
return fmt.Errorf("%s: %w", i18n.GetMsgByKey(message), err)
}
return nil
}
func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task) (result dto.FirewallRuleCreateResponse, taskErr error) {
var firstFailure error
defer func() {
if t != nil {
t.Log(i18n.GetMsgWithMap("FirewallCreateRulesResult", map[string]interface{}{
"succeeded": result.Succeeded, "failed": result.Failed, "skipped": result.Skipped,
}))
}
if taskErr == nil {
taskErr = firstFailure
}
}()
type createItem struct {
index, part, count int
request dto.FirewallRuleCreateItem
stored model.FirewallRule
}
describe := func(rule filter.FirewallRule) string {
return fmt.Sprintf("%s %s %s:%s -> %s:%s %s", rule.Scope.Family, rule.Protocol,
rule.SourceAddress, rule.SourcePort, rule.DestinationAddress, rule.DestinationPort, rule.Action)
}
record := func(item createItem, status string, err error) {
rule := item.request.Rule
label := fmt.Sprintf("[%d/%d]", item.index+1, len(request.Items))
if item.count > 1 {
label += fmt.Sprintf("[%d/%d]", item.part+1, item.count)
}
label += fmt.Sprintf(" %s %s", rule.Scope.Provider, describe(rule))
switch status {
case "succeeded":
result.Succeeded++
if t != nil {
t.LogSuccess(label)
}
case "failed":
if firstFailure == nil {
firstFailure = err
}
result.Failed++
if t != nil {
t.LogFailedWithErr(label, err)
}
case "skipped":
result.Skipped++
if t != nil {
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), err)
}
}
if err != nil {
result.Errors = append(result.Errors, dto.FirewallRuleCreateFailure{
Index: item.index, Status: status, Rule: rule, Error: err.Error(),
})
}
}
selected, err := s.selectedProvider(ctx)
if err != nil {
for index, item := range request.Items {
record(createItem{index: index, request: item}, "skipped", err)
}
return result, err
}
runtime, err := s.firewallAdapter(selected)
if err != nil {
for index, item := range request.Items {
record(createItem{index: index, request: item}, "skipped", err)
}
return result, err
}
items := make([]createItem, 0, len(request.Items))
var stop error
for index, item := range request.Items {
if stop == nil {
stop = ctx.Err()
}
if stop != nil {
record(createItem{index: index, request: item}, "skipped", stop)
continue
}
item.Rule.OrderIndex = nil
rules, err := expandFirewallCreateRule(item, selected)
if err != nil {
record(createItem{index: index, request: item}, "failed", err)
continue
}
if item.SourceKind == constant.FirewallRuleSourceImported && t != nil {
t.Log(i18n.GetMsgWithMap("FirewallImportRuleConversion", map[string]interface{}{
"index": index + 1, "total": len(request.Items), "source": item.Rule.Scope.Provider,
"target": selected, "rule": describe(item.Rule), "count": len(rules),
}))
}
for part, rule := range rules {
child := item
child.Rule = rule
entry := createItem{index: index, part: part, count: len(rules), request: child}
prepared, err := prepareFirewallCreateRule(ctx, runtime, child)
if err != nil {
record(entry, "failed", err)
continue
}
entry.request = prepared
items = append(items, entry)
}
}
stored, err := s.rules.List(ctx)
if err != nil {
for _, item := range items {
record(item, "failed", err)
}
return result, err
}
identities, err := firewallRuleCollisions(stored, selected)
if err != nil {
return result, err
}
valid := items[:0]
for _, item := range items {
if stop == nil {
stop = ctx.Err()
}
if stop != nil {
record(item, "skipped", stop)
continue
}
rule := item.request.Rule
if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) {
record(item, "skipped", err)
continue
} else if err != nil {
record(item, "failed", err)
continue
}
item.stored, err = firewallRuleModelForCreate(rule, item.request, constant.FirewallRuleOriginCreated)
if err != nil {
record(item, "failed", err)
continue
}
item.stored.UUID = uuid.NewString()
rule.UUID = item.stored.UUID
item.request.Rule = rule
if err := identities.Add(rule); err != nil {
record(item, "failed", err)
continue
}
valid = append(valid, item)
}
changes := make([]firewallRuleBatchItem, 0, len(valid))
for index := range valid {
rule := &valid[index].request.Rule
changes = append(changes, firewallRuleBatchItem{
snapshot: filter.RuleSet{Scope: rule.Scope},
change: filter.RuleChange{Operation: filter.ChangeCreate, After: rule, Append: true},
})
}
executeFirewallRuleBatches(ctx, runtime, changes, t, func(index int, err error) {
item := valid[index]
if err != nil {
if errors.Is(err, filterfirewalld.ErrAlreadyEnabled) {
record(item, "skipped", err)
} else if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
record(item, "skipped", err)
stop = err
} else {
record(item, "failed", fmt.Errorf("%s: %w", i18n.GetMsgByKey("FirewallCreateRuleExecutionFailed"), err))
}
return
}
persistCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
if err := s.saveFirewallRule(persistCtx, &item.stored); err != nil {
record(item, "failed", err)
} else {
record(item, "succeeded", nil)
}
})
return result, stop
}
func validateFirewallCreateBatch(request dto.FirewallRuleCreate, provider filter.Provider) error {
count := 0
if len(request.Items) > filter.MaxAtomicExpansion {
return fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
}
for _, item := range request.Items {
rules, expandErr := expandFirewallCreateRule(item, provider)
if errors.Is(expandErr, filter.ErrExpansionLimit) {
return fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
}
count += len(rules)
if count > filter.MaxAtomicExpansion {
return fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
}
}
return nil
}
func expandFirewallCreateRule(item dto.FirewallRuleCreateItem, provider filter.Provider) ([]filter.FirewallRule, error) {
if item.SourceKind == constant.FirewallRuleSourceImported {
source := item.Rule.Scope.Normalize().Provider
if source == "" {
source = provider
}
rules, err := filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(item.Rule, source))
if err != nil {
return nil, err
}
var converted []filter.FirewallRule
for _, sourceRule := range rules {
policy, err := firewallRuleFromDomain(sourceRule)
if err != nil {
return nil, err
}
targetRules, err := expandStoredFirewallRule(policy, provider)
if err != nil {
return nil, err
}
converted = append(converted, targetRules...)
}
return converted, nil
}
rule := applySelectedProviderScopeDefaults(item.Rule, provider)
if provider == filter.ProviderUFW && strings.TrimSpace(rule.DestinationPort) != "" {
protocol := strings.ToLower(strings.TrimSpace(rule.Protocol))
if protocol == "" || protocol == "all" || protocol == "any" {
return filter.ExpandAtomicRules(rule)
}
}
return []filter.FirewallRule{rule}, nil
}
func applySelectedProviderScopeDefaults(rule filter.FirewallRule, selected filter.Provider) filter.FirewallRule {
scope := rule.Scope
if scope.Provider == "" {
scope.Provider = selected
}
if scope.Provider != selected {
return rule
}
if scope.Direction == "" {
scope.Direction = filter.DirectionInput
}
if scope.Family == "" {
switch {
case strings.EqualFold(strings.TrimSpace(rule.Protocol), "icmpv6"), strings.Contains(rule.SourceAddress, ":"), strings.Contains(rule.DestinationAddress, ":"):
scope.Family = filter.FamilyIPv6
case selected == filter.ProviderFirewalld:
scope.Family = filter.FamilyInet
default:
scope.Family = filter.FamilyIPv4
}
}
switch selected {
case filter.ProviderIptables, filter.ProviderNftables:
if scope.Table == "" {
scope.Table = "filter"
}
if scope.Chain == "" {
scope.Chain = filter.IptablesInputChain
}
case filter.ProviderFirewalld:
if scope.Zone == "" {
scope.Zone = filter.FirewalldInputZone
}
case filter.ProviderUFW:
if scope.Chain == "" {
scope.Chain = filter.UFWInputChain
}
}
rule.Scope = scope
return rule
}
func prepareFirewallCreateRule(ctx context.Context, runtime filter.Adapter, request dto.FirewallRuleCreateItem) (dto.FirewallRuleCreateItem, error) {
selected := runtime.Provider()
rule, err := filter.NormalizeRule(applySelectedProviderScopeDefaults(request.Rule, selected))
if err != nil {
return dto.FirewallRuleCreateItem{}, err
}
if rule.Scope.Provider != selected {
return dto.FirewallRuleCreateItem{}, fmt.Errorf("%w: selected provider is %s", filter.ErrInvalidRule, selected)
}
rule, err = prepareFirewallBackendRule(ctx, runtime, rule)
if err != nil {
return dto.FirewallRuleCreateItem{}, err
}
rule.UUID = ""
request.Rule = rule
if request.SourceKind == "" {
request.SourceKind = constant.FirewallRuleSourceUser
}
return request, nil
}
func readFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
snapshots, err := listFirewallRuleScopes(client, ctx, scopes)
if err != nil || len(snapshots) == 0 {
return snapshots, err
}
ports, err := loadFirewallPortWhiteList()
if err != nil {
return nil, err
}
for index := range snapshots {
snapshots[index], err = filter.ProtectRuleSet(snapshots[index], ports)
if err != nil {
return nil, err
}
}
return snapshots, nil
}
func observedFirewallRuleCollisionIndex(snapshot filter.RuleSet) (firewallRuleCollisionIndex, error) {
index := make(firewallRuleCollisionIndex, len(snapshot.Rules))
for _, observed := range snapshot.Rules {
if observed.ParseStatus != filter.ParseStatusSupported {
continue
}
if err := index.Add(observed.Rule); err != nil {
return nil, err
}
}
return index, nil
}
func firewallRuleFromDomain(rule filter.FirewallRule) (model.FirewallRule, error) {
normalized, err := filter.NormalizeRule(rule)
if err != nil {
return model.FirewallRule{}, err
}
switch normalized.NativeKind {
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
default:
return model.FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
}
record := model.FirewallRule{
Family: string(normalized.Scope.Family),
Protocol: normalized.Protocol,
SourceAddress: normalized.SourceAddress,
SourcePort: normalized.SourcePort,
DestinationAddress: normalized.DestinationAddress,
DestinationPort: normalized.DestinationPort,
Interface: normalized.Interface,
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
Action: string(normalized.Action),
Description: normalized.Description,
}
return record, nil
}
func sortFirewallRules(rules []model.FirewallRule, provider filter.Provider) {
sort.SliceStable(rules, func(i, j int) bool {
left, right := rules[i], rules[j]
if provider == filter.ProviderFirewalld {
switch {
case left.Priority == nil && right.Priority != nil:
return false
case left.Priority != nil && right.Priority == nil:
return true
case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority:
return *left.Priority < *right.Priority
}
} else {
switch {
case left.Sequence == nil && right.Sequence != nil:
return false
case left.Sequence != nil && right.Sequence == nil:
return true
case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence:
return *left.Sequence < *right.Sequence
}
}
return left.UUID < right.UUID
})
}
func firewallRuleModelForCreate(rule filter.FirewallRule, request dto.FirewallRuleCreateItem, origin string) (model.FirewallRule, error) {
record, err := firewallRuleFromDomain(rule)
if err != nil {
return model.FirewallRule{}, err
}
record.Origin = origin
record.Owner = strings.TrimSpace(request.SourceKind)
if sourceID := strings.TrimSpace(request.SourceID); sourceID != "" {
record.Owner += ":" + sourceID
}
return record, nil
}
func firewallTaskName(operation, subsystem, backend string) string {
name := i18n.GetMsgByKey(subsystem)
if backend != "" {
name += " · " + backend
}
key := "FirewallRule" + operation
if operation == task.TaskExec {
key = "FirewallTaskInitialize"
}
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
}
func closeUnstartedFirewallTask(t *task.Task) {
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(t.TaskID)
if closer, ok := t.Logger.Out.(io.Closer); ok {
_ = closer.Close()
}
}
func whitelistRules(provider filter.Provider, ports, required []firewall.SystemPort) []filter.FirewallRule {
rules := make([]filter.FirewallRule, 0, len(ports)+len(required))
for _, port := range required {
rule := firewall.RuleForSystemPort(provider, firewall.SystemPort(port))
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {
rule.Scope.Chain = filter.BasicBeforeChain
}
rules = append(rules, rule)
}
for _, port := range ports {
rules = append(rules, firewall.RuleForSystemPort(provider, firewall.SystemPort(port)))
}
return rules
}
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
result := make([]firewall.PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
result = append(result, entry)
}
}
return result
}
func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (filter.Adapter, []*firewallSyncRule, []filter.RuleSet, error) {
if request.SourceProvider != "" || request.ResetSource {
return nil, nil, nil, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule)
}
if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil {
return nil, nil, nil, err
}
runtime, err := s.firewallAdapter(request.TargetProvider)
if err != nil {
return nil, nil, nil, err
}
stored, err := s.rules.List(ctx)
if err != nil {
return nil, nil, nil, err
}
sortFirewallRules(stored, request.TargetProvider)
ports, err := loadFirewallPortWhiteList()
if err != nil {
return nil, nil, nil, err
}
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return nil, nil, nil, err
}
whitelistDesired := whitelistRules(request.TargetProvider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
rules := make([]*firewallSyncRule, 0, len(stored))
preservedMarkers := make(map[string]bool)
compileFailed := false
whitelist := filter.NewPortWhitelistIndex(ports)
for _, record := range stored {
desired, preserved, err := s.compileRestorableFirewallRules(ctx, record, runtime, required)
if err != nil {
compileFailed = true
rules = append(rules, &firewallSyncRule{FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Status: firewallsync.StatusBlocked, Reason: err.Error()}})
continue
}
for _, rule := range preserved {
preservedMarkers[rule.Marker] = true
}
for _, native := range desired {
rule := native.Rule
native.Protected = whitelist.Matches(rule)
rules = append(rules, &firewallSyncRule{desired: native, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Rule: &rule}})
}
}
identities := make(firewallRuleCollisionIndex, len(rules))
for _, existing := range rules {
if existing.Rule != nil {
if err := identities.Add(*existing.Rule); err != nil {
return nil, nil, nil, err
}
}
}
for _, candidate := range whitelistDesired {
prepared, err := prepareFirewallCreateRule(ctx, runtime, dto.FirewallRuleCreateItem{Rule: candidate})
if err != nil {
return nil, nil, nil, err
}
rule := prepared.Rule
if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) {
continue
} else if err != nil {
return nil, nil, nil, err
}
if err := identities.Add(rule); err != nil {
return nil, nil, nil, err
}
key, err := filter.RuleKey(rule)
if err != nil {
return nil, nil, nil, err
}
rule.UUID = uuid.NewSHA1(uuid.NameSpaceOID, []byte(key)).String()
rules = append(rules, &firewallSyncRule{
desired: filter.DesiredRule{UUID: rule.UUID, Rule: rule, RuleKey: key, Origin: filter.RuleOriginCreated, Protected: true},
FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: rule.UUID, Rule: &rule},
})
}
scopes := filter.ManagedInputScopes(request.TargetProvider)
if compileFailed {
scopes = slices.DeleteFunc(scopes, func(scope filter.Scope) bool {
return !slices.ContainsFunc(rules, func(rule *firewallSyncRule) bool {
return rule.Rule != nil && rule.Rule.Scope.Key() == scope.Key()
})
})
}
snapshots := make([]filter.RuleSet, 0, len(scopes))
for _, group := range firewallScopeReadGroups(scopes) {
current, err := readMutableFirewallRuleScopes(runtime, ctx, group)
if errors.Is(err, filter.ErrFamilyUnavailable) {
for _, rule := range rules {
if rule.Rule != nil && slices.ContainsFunc(group, func(scope filter.Scope) bool { return scope.Key() == rule.Rule.Scope.Key() }) {
rule.Status, rule.Reason = firewallsync.StatusBlocked, err.Error()
}
}
continue
}
if err != nil {
return nil, nil, nil, err
}
snapshots = append(snapshots, current...)
}
for _, snapshot := range snapshots {
scope := snapshot.Scope
desired := make([]filter.DesiredRule, 0)
byUUID := make(map[string]*firewallSyncRule)
for _, rule := range rules {
if rule.Rule != nil && rule.Rule.Scope.Key() == scope.Key() {
desired = append(desired, rule.desired)
byUUID[rule.desired.Rule.UUID] = rule
}
}
inventory, err := mergeFirewallInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
if err != nil {
return nil, nil, nil, err
}
for _, item := range inventory {
if item.Desired == nil {
if compileFailed || item.Observed == nil || !strings.HasPrefix(item.Observed.Marker, "1panel-rule:") || preservedMarkers[item.Observed.Marker] {
continue
}
observed := item.Observed
if observed.Rule.Scope.Chain == filter.BasicBeforeChain {
continue
}
rule := &firewallSyncRule{observed: observed, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{
SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &observed.Rule, Status: firewallsync.StatusRemove,
ReasonCode: firewallsync.ReasonManagedOnlyInTarget, Reason: firewallSyncReasonMessage(firewallsync.ReasonManagedOnlyInTarget),
}}
if observed.Protected || observed.ParseStatus == filter.ParseStatusOpaque {
rule.Status, rule.ReasonCode = firewallsync.StatusBlocked, firewallsync.ReasonUnsafeRemoval
rule.Reason = firewallSyncReasonMessage(rule.ReasonCode)
}
rules = append(rules, rule)
continue
}
rule := byUUID[item.Desired.Rule.UUID]
rule.observed = item.Observed
if item.Match == filter.InventoryMatchExact && item.Observed != nil && scope.Provider == filter.ProviderFirewalld {
rule.Rule.Priority = item.Observed.Rule.Priority
rule.Rule.NativeKind = item.Observed.Rule.NativeKind
rule.Rule.OrderBucket = item.Observed.Rule.OrderBucket
rule.desired.Rule = *rule.Rule
rule.desired.RuleKey, err = filter.RuleKey(*rule.Rule)
if err != nil {
return nil, nil, nil, err
}
}
divergent := item.Observed != nil && item.Observed.Persistence != "" && item.Observed.Persistence != filter.PersistenceStatusConverged
switch {
case item.Match == filter.InventoryMatchExact && !divergent:
rule.Status, rule.Reason = firewallsync.StatusExisting, "rule already matches database policy"
case item.Match == filter.InventoryMatchMissing || item.Match == filter.InventoryMatchChanged || item.Match == filter.InventoryMatchExact:
rule.Status, rule.Reason = firewallsync.StatusReady, "target rule differs from database policy"
if item.Observed != nil && item.Observed.Protected {
rule.Status, rule.Reason = firewallsync.StatusBlocked, filter.ErrProtectedRule.Error()
}
default:
rule.Status, rule.Reason = firewallsync.StatusBlocked, fmt.Sprintf("target rule cannot be synchronized: %s", item.Match)
}
}
}
return runtime, rules, snapshots, nil
}
func (s *FirewallService) compileRestorableFirewallRules(ctx context.Context, stored model.FirewallRule, client filter.Adapter, required []firewall.PortWhitelist) (restorable, preserved []filter.DesiredRule, err error) {
provider := client.Provider()
compiled, err := compileStoredFirewallRules(ctx, stored, client)
if err != nil {
return nil, nil, err
}
if (provider != filter.ProviderIptables && provider != filter.ProviderNftables) || !strings.HasPrefix(stored.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) {
return compiled, nil, nil
}
requiredPorts := firewall.ExpandPortWhitelist(required)
for _, desired := range compiled {
covered := false
for _, port := range requiredPorts {
covered, err = filter.SameRuleContent(desired.Rule, firewall.RuleForSystemPort(provider, firewall.SystemPort(port)))
if err != nil {
return nil, nil, err
}
if covered {
break
}
}
if covered {
preserved = append(preserved, desired)
} else {
restorable = append(restorable, desired)
}
}
return restorable, preserved, nil
}
func (s *FirewallService) removeTransferredSystemPortRules(ctx context.Context, provider filter.Provider, ports []firewall.PortWhitelist) error {
if provider != filter.ProviderIptables && provider != filter.ProviderNftables {
return nil
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return err
}
runtime, err := s.firewallAdapter(provider)
if err != nil {
return err
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
custom := filter.NewPortWhitelistIndex(customWhitelist(ports))
candidates := make([]model.FirewallRule, 0)
keysByUUID := make(map[string][]string)
scopes := make([]filter.Scope, 0)
for _, record := range stored {
if record.Origin != constant.FirewallRuleOriginCreated || !strings.HasPrefix(record.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) {
continue
}
restorable, preserved, err := s.compileRestorableFirewallRules(ctx, record, runtime, required)
if isFirewallPolicyIncompatible(err) {
continue
}
if err != nil {
return err
}
if len(restorable) != 0 || len(preserved) == 0 || slices.ContainsFunc(preserved, func(rule filter.DesiredRule) bool { return custom.Matches(rule.Rule) }) {
continue
}
for _, desired := range preserved {
rule := desired.Rule
rule.Scope.Chain = filter.BasicBeforeChain
key, err := filter.RuleMatchKey(rule)
if err != nil {
return err
}
keysByUUID[record.UUID] = append(keysByUUID[record.UUID], key)
scopes = append(scopes, rule.Scope)
}
candidates = append(candidates, record)
}
present := make(map[string]bool)
for _, group := range firewallScopeReadGroups(scopes) {
snapshots, err := readMutableFirewallRuleScopes(runtime, ctx, group)
if errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return err
}
for _, snapshot := range snapshots {
for _, observed := range snapshot.Rules {
if observed.ParseStatus != filter.ParseStatusSupported || observed.Rule.Action != filter.ActionAccept || (observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged) {
continue
}
key, err := filter.RuleMatchKey(observed.Rule)
if err != nil {
return err
}
present[key] = true
}
}
}
candidates = slices.DeleteFunc(candidates, func(record model.FirewallRule) bool {
return slices.ContainsFunc(keysByUUID[record.UUID], func(key string) bool { return !present[key] })
})
var failures []error
for ruleUUID, err := range s.rules.DeleteBatchWithRevision(ctx, candidates) {
failures = append(failures, fmt.Errorf("remove transferred system port rule %s: %w", ruleUUID, err))
}
return errors.Join(failures...)
}
func firewallInventoryRuleKey(rule filter.FirewallRule) (string, error) {
if rule.Scope.Provider == filter.ProviderFirewalld {
key, err := filter.RuleMatchKey(rule)
return key + ":" + string(rule.Action), err
}
return filter.RuleKey(rule)
}
func mergeFirewallInventory(input filter.InventoryMergeInput) ([]filter.InventoryItem, error) {
candidates := make([]observedInventoryCandidate, len(input.Observed))
byRuleKey := make(map[string][]int)
byInstanceKey := make(map[string][]int)
byMarker := make(map[string][]int)
bySemanticKey := make(map[string][]int)
byPartialRuleKey := make(map[string][]int)
for index, observed := range input.Observed {
candidate := observedInventoryCandidate{rule: observed}
if marker := strings.TrimSpace(candidate.rule.Marker); marker != "" {
markerKey := candidate.rule.Rule.Scope.Key() + "\x00" + marker
byMarker[markerKey] = append(byMarker[markerKey], index)
}
if observed.ParseStatus == filter.ParseStatusSupported {
normalized, err := filter.NormalizeRule(observed.Rule)
if err != nil {
return nil, fmt.Errorf("normalize observed firewall rule %d: %w", index, err)
}
candidate.rule.Rule = normalized
candidate.ruleKey, err = filter.RuleKey(normalized)
if err != nil {
return nil, err
}
key, err := firewallInventoryRuleKey(normalized)
if err != nil {
return nil, err
}
byRuleKey[key] = append(byRuleKey[key], index)
if instanceKey, err := filter.InstanceKey(candidate.rule); err == nil {
candidate.instanceKey = instanceKey
candidate.rule.InstanceKey = instanceKey
byInstanceKey[instanceKey] = append(byInstanceKey[instanceKey], index)
}
}
if observed.ParseStatus != filter.ParseStatusOpaque {
rule := candidate.rule.Rule
protocolUnknown, supportedFields := false, true
for _, field := range observed.UncertainFields {
if field != filter.ObservedFieldProtocol {
supportedFields = false
break
}
protocolUnknown = true
}
if supportedFields {
key := candidate.ruleKey
var err error
if protocolUnknown {
rule.Protocol = "tcp"
key, err = filter.RuleKey(rule)
} else if key == "" {
key, err = filter.RuleKey(rule)
}
if err == nil {
key += "\x00" + strings.TrimSpace(observed.Marker)
if protocolUnknown {
key = "protocol\x00" + key
byPartialRuleKey[key] = append(byPartialRuleKey[key], index)
} else if observed.ParseStatus != filter.ParseStatusSupported {
key = "exact\x00" + key
byPartialRuleKey[key] = append(byPartialRuleKey[key], index)
} else {
bySemanticKey[key] = append(bySemanticKey[key], index)
}
}
}
}
candidates[index] = candidate
}
normalizedDesired := make([]filter.DesiredRule, 0, len(input.Desired))
desiredMatches := make(map[int]int)
desiredMatchStates := make([]filter.InventoryMatch, 0, len(input.Desired))
for _, desired := range input.Desired {
normalized, err := filter.NormalizeRule(desired.Rule)
if err != nil {
return nil, fmt.Errorf("normalize desired firewall rule %q: %w", desired.UUID, err)
}
desired.Rule = normalized
calculatedKey, err := filter.RuleKey(normalized)
if err != nil {
return nil, err
}
if desired.RuleKey != "" && desired.RuleKey != calculatedKey {
return nil, fmt.Errorf("%w: desired rule %q key does not match its semantics", filter.ErrInvalidRule, desired.UUID)
}
desired.RuleKey = calculatedKey
match, matchState := findObservedInventoryMatch(desired, candidates, byRuleKey, byInstanceKey, byMarker, bySemanticKey, byPartialRuleKey)
normalizedIndex := len(normalizedDesired)
normalizedDesired = append(normalizedDesired, desired)
desiredMatchStates = append(desiredMatchStates, matchState)
if match >= 0 {
candidates[match].claimed = true
desiredMatches[match] = normalizedIndex
}
}
items := make([]filter.InventoryItem, 0, len(candidates)+len(normalizedDesired))
matchedDesired := make(map[int]struct{}, len(desiredMatches))
for index := range candidates {
candidate := &candidates[index]
if desiredIndex, exists := desiredMatches[index]; exists {
desired := normalizedDesired[desiredIndex]
observed := candidate.rule
match := desiredMatchStates[desiredIndex]
if match == filter.InventoryMatchExact && observed.ParseStatus != filter.ParseStatusSupported {
orderIndex := observed.Rule.OrderIndex
observed.Rule = desired.Rule
observed.Rule.OrderIndex = orderIndex
observed.ParseStatus = filter.ParseStatusSupported
observed.UncertainFields = nil
}
displayRule := observed.Rule
displayRule.Description = desired.Rule.Description
state := inventoryStateForDesired(desired, match)
if observed.Protected {
state = filter.InventoryStateProtected
} else if observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged {
state = filter.InventoryStateDrifted
}
items = append(items, filter.InventoryItem{
Rule: displayRule,
Observed: &observed,
Desired: &desired,
State: state,
Match: match,
})
matchedDesired[desiredIndex] = struct{}{}
continue
}
observed := candidate.rule
state := filter.InventoryStateExternal
if observed.Protected {
state = filter.InventoryStateProtected
} else if _, protected := input.ProtectedObservedKeys[candidate.ruleKey]; protected {
state = filter.InventoryStateProtected
}
match := filter.InventoryMatchNone
if observed.ParseStatus != filter.ParseStatusSupported {
match = filter.InventoryMatchOpaque
}
items = append(items, filter.InventoryItem{Rule: observed.Rule, Observed: &observed, State: state, Match: match})
}
for index, desired := range normalizedDesired {
if _, matched := matchedDesired[index]; matched {
continue
}
desiredCopy := desired
match := desiredMatchStates[index]
items = append(items, filter.InventoryItem{
Rule: desired.Rule,
Desired: &desiredCopy,
State: inventoryStateForDesired(desired, match),
Match: match,
})
}
return items, nil
}
func findObservedInventoryMatch(desired filter.DesiredRule, candidates []observedInventoryCandidate, byRuleKey, byInstanceKey, byMarker, bySemanticKey, byPartialRuleKey map[string][]int) (int, filter.InventoryMatch) {
if marker := strings.TrimSpace(desired.Marker); marker != "" {
match, status := matchUnclaimedSemanticCandidate(desired, marker, candidates, bySemanticKey, byPartialRuleKey)
if status != filter.InventoryMatchMissing {
return match, status
}
markerKey := desired.Rule.Scope.Key() + "\x00" + marker
match, status = uniqueUnclaimedCandidate(byMarker, markerKey, candidates)
if match >= 0 && candidates[match].rule.ParseStatus != filter.ParseStatusOpaque &&
!filter.ObservedRuleMatchesExpected(candidates[match].rule, desired.Rule) {
return match, filter.InventoryMatchChanged
}
if status != filter.InventoryMatchMissing {
return match, status
}
if desired.Origin == filter.RuleOriginAdopted {
match, status = matchUnclaimedSemanticCandidate(desired, "", candidates, bySemanticKey, byPartialRuleKey)
if status != filter.InventoryMatchMissing {
if match >= 0 {
return match, filter.InventoryMatchChanged
}
return match, status
}
}
legacyMarker := "1panel-rule:" + strings.TrimSpace(desired.UUID)
if legacyMarker != "1panel-rule:" && legacyMarker != marker {
match, status = matchUnclaimedSemanticCandidate(desired, legacyMarker, candidates, bySemanticKey, byPartialRuleKey)
if status != filter.InventoryMatchMissing {
if match >= 0 {
return match, filter.InventoryMatchChanged
}
return match, status
}
}
return match, status
}
var match int
if desired.ObservedInstanceKey != "" {
match = firstUnclaimedCandidate(byInstanceKey, desired.ObservedInstanceKey, candidates)
} else {
key, err := firewallInventoryRuleKey(desired.Rule)
if err != nil {
return -1, filter.InventoryMatchMissing
}
match = firstUnclaimedCandidate(byRuleKey, key, candidates)
}
if match < 0 {
return -1, filter.InventoryMatchMissing
}
return match, filter.InventoryMatchExact
}
func firstUnclaimedCandidate(byKey map[string][]int, key string, candidates []observedInventoryCandidate) int {
indices := byKey[key]
for len(indices) > 0 && candidates[indices[0]].claimed {
indices = indices[1:]
}
if len(indices) == 0 {
delete(byKey, key)
return -1
}
byKey[key] = indices
return indices[0]
}
func uniqueUnclaimedCandidate(byKey map[string][]int, key string, candidates []observedInventoryCandidate) (int, filter.InventoryMatch) {
match := firstUnclaimedCandidate(byKey, key, candidates)
if match < 0 {
return -1, filter.InventoryMatchMissing
}
for _, index := range byKey[key][1:] {
if !candidates[index].claimed {
return -1, filter.InventoryMatchAmbiguous
}
}
return match, filter.InventoryMatchExact
}
func matchUnclaimedSemanticCandidate(desired filter.DesiredRule, marker string, candidates []observedInventoryCandidate, bySemanticKey, byPartialRuleKey map[string][]int) (int, filter.InventoryMatch) {
key := desired.RuleKey + "\x00" + marker
if match := firstUnclaimedCandidate(bySemanticKey, key, candidates); match >= 0 {
return match, filter.InventoryMatchExact
}
if len(byPartialRuleKey) == 0 {
return -1, filter.InventoryMatchMissing
}
match, status := uniqueUnclaimedCandidate(byPartialRuleKey, "exact\x00"+key, candidates)
if status == filter.InventoryMatchAmbiguous {
return match, status
}
protocolIndependent := desired.Rule
protocolIndependent.Protocol = "tcp"
partialKey, err := filter.RuleKey(protocolIndependent)
if err != nil {
return match, status
}
partial, partialStatus := uniqueUnclaimedCandidate(byPartialRuleKey, "protocol\x00"+partialKey+"\x00"+marker, candidates)
if partialStatus == filter.InventoryMatchAmbiguous || (match >= 0 && partial >= 0) {
return -1, filter.InventoryMatchAmbiguous
}
if match >= 0 {
return match, status
}
return partial, partialStatus
}
func inventoryStateForDesired(desired filter.DesiredRule, match filter.InventoryMatch) filter.InventoryState {
if match != filter.InventoryMatchExact {
return filter.InventoryStateDrifted
}
if desired.Protected {
return filter.InventoryStateProtected
}
switch desired.Origin {
case filter.RuleOriginAdopted:
return filter.InventoryStateAdopted
default:
return filter.InventoryStateManaged
}
}
func firewallSyncReasonMessage(code firewallsync.ReasonCode) string {
switch code {
case firewallsync.ReasonAlreadyExists:
return "rule already exists in target backend"
case firewallsync.ReasonOnlyExistsInTarget:
return "rule exists only in target backend"
case firewallsync.ReasonManagedOnlyInTarget:
return "managed rule exists only in target backend"
case firewallsync.ReasonUnsafeRemoval:
return "managed runtime rule cannot be safely removed"
case firewallsync.ReasonReadOnlyRule:
return "read-only runtime rule is preserved but cannot be synchronized"
default:
return ""
}
}
func appendDatabaseSyncFailure(result *dto.FirewallRuleSyncResult, item dto.FirewallRuleSyncItem, err error) {
if err == nil {
err = errors.New("database synchronization failed")
}
result.Failed++
result.Errors = append(result.Errors, dto.FirewallRuleSyncFailure{
SourceUUID: item.SourceUUID,
Rule: item.Rule, ForwardRule: item.ForwardRule, DockerRule: item.DockerRule,
Error: err.Error(),
})
}
func firewallDeleteChange(current filter.ObservedRule, desired filter.DesiredRule) (filter.RuleChange, error) {
if err := filter.GuardMutation(current); err != nil {
return filter.RuleChange{}, err
}
before := current.Rule
if before.UUID == "" && strings.HasPrefix(current.Marker, "1panel-rule:") {
before.UUID = strings.TrimSpace(strings.TrimPrefix(current.Marker, "1panel-rule:"))
}
if before.UUID == "" {
before.UUID = desired.Rule.UUID
}
return filter.RuleChange{Operation: filter.ChangeDelete, Before: &before, Locator: &current.Locator, UnmarkedAdopted: current.Marker == "" && desired.Origin == filter.RuleOriginAdopted}, nil
}
func loadForwardingFirewallOverview(manager forwarding.Adapter) (dto.FirewallSubsystemStatus, error) {
ipv4, ipv4Err := loadForwardingFamilyInfo(manager, manager.Name(), constant.FirewallFamilyIPv4)
ipv6, ipv6Err := loadForwardingFamilyInfo(manager, manager.Name(), constant.FirewallFamilyIPv6)
if ipv6.Available {
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile)
var pathError *os.PathError
switch {
case errors.As(err, &pathError) && errors.Is(err, os.ErrNotExist) && pathError.Path == "/proc/net/if_inet6":
ipv6.Available, ipv6.Initialized, ipv6.Bound = false, false, false
case err != nil:
ipv6.Reason = "ipv6_ra_check_failed"
ipv6.Bound = false
case len(interfaces) > 0:
ipv6.Reason, ipv6.RAInterfaces = "ipv6_ra_required", interfaces
ipv6.Bound = false
case !ipv6.Bound:
ipv6.Reason = "ipv6_forwarding_not_enabled"
}
}
return dto.FirewallSubsystemStatus{IsInit: ipv4.Initialized || ipv6.Initialized, IsBind: ipv4.Bound || ipv6.Bound, IPv4: ipv4, IPv6: ipv6}, errors.Join(ipv4Err, ipv6Err)
}
func loadForwardingFamilyInfo(manager forwarding.Adapter, backend, family string) (dto.FirewallBackendFamilyStatus, error) {
initialized, bound, err := manager.FamilyStatus(family)
available := err == nil
if backend == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
commands, commandErr := lifecycle.ResolveIptablesCommands()
available = available && commandErr == nil && commands.IPv6Available()
}
return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound}, err
}
func (s *ForwardingService) forwardingEnabled() (bool, error) {
if s.enabled != nil {
return s.enabled()
}
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
return status == constant.StatusEnable, err
}
func (s *ForwardingService) initializeForwarding(manager forwarding.Adapter) error {
if err := s.saveForwardingBackend(manager.Name()); err != nil {
return err
}
return manager.Enable()
}
func (s *ForwardingService) saveForwardingBackend(backend string) error {
if s.persistBackend != nil {
return s.persistBackend(backend)
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
}
func (s *ForwardingService) persistForwardingEnabled() error {
if s.markEnabled != nil {
return s.markEnabled()
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
}
func recordForwardingSyncError(err error) {
forwardingSyncStateMu.Lock()
forwardingLastSyncErr = err
forwardingSyncStateMu.Unlock()
}
func forwardingRulesFromModels(stored []model.ForwardingRule) []forwarding.Rule {
rules := make([]forwarding.Rule, 0, len(stored))
for _, rule := range stored {
rules = append(rules, forwarding.Rule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
})
}
return rules
}
func newForwardingService() *ForwardingService {
return &ForwardingService{
clientFactory: newForwardingAdapter,
rules: forwardingRuleRepo,
markEnabled: func() error {
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
},
persistBackend: func(backend string) error {
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
},
}
}
func newForwardingAdapter() (forwarding.Adapter, error) {
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
selected = strings.TrimSpace(selected)
if selected == "" {
selected = constant.FirewallProviderIptables
}
return newForwardingAdapterFor(selected)
}
func newForwardingAdapterFor(backend string) (forwarding.Adapter, error) {
client, err := lifecycle.NewClient(backend)
if err != nil {
return nil, fmt.Errorf(
"%w: selected forwarding backend %s: %w",
errForwardingBackendUnavailable, backend, err,
)
}
switch client.Name() {
case constant.FirewallProviderIptables:
return forwarding.NewIptables(client.Name()), nil
case constant.FirewallProviderNftables:
return forwarding.NewNftables(), nil
default:
return nil, errForwardingBackendUnavailable
}
}
func ReconcileDockerPortGuard(ctx context.Context) error {
return NewIDockerPortGuardService().Reconcile(ctx)
}
func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) error {
persistedEnabled, err := dockerPortGuardPersistedEnabled()
if err != nil {
return fmt.Errorf("load Docker port guard persisted status: %w", err)
}
backends := []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables}
if s.runtime != nil {
backends = []string{selectedDockerFirewallBackend(constant.FirewallProviderIptables)}
}
initializedByBackend := make(map[string]bool, len(backends))
initialized := false
for _, backend := range backends {
initialized, err = s.guardRuntime(backend).Initialized(dockerfirewall.FamilyIPv4)
if err != nil {
return &dockerfirewall.FamilyError{Family: dockerfirewall.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)}
}
initializedByBackend[backend] = initialized
if initialized {
break
}
}
if !initialized && !persistedEnabled {
return nil
}
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
initialized, inspected := initializedByBackend[backend]
if !inspected {
initialized, err = runtime.Initialized(dockerfirewall.FamilyIPv4)
if err != nil {
return &dockerfirewall.FamilyError{Family: dockerfirewall.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)}
}
}
if !initialized && !persistedEnabled {
return nil
}
policies, err := s.runtimePolicies(ctx)
if err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
if !initialized {
err = runtime.Initialize(policies, inventory)
} else {
err = runtime.ReplacePolicies(policies, inventory)
}
if err != nil {
return err
}
return verifyDockerFirewall(runtime, policies, inventory.ReadOnly)
}
func (s *DockerPortGuardService) runtimeForDocker(ctx context.Context) (dockerfirewall.Runtime, string, error) {
if s.runtime != nil {
return s.runtime, selectedDockerFirewallBackend(constant.FirewallProviderIptables), nil
}
cli, err := s.client()
if err != nil {
return nil, "", buserr.WithDetail("ErrDockerFailed", err.Error(), err)
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return nil, "", buserr.WithDetail("ErrDockerFailed", err.Error(), err)
}
backend := selectedDockerFirewallBackend(dockerFirewallBackend(info))
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
return nil, backend, fmt.Errorf("Docker firewall backend %q is not supported", backend)
}
return s.guardRuntime(backend), backend, nil
}
func (s *DockerPortGuardService) guardRuntime(backend string) dockerfirewall.Runtime {
if s.runtimeForBackend != nil {
return s.runtimeForBackend(backend)
}
if s.runtime != nil {
return s.runtime
}
return newDockerFirewallRuntime(backend)
}
func newDockerFirewallRuntime(backend string) dockerfirewall.Runtime {
if backend == constant.FirewallProviderNftables {
return dockerfirewall.NewNftables()
}
return dockerfirewall.NewIptables()
}
func selectedDockerFirewallBackend(fallback string) string {
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
selected = strings.ToLower(strings.TrimSpace(selected))
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected
}
fallback = strings.ToLower(strings.TrimSpace(fallback))
if fallback == constant.FirewallProviderNftables {
return fallback
}
return constant.FirewallProviderIptables
}
func dockerFirewallBackend(info system.Info) string {
if info.FirewallBackend == nil || info.FirewallBackend.Driver == "" {
return constant.FirewallProviderIptables
}
return strings.ToLower(info.FirewallBackend.Driver)
}
func (s *DockerPortGuardService) runtimePolicies(ctx context.Context) ([]dockerfirewall.Policy, error) {
stored, err := s.policies.ListManaged(ctx)
if err != nil {
return nil, err
}
policies := make([]dockerfirewall.Policy, 0, len(stored))
for _, policy := range stored {
sources := []string{}
_ = json.Unmarshal([]byte(policy.Sources), &sources)
policies = append(policies, dockerfirewall.Policy{UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources})
}
return policies, nil
}
func dockerGuardReadOnlyPolicyUUID(policy dockerfirewall.ReadOnlyPolicy) string {
nativeRules, _ := json.Marshal(policy.NativeRules)
fingerprint := strings.Join([]string{
policy.Policy.Family, policy.Policy.HostIP, strconv.Itoa(int(policy.Policy.HostPort)),
policy.Policy.Protocol, policy.Action, string(nativeRules),
}, "\x00")
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(fingerprint)).String()
}
func dockerPortGuardPersistedEnabled() (bool, error) {
status, err := settingRepo.GetValueByKey(constant.FirewallDockerPortGuardStatusKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return status == constant.StatusEnable, err
}
func verifyDockerFirewall(runtime dockerfirewall.Runtime, desired []dockerfirewall.Policy, preserved []dockerfirewall.ReadOnlyPolicy) error {
inventory, err := runtime.ListPolicies()
if err != nil {
return fmt.Errorf("verify synchronized Docker firewall policies: %w", err)
}
if !dockerFirewallPoliciesEqual(inventory.Policies, desired) {
return fmt.Errorf("verify synchronized Docker firewall policies: target policies do not match the database")
}
if !readOnlyStatesEqual(inventory.ReadOnly, preserved) {
return fmt.Errorf("verify synchronized Docker firewall policies: read-only runtime rules changed")
}
return nil
}
func dockerFirewallPoliciesEqual(left, right []dockerfirewall.Policy) bool {
if len(left) != len(right) {
return false
}
counts := make(map[string]int, len(left))
for _, policy := range left {
counts[dockerFirewallPolicyKey(policy)]++
}
for _, policy := range right {
key := dockerFirewallPolicyKey(policy)
if counts[key] == 0 {
return false
}
counts[key]--
}
return true
}
func dockerFirewallPolicyKey(policy dockerfirewall.Policy) string {
mode := policy.Mode
if mode == dockerfirewall.ModeAllow && len(policy.Sources) == 0 {
mode = dockerfirewall.ModeAll
}
sources := make([]string, 0, len(policy.Sources))
for _, source := range policy.Sources {
source = strings.TrimSpace(source)
if prefix, err := netip.ParsePrefix(source); err == nil {
source = prefix.Masked().String()
} else if address, err := netip.ParseAddr(source); err == nil {
address = address.Unmap()
source = netip.PrefixFrom(address, address.BitLen()).String()
}
sources = append(sources, source)
}
sort.Strings(sources)
host := policy.HostIP
if address, err := netip.ParseAddr(host); err == nil {
host = address.String()
}
return strings.Join([]string{
policy.UUID, policy.Family, host, strconv.Itoa(int(policy.HostPort)),
policy.Protocol, mode, strings.Join(sources, ","),
}, "\x00")
}
func readOnlyStatesEqual(left, right []dockerfirewall.ReadOnlyPolicy) bool {
if len(left) != len(right) {
return false
}
leftRules := flattenNativeRules(left)
rightRules := flattenNativeRules(right)
if len(leftRules) != len(rightRules) {
return false
}
for index := range leftRules {
if leftRules[index].Family != rightRules[index].Family || !slices.Equal(leftRules[index].Tokens, rightRules[index].Tokens) {
return false
}
}
return true
}
func flattenNativeRules(policies []dockerfirewall.ReadOnlyPolicy) []dockerfirewall.NativeRule {
rules := make([]dockerfirewall.NativeRule, 0)
for _, policy := range policies {
rules = append(rules, policy.NativeRules...)
}
slices.SortStableFunc(rules, func(left, right dockerfirewall.NativeRule) int {
if left.Family < right.Family {
return -1
}
if left.Family > right.Family {
return 1
}
if left.Order < right.Order {
return -1
}
if left.Order > right.Order {
return 1
}
return 0
})
return rules
}
func newDockerPortGuardService() *DockerPortGuardService {
return &DockerPortGuardService{
policies: repo.NewIDockerPortGuardRepo(),
client: docker.NewDockerClient,
version: dockerFirewallVersion,
}
}
func dockerFirewallVersion(backend string) string {
client, err := lifecycle.NewClient(backend)
if err != nil {
return "-"
}
version, err := client.Version()
if err != nil || strings.TrimSpace(version) == "" {
return "-"
}
return version
}
func firewallDockerActive() (bool, error) {
if !cmd.Which("docker") {
return false, nil
}
return controller.CheckActive("docker")
}
func restoreFirewalldDependents(ctx context.Context, reason string, restoreDocker bool, restoreForwarding func(context.Context) error, restoreDockerGuard func(context.Context) error) error {
var errs []error
if err := restoreForwarding(ctx); err != nil {
errs = append(errs, fmt.Errorf("restore port forwarding %s: %w", reason, err))
}
if restoreDocker {
if err := restoreDockerGuard(ctx); err != nil {
errs = append(errs, fmt.Errorf("restore Docker port guard %s: %w", reason, err))
}
}
return errors.Join(errs...)
}
func operateFirewallLifecycle(client lifecycle.Client, operation string, withDockerRestart bool, prepareStart func(lifecycle.Client) error, t *task.Task) error {
run := func(operation, name string, action func() error) error {
if t != nil {
return runFirewallLifecycleAction(t, task.GetTaskName(name, operation, ""), action)
}
return action()
}
switch operation {
case string(lifecycle.OperationStart):
if err := run("Start", client.Name(), client.Start); err != nil {
return err
}
case string(lifecycle.OperationRestart):
if err := run("TaskRestart", client.Name(), client.Restart); err != nil {
return err
}
case string(lifecycle.OperationStop):
return stopFirewallLifecycle(client, withDockerRestart, nil, t)
default:
return fmt.Errorf("not supported operation: %s", operation)
}
var recoveryErrors []error
if prepareStart != nil {
err := prepareStart(client)
if err == nil && client.Name() == constant.FirewallProviderFirewalld {
err = lifecycleproviders.RemoveFirewalldSSHService()
}
if err != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after %s: %w", operation, err))
}
}
if withDockerRestart {
if err := run("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil {
recoveryErrors = append(recoveryErrors, &firewallDockerRestartError{Err: err})
}
}
if client.Name() == constant.FirewallProviderFirewalld && operation == string(lifecycle.OperationStart) {
if err := run("TaskRecover", "Fail2Ban", restoreFail2BanAfterFirewallStart); err != nil {
recoveryErrors = append(recoveryErrors, err)
}
}
if err := errors.Join(recoveryErrors...); err != nil {
return &firewallCompletedOperationError{Operation: operation, Err: err}
}
return nil
}
func (c firewallLifecycleClient) Start() error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
return c.Client.Start()
}
func (c firewallLifecycleClient) Restart() error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
return c.Client.Restart()
}
func runFirewallLifecycleAction(t *task.Task, name string, action func() error) error {
t.Log(i18n.GetWithName("TaskStart", name))
started := time.Now()
err := t.TaskCtx.Err()
if err == nil {
err = action()
}
t.LogWithStatus(fmt.Sprintf("%s (%.2fs)", name, time.Since(started).Seconds()), err)
return err
}
func stopFirewallLifecycle(client lifecycle.Client, withDockerRestart bool, prepareStop func() error, t *task.Task) error {
if client.Name() == constant.FirewallProviderFirewalld {
if err := rememberFail2BanBeforeFirewallStop(); err != nil {
return err
}
}
if prepareStop != nil {
if err := prepareStop(); err != nil {
return err
}
}
stop := client.Stop
if t != nil {
stop = func() error {
return runFirewallLifecycleAction(t, task.GetTaskName(client.Name(), "Stop", ""), client.Stop)
}
}
if err := stop(); err != nil {
return err
}
if withDockerRestart {
restart := func() error { return controller.HandleRestart("docker") }
var err error
if t != nil {
err = runFirewallLifecycleAction(t, task.GetTaskName("Docker", "TaskRestart", ""), restart)
} else {
err = restart()
}
if err != nil {
return &firewallDockerRestartError{Err: err}
}
}
return nil
}
func (c firewallLifecycleClient) Stop() error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
return c.Client.Stop()
}
func rememberFail2BanBeforeFirewallStop() error {
exists, err := controller.CheckExist("fail2ban.service")
if err != nil {
global.LOG.Warnf("check fail2ban.service installation before stopping the firewall failed: %v", err)
}
if !exists {
return nil
}
active, err := controller.CheckActive("fail2ban.service")
if err != nil {
global.LOG.Warnf("check fail2ban.service status before stopping the firewall failed: %v", err)
}
if !active {
return nil
}
if err := os.WriteFile(fail2BanRestoreWithFirewallMarker, nil, 0600); err != nil {
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
}
return nil
}
func restoreFail2BanAfterFirewallStart() error {
if _, err := os.Stat(fail2BanRestoreWithFirewallMarker); err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
}
if err := controller.HandleStart("fail2ban.service"); err != nil {
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
}
if err := os.Remove(fail2BanRestoreWithFirewallMarker); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
}
return nil
}
func currentFirewallRuleSyncTaskLocked() (dto.FirewallRuleSyncTask, error) {
if firewallRuleSyncTaskID != "" {
return dto.FirewallRuleSyncTask{TaskID: firewallRuleSyncTaskID, Executing: true}, nil
}
if global.TaskDB == nil {
return dto.FirewallRuleSyncTask{}, nil
}
taskRepo := repo.NewITaskRepo()
record, err := taskRepo.GetFirst(
repo.WithByStatus(constant.StatusExecuting),
repo.WithByType(task.TaskScopeFirewall),
taskRepo.WithOperate(task.TaskSync),
)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return dto.FirewallRuleSyncTask{}, nil
}
return dto.FirewallRuleSyncTask{}, err
}
return dto.FirewallRuleSyncTask{TaskID: record.ID, Executing: true}, nil
}
func (s *FirewallService) operateFilterChainBase(provider string, request dto.FilterChainOperation) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
return s.operateFilterChainBaseLocked(provider, request)
}
func (s *FirewallService) operateFilterChainBaseLocked(provider string, request dto.FilterChainOperation) error {
if err := s.checkSelectedProvider(context.Background(), filter.Provider(provider)); err != nil {
return err
}
if provider != constant.FirewallProviderIptables && provider != constant.FirewallProviderNftables {
return fmt.Errorf("filter chain operations are not supported for %s", provider)
}
operation := firewall.BaseOperation(request.Operate)
var ports []firewall.PortWhitelist
if operation == firewall.BaseOperationInit || operation == firewall.BaseOperationBind {
var err error
ports, err = LoadRequiredFirewallPortWhiteList()
if err != nil {
return err
}
}
if provider == constant.FirewallProviderNftables {
if err := nftables_helper.Operate(operation, ports); err != nil {
return err
}
} else if err := iptables_helper.Operate(operation, ports); err != nil {
return err
}
status := constant.StatusEnable
if operation == firewall.BaseOperationUnbind {
status = constant.StatusDisable
}
return settingRepo.Update("IptablesStatus", status)
}
func LoadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return nil, err
}
return firewall.RequiredPortWhitelist(ports)
}
func lockFirewallLifecycleIdle() error {
if !firewallLifecycleTaskMu.TryLock() {
return buserr.New("TaskIsExecuting")
}
if firewallLifecycleTaskID != "" {
firewallLifecycleTaskMu.Unlock()
return buserr.New("TaskIsExecuting")
}
return nil
}
func cleanupInactiveSystemBackend(backend string) error {
switch backend {
case constant.FirewallProviderIptables:
return iptables_helper.Cleanup()
case constant.FirewallProviderNftables:
return nftables_helper.Cleanup()
default:
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
}
}
func cleanupSystemBackend(backend string) error {
switch backend {
case constant.FirewallProviderIptables:
if err := iptables_helper.Cleanup(); err != nil {
return err
}
case constant.FirewallProviderNftables:
if err := nftables_helper.Cleanup(); err != nil {
return err
}
default:
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
}
return settingRepo.Update("IptablesStatus", constant.StatusDisable)
}
func resetServiceFirewallBackend(provider string, withDockerRestart bool) error {
client, err := lifecycle.NewClient(provider)
if err != nil {
return err
}
return resetServiceFirewallClient(client, withDockerRestart, func(
client lifecycle.Client,
restartDocker bool,
prepareStop func() error,
) error {
return stopFirewallLifecycle(client, restartDocker, prepareStop, nil)
})
}
func resetServiceFirewallClient(client lifecycle.Client, withDockerRestart bool, stop func(lifecycle.Client, bool, func() error) error) error {
resetter, ok := client.(lifecycle.Resetter)
if !ok {
return fmt.Errorf("firewall provider %s does not support reset", client.Name())
}
if resetBeforeStop, ok := client.(lifecycle.PreStopResetter); ok {
if err := stop(client, withDockerRestart, resetBeforeStop.ResetBeforeStop); err != nil {
return err
}
return nil
}
return resetter.Reset()
}
func (s *FirewallService) desiredFirewallRulesByScope(ctx context.Context, stored []model.FirewallRule, runtime filter.Adapter) (map[string][]filter.DesiredRule, []filter.InventoryItem) {
provider := runtime.Provider()
desired := make(map[string][]filter.DesiredRule)
var failures []filter.InventoryItem
ports, protectionErr := loadFirewallPortWhiteList()
var required []firewall.PortWhitelist
if protectionErr == nil {
loadRequired := s.requiredPorts
if loadRequired != nil {
required, protectionErr = loadRequired()
} else {
required, protectionErr = firewall.RequiredPortWhitelist(ports)
}
}
whitelist := filter.NewPortWhitelistIndex(ports)
for _, record := range stored {
compiled, _, err := s.compileRestorableFirewallRules(ctx, record, runtime, required)
if err == nil {
err = protectionErr
}
if err != nil {
rule := filter.FirewallRule{
UUID: record.UUID,
Scope: filter.Scope{Provider: provider, Family: filter.Family(record.Family), Direction: filter.DirectionInput}.Normalize(),
Protocol: record.Protocol, SourceAddress: record.SourceAddress, SourcePort: record.SourcePort,
DestinationAddress: record.DestinationAddress, DestinationPort: record.DestinationPort,
Interface: record.Interface, ConnectionStates: strings.FieldsFunc(record.ConnectionStates, func(r rune) bool { return r == ',' }),
Action: filter.Action(record.Action), Description: record.Description, Priority: record.Priority,
}
failures = append(failures, filter.InventoryItem{
Incompatible: isFirewallPolicyIncompatible(err),
Rule: rule, State: filter.InventoryStateDrifted, Match: filter.InventoryMatchNone,
Desired: &filter.DesiredRule{UUID: record.UUID, Rule: rule, Origin: filter.RuleOrigin(record.Origin), Protected: protectionErr != nil || whitelist.Matches(rule)},
Error: fmt.Sprintf("policy %s: %v", record.UUID, err),
})
continue
}
for _, rule := range compiled {
rule.Protected = whitelist.Matches(rule.Rule)
rule.Expanded = len(compiled) > 1
key := rule.Rule.Scope.Key()
desired[key] = append(desired[key], rule)
}
}
return desired, failures
}
func firewallInventoryPositionRanges(provider filter.Provider, items []filter.InventoryItem) (ipv4, ipv6 filter.PositionRange) {
if provider == filter.ProviderFirewalld {
return filter.PositionRange{Min: -32768, Max: 32767}, filter.PositionRange{Min: -32768, Max: 32767}
}
for _, item := range items {
if item.Observed == nil || item.Observed.Locator.Position == nil {
continue
}
scope := item.Observed.Rule.Scope
if scope.Provider != provider || scope.Direction != filter.DirectionInput {
continue
}
if (provider == filter.ProviderIptables || provider == filter.ProviderNftables) &&
(scope.Table != "filter" || scope.Chain != filter.IptablesInputChain) {
continue
}
bounds := &ipv4
if scope.Family == filter.FamilyIPv6 {
bounds = &ipv6
} else if scope.Family != filter.FamilyIPv4 {
continue
}
position := *item.Observed.Locator.Position
if position < 1 {
continue
}
if bounds.Min == 0 || position < bounds.Min {
bounds.Min = position
}
bounds.Max = max(bounds.Max, position)
if provider != filter.ProviderUFW {
bounds.Min = 1
}
}
return
}
func (s *FirewallService) adoptRule(ctx context.Context, runtime filter.Adapter, snapshot filter.RuleSet, observed filter.ObservedRule, source dto.FirewallRuleCreateItem) error {
if (observed.Rule.Scope.Provider == filter.ProviderIptables || observed.Rule.Scope.Provider == filter.ProviderNftables) &&
(observed.Rule.Scope.Chain == filter.BasicBeforeChain || observed.Rule.Scope.Chain == filter.BasicAfterChain) {
return fmt.Errorf("%w: system preset chains cannot be adopted", filter.ErrUnsupportedScope)
}
if observed.Protected {
return filter.ErrProtectedRule
}
if observed.ParseStatus != filter.ParseStatusSupported ||
(observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged) {
return fmt.Errorf("%w: rule cannot be managed", filter.ErrRuleOperation)
}
rule, err := prepareFirewallBackendRule(ctx, runtime, observed.Rule)
if err != nil {
return err
}
if rule.Scope.Provider != filter.ProviderFirewalld && observed.Locator.Position != nil {
position := int64(*observed.Locator.Position)
rule.OrderIndex = &position
}
record, err := firewallRuleModelForCreate(rule, source, constant.FirewallRuleOriginAdopted)
if err != nil {
return err
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
if runtime.Provider() == filter.ProviderNftables {
duplicates := 0
for _, candidate := range snapshot.Rules {
if candidate.ParseStatus != filter.ParseStatusSupported {
continue
}
same, err := filter.SameRuleContent(candidate.Rule, rule)
if err != nil {
return err
}
if same {
duplicates++
if duplicates > 1 {
return buserr.WithDetail("ErrInvalidParams", "duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", nil)
}
}
}
}
identities, err := firewallRuleCollisions(stored, rule.Scope.Provider)
if err != nil {
return err
}
for _, existing := range stored {
marker := "1panel-rule:" + existing.UUID
if existing.UUID != "" && (observed.Marker == marker || strings.HasPrefix(observed.Marker, marker+"-")) {
return fmt.Errorf("%w: rule is already managed", filter.ErrRuleOperation)
}
}
if err := identities.CheckDuplicate(rule); err != nil {
if errors.Is(err, filter.ErrRuleOperation) {
return buserr.WithDetail("ErrInvalidParams", "duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", nil)
}
return err
}
record.UUID = uuid.NewString()
rule.UUID = record.UUID
if runtime.Provider() != filter.ProviderNftables {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
if filter.RuleMatchesPortWhitelist(rule, ports) {
return filter.ErrProtectedRule
}
before := observed.Rule
before.UUID = record.UUID
scope := filter.RuleSet{Scope: rule.Scope}
remove, err := runtime.BuildCommands(scope, []filter.RuleChange{{
Operation: filter.ChangeDelete, Before: &before, CommandOnly: true,
UnmarkedAdopted: observed.Marker == "", PreviousMarker: observed.Marker,
}})
if err != nil {
return err
}
create, err := runtime.BuildCommands(scope, []filter.RuleChange{{
Operation: filter.ChangeCreate, After: &rule, CommandOnly: true, Append: rule.OrderIndex == nil,
}})
if err != nil {
return err
}
remove.CommandOnly, create.CommandOnly = true, true
if err := runtime.RunCommands(ctx, remove); err != nil {
return err
}
record.Priority = rule.Priority
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
err = s.saveFirewallRule(saveCtx, &record)
cancel()
if err != nil {
return err
}
runErr := runtime.RunCommands(ctx, create)
if err := errors.Join(runErr, persistFirewallRules(ctx, runtime, create)); err != nil {
return buserr.WithDetail("ErrFirewallRuleSavedApplyFailed", err.Error(), err)
}
return nil
}
plan, verification, err := applyFirewallChanges(runtime, ctx, snapshot, []filter.RuleChange{{
Operation: filter.ChangeAdopt, After: &rule, Locator: &observed.Locator, PreviousMarker: observed.Marker,
}})
if err != nil {
return err
}
if !verification.Matched {
return filter.ErrVerificationFailed
}
if _, err := filter.FindCommittedObserved(verification.RuleSet, rule, plan); err != nil {
return rollbackFirewallPlan(ctx, runtime, plan, err)
}
return s.saveFirewallRule(ctx, &record)
}
func (index firewallRuleCollisionIndex) CheckDuplicate(rule filter.FirewallRule) error {
key, err := filter.RuleMatchKey(rule)
if err != nil {
return err
}
for _, action := range index[key] {
if action == rule.Action {
return checkCollisionActions(rule.Action, action)
}
}
return nil
}
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
return samePortWhitelistRule(rule, target)
})
if index < 0 {
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
}
return index, nil
}
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
if reflect.DeepEqual(left, right) {
return true
}
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
if err != nil {
return false
}
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
if err != nil {
return false
}
slices.Sort(normalizedLeft[0].Sources)
slices.Sort(normalizedRight[0].Sources)
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
}
func loadSSHWhitelistPortFrom(path string) (string, error) {
directives, _, err := parseSSHConfigTree(path)
if errors.Is(err, os.ErrNotExist) {
return defaultSSHPort, nil
}
if err != nil {
return "", err
}
return loadSSHPortValues(directives)[0], nil
}
func newFirewallService() *FirewallService {
return &FirewallService{
rules: repo.NewIFirewallRuleRepo(),
adapters: nil,
forwardingSync: newForwardingService(),
dockerSync: newDockerPortGuardService(),
selectedProvider: firewallRuleSelectedProvider,
requiredPorts: LoadRequiredFirewallPortWhiteList,
cleanupBackend: cleanupSystemBackend,
cleanupInactiveBackend: cleanupInactiveSystemBackend,
resetBackend: resetServiceFirewallBackend,
dockerActive: firewallDockerActive,
restoreForwarding: func(ctx context.Context) error {
return newForwardingService().Restore(ctx)
},
restoreDockerGuard: ReconcileDockerPortGuard,
baseClient: NewSelectedSystemFirewallClient,
}
}
func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) {
client, err := NewSelectedSystemFirewallClient()
if err != nil {
return "", fmt.Errorf("%w: %v", filter.ErrProviderUnavailable, err)
}
return filter.Provider(client.Name()), nil
}
func (s *ForwardingService) loadRuleSyncCandidates(ctx context.Context, targetProvider filter.Provider) (forwarding.Adapter, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) {
target, err := s.clientFactory()
if err != nil {
return nil, nil, nil, false, err
}
if target.Name() != string(targetProvider) {
return nil, nil, nil, false, fmt.Errorf(
"%w: selected forwarding backend is %s, requested target is %s",
filter.ErrProviderUnavailable, target.Name(), targetProvider,
)
}
stored, err := s.rules.List(ctx)
if err != nil {
return nil, nil, nil, false, err
}
candidates := make([]forwardingRuleSyncCandidate, 0, len(stored))
for _, record := range stored {
rule := forwarding.Rule{
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
TargetPort: record.TargetPort, Interface: record.Interface,
}
normalized, normalizeErr := forwarding.NormalizeRule(rule)
candidates = append(candidates, forwardingRuleSyncCandidate{rule: normalized, err: normalizeErr})
}
initialized, _, err := target.InitStatus()
if err != nil {
return nil, nil, nil, false, err
}
targetRules := make([]forwarding.Rule, 0)
if initialized {
targetRules, err = target.List()
if err != nil {
return nil, nil, nil, false, err
}
targetRules, err = normalizeForwardingRuntimeRules(targetRules)
if err != nil {
return nil, nil, nil, false, err
}
}
return target, candidates, targetRules, initialized, nil
}
func normalizeForwardingRuntimeRules(rules []forwarding.Rule) ([]forwarding.Rule, error) {
normalized := make([]forwarding.Rule, 0, len(rules))
for _, rule := range rules {
item, err := forwarding.NormalizeRule(rule)
if err != nil {
return nil, fmt.Errorf("normalize target forwarding rule %s: %w", rule.Identity(), err)
}
normalized = append(normalized, item)
}
return normalized, nil
}
func databaseRuleSyncTarget(request dto.FirewallRuleSyncRequest, subsystem string) (filter.Provider, error) {
if request.SourceProvider != "" {
return "", fmt.Errorf("%w: %s synchronization reads rules from the database and does not accept a source provider", filter.ErrInvalidRule, subsystem)
}
if request.ResetSource {
return "", fmt.Errorf("%w: %s synchronization does not have a source firewall to reset", filter.ErrInvalidRule, subsystem)
}
if request.TargetProvider != filter.ProviderIptables && request.TargetProvider != filter.ProviderNftables {
return "", fmt.Errorf("%w: %s synchronization only supports iptables and nftables targets", filter.ErrInvalidRule, subsystem)
}
return request.TargetProvider, nil
}
func forwardingSyncPreview(target filter.Provider, candidates []forwardingRuleSyncCandidate, actual []forwarding.Rule) dto.FirewallRuleSyncPreview {
desired := make([]firewallSyncDesired[forwarding.Rule], 0, len(candidates))
for _, candidate := range candidates {
desired = append(desired, firewallSyncDesired[forwarding.Rule]{
Value: candidate.rule,
Payload: dto.FirewallRuleSyncItem{
SourceUUID: candidate.rule.Identity(), ForwardRule: &dto.ForwardRule{Family: candidate.rule.Family, Protocol: candidate.rule.Protocol, Port: candidate.rule.Port, TargetIP: candidate.rule.TargetIP, TargetPort: candidate.rule.TargetPort, Interface: candidate.rule.Interface},
},
Err: candidate.err,
})
}
return firewallDiffPreview(
"forwarding", target, desired, actual,
func(rule forwarding.Rule) string { return rule.Identity() },
func(rule forwarding.Rule) dto.FirewallRuleSyncItem {
return dto.FirewallRuleSyncItem{SourceUUID: rule.Identity(), ForwardRule: &dto.ForwardRule{Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface}}
},
)
}
func firewallDiffPreview[T any](subsystem string, target filter.Provider, desired []firewallSyncDesired[T], actual []T, key func(T) string, actualItem func(T) dto.FirewallRuleSyncItem) dto.FirewallRuleSyncPreview {
preview := dto.FirewallRuleSyncPreview{Subsystem: subsystem, TargetProvider: target, Items: make([]dto.FirewallRuleSyncItem, 0, len(desired)+len(actual))}
actualByKey := make(map[string][]int, len(actual))
for index, value := range actual {
actualByKey[key(value)] = append(actualByKey[key(value)], index)
}
matched := make([]bool, len(actual))
for _, candidate := range desired {
item := candidate.Payload
item.Status, item.ReasonCode, item.Reason = "", "", ""
switch {
case candidate.Err != nil:
item.Status, item.ReasonCode, item.Reason = firewallsync.StatusBlocked, firewallsync.ReasonInvalidPolicy, candidate.Err.Error()
default:
match := -1
for _, index := range actualByKey[key(candidate.Value)] {
if !matched[index] {
match = index
break
}
}
if match >= 0 {
matched[match] = true
item.Status, item.ReasonCode = firewallsync.StatusExisting, firewallsync.ReasonAlreadyExists
item.Reason = firewallSyncReasonMessage(item.ReasonCode)
} else {
item.Status = firewallsync.StatusReady
}
}
preview.Add(item)
}
for index, value := range actual {
if matched[index] {
continue
}
item := actualItem(value)
item.Status, item.ReasonCode = firewallsync.StatusRemove, firewallsync.ReasonOnlyExistsInTarget
item.Reason = firewallSyncReasonMessage(item.ReasonCode)
preview.Add(item)
}
return preview
}
func firewallSyncResult(preview dto.FirewallRuleSyncPreview, cause error, executed bool) dto.FirewallRuleSyncResult {
result := dto.FirewallRuleSyncResult{Subsystem: preview.Subsystem, TargetProvider: preview.TargetProvider}
for _, item := range preview.Items {
if item.Status != firewallsync.StatusRemove {
result.Total++
}
switch item.Status {
case firewallsync.StatusExisting:
result.Skipped++
case firewallsync.StatusBlocked:
appendDatabaseSyncFailure(&result, item, errors.New(item.Reason))
case firewallsync.StatusReady:
if executed {
if cause != nil {
appendDatabaseSyncFailure(&result, item, cause)
} else {
result.Succeeded++
}
}
case firewallsync.StatusRemove:
if executed && cause == nil {
result.Removed++
}
}
}
return result
}
func firewallRuleStatesEqual[T any](left, right []T, key func(T) string) bool {
if len(left) != len(right) {
return false
}
counts := make(map[string]int, len(left))
for _, value := range left {
counts[key(value)]++
}
for _, value := range right {
valueKey := key(value)
if counts[valueKey] == 0 {
return false
}
counts[valueKey]--
}
return true
}
func (s *DockerPortGuardService) loadRuleSyncCandidates(ctx context.Context, request dto.FirewallRuleSyncRequest) (string, []model.DockerPortGuardPolicy, dockerfirewall.Runtime, error) {
targetProvider, err := databaseRuleSyncTarget(request, "Docker")
if err != nil {
return "", nil, nil, err
}
target := string(targetProvider)
selected, err := s.selectedRuleSyncBackend(ctx)
if err != nil {
return "", nil, nil, err
}
if target != selected {
return "", nil, nil, fmt.Errorf(
"%w: selected Docker firewall backend is %s, requested target is %s",
filter.ErrProviderUnavailable, selected, target,
)
}
policies, err := s.policies.ListManaged(ctx)
if err != nil {
return "", nil, nil, err
}
return target, policies, s.guardRuntime(target), nil
}
func (s *DockerPortGuardService) selectedRuleSyncBackend(ctx context.Context) (string, error) {
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
selected = strings.ToLower(strings.TrimSpace(selected))
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected, nil
}
if s.client == nil {
return "", buserr.New("ErrDockerFailed")
}
cli, err := s.client()
if err != nil {
return "", buserr.WithDetail("ErrDockerFailed", err.Error(), err)
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return "", buserr.WithDetail("ErrDockerFailed", err.Error(), err)
}
return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil
}
func dockerSyncPreview(target filter.Provider, policies []model.DockerPortGuardPolicy, inventory dockerfirewall.PolicyInventory) dto.FirewallRuleSyncPreview {
desired := make([]firewallSyncDesired[dockerfirewall.Policy], 0, len(policies))
for _, policy := range policies {
sources := []string{}
_ = json.Unmarshal([]byte(policy.Sources), &sources)
desired = append(desired, firewallSyncDesired[dockerfirewall.Policy]{
Value: dockerfirewall.Policy{
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources,
},
Payload: dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources, Description: policy.Description,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
}},
})
}
preview := firewallDiffPreview(
"docker", target, desired, inventory.Policies, dockerFirewallPolicyKey,
func(policy dockerfirewall.Policy) dto.FirewallRuleSyncItem {
return dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...),
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
}}
},
)
for _, policy := range inventory.ReadOnly {
preview.Add(dto.FirewallRuleSyncItem{
SourceUUID: dockerGuardReadOnlyPolicyUUID(policy),
DockerRule: &dto.DockerPortGuardEndpoint{
Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort,
Protocol: policy.Policy.Protocol, PolicyUUID: dockerGuardReadOnlyPolicyUUID(policy), Sources: append([]string(nil), policy.Policy.Sources...),
NativeAction: policy.Action, ReadOnly: true, TrafficPath: dockerTrafficPathUnknown,
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
},
Status: firewallsync.StatusBlocked,
ReasonCode: firewallsync.ReasonReadOnlyRule,
Reason: firewallSyncReasonMessage(firewallsync.ReasonReadOnlyRule),
})
}
return preview
}
func discoverDockerEndpoints(ctx context.Context, cli *client.Client, all bool) ([]dto.DockerPortGuardEndpoint, error) {
containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: all})
if err != nil {
return nil, err
}
endpoints := make([]dto.DockerPortGuardEndpoint, 0)
for _, item := range containers {
name := ""
if len(item.Names) > 0 {
name = strings.TrimPrefix(item.Names[0], "/")
}
compose := item.Labels[dockerGuardComposeProjectLabel]
application := ""
if created, ok := item.Labels[dockerGuardComposeCreatedBy]; ok && created == "Apps" {
application = compose
}
for _, port := range item.Ports {
if port.PublicPort == 0 || (port.Type != "tcp" && port.Type != "udp") {
continue
}
family := dockerfirewall.FamilyIPv4
hostIP := port.IP
if addr, err := netip.ParseAddr(hostIP); err == nil && addr.Is6() {
family = dockerfirewall.FamilyIPv6
} else if hostIP == "" {
hostIP = "0.0.0.0"
}
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{Family: family, HostIP: hostIP, HostPort: port.PublicPort, Protocol: port.Type, ContainerID: item.ID, ContainerName: name, ContainerState: item.State, ContainerPort: port.PrivatePort, Compose: compose, Application: application, Sources: []string{}})
}
}
return endpoints, nil
}
func annotateDockerEndpointManagement(endpoints []dto.DockerPortGuardEndpoint, backend string) {
rules := map[string]dockerfirewall.DNATRules{
constant.FirewallFamilyIPv4: dockerfirewall.ReadDNATRules(backend, constant.FirewallFamilyIPv4),
constant.FirewallFamilyIPv6: dockerfirewall.ReadDNATRules(backend, constant.FirewallFamilyIPv6),
}
proxies := dockerfirewall.ReadProxyEndpoints()
inspections := make(map[string]dockerfirewall.EndpointInspection, len(rules))
for family, familyRules := range rules {
inspections[family] = dockerfirewall.InspectEndpoints(backend, family, familyRules, proxies)
}
for i := range endpoints {
endpoints[i].TrafficPath, endpoints[i].ManagementTarget, endpoints[i].ManagementReason =
dockerEndpointManagement(inspections[endpoints[i].Family], endpoints[i])
}
}
func dockerEndpointManagement(inspection dockerfirewall.EndpointInspection, endpoint dto.DockerPortGuardEndpoint) (string, string, string) {
if !inspection.DNATInspected {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATInspectFailed
}
dnatMatched := inspection.DNATMatches(endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)
if dnatMatched && inspection.IngressReachable {
return dockerTrafficPathForward, dockerManagementContainerGuard, ""
}
if !inspection.ProxyInspected {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonProxyInspectFailed
}
if inspection.ProxyMatches(endpoint.HostIP, endpoint.HostPort, endpoint.Protocol) {
return dockerTrafficPathInput, dockerManagementHostFirewall, ""
}
if dnatMatched {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATChainUnreachable
}
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNoMatchingPath
}
func groupDockerGuardContainers(endpoints []dto.DockerPortGuardEndpoint) []dto.DockerPortGuardContainer {
containers := make(map[string]*dto.DockerPortGuardContainer)
order := make([]string, 0)
for _, endpoint := range endpoints {
key := endpoint.ContainerID
if key == "" {
key = "__orphan__"
}
container, ok := containers[key]
if !ok {
container = &dto.DockerPortGuardContainer{
Key: key, Name: endpoint.ContainerName, Compose: endpoint.Compose,
Application: endpoint.Application, Endpoints: []dto.DockerPortGuardEndpoint{},
}
containers[key] = container
order = append(order, key)
}
container.Endpoints = append(container.Endpoints, endpoint)
}
sort.Slice(order, func(i, j int) bool {
return containers[order[i]].Name < containers[order[j]].Name
})
result := make([]dto.DockerPortGuardContainer, 0, len(order))
for _, key := range order {
container := containers[key]
items := make([]docker.PortRangeItem, 0, len(container.Endpoints))
for i, endpoint := range container.Endpoints {
sources := append([]string(nil), endpoint.Sources...)
sort.Strings(sources)
policyKey := fmt.Sprintf("%t|%s|%s|%t|%s|%s|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.Description, endpoint.ManagementTarget, endpoint.ManagementReason)
items = append(items, docker.PortRangeItem{
Key: endpoint.Family + "|" + endpoint.HostIP + "|" + endpoint.Protocol + "|" + policyKey,
PublicPort: endpoint.HostPort, PrivatePort: endpoint.ContainerPort,
HasPrivatePort: endpoint.ContainerPort != 0, Position: i,
})
}
container.PortGroups = make([]dto.DockerPortGuardPortGroup, 0, len(items))
for _, portRange := range docker.MergePortRanges(items) {
start := container.Endpoints[portRange.Start.Position]
address := start.HostIP
if strings.Contains(address, ":") {
address = "[" + address + "]"
}
ports := fmt.Sprintf("%d", portRange.Start.PublicPort)
if portRange.Start.PublicPort != portRange.End.PublicPort {
ports = fmt.Sprintf("%d-%d", portRange.Start.PublicPort, portRange.End.PublicPort)
}
container.PortGroups = append(container.PortGroups, dto.DockerPortGuardPortGroup{
Key: fmt.Sprintf("%s|%d-%d", portRange.Start.Key, portRange.Start.PublicPort, portRange.End.PublicPort),
Label: fmt.Sprintf("%s:%s/%s", address, ports, start.Protocol), Endpoint: start,
Endpoints: func() []dto.DockerPortGuardEndpoint {
members := make([]dto.DockerPortGuardEndpoint, 0, len(portRange.Items))
for _, item := range portRange.Items {
members = append(members, container.Endpoints[item.Position])
}
return members
}(),
})
}
result = append(result, *container)
}
return result
}
func normalizeDockerFirewallUUIDs(values []string) ([]string, error) {
uuids := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, policyUUID := range values {
policyUUID = strings.TrimSpace(policyUUID)
if policyUUID == "" {
return nil, buserr.WithDetail("ErrInvalidParams", "policy UUID cannot be empty", nil)
}
if _, exists := seen[policyUUID]; exists {
continue
}
seen[policyUUID] = struct{}{}
uuids = append(uuids, policyUUID)
}
if len(uuids) == 0 {
return nil, buserr.WithDetail("ErrInvalidParams", "policy UUIDs cannot be empty", nil)
}
return uuids, nil
}
func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
t.Logf("rules=%d", len(labels))
err := t.TaskCtx.Err()
if err == nil {
err = apply(t.TaskCtx)
}
succeeded, failed := 0, 0
for _, label := range labels {
if err != nil {
failed++
t.LogFailedWithErr(label, err)
} else {
succeeded++
t.LogSuccess(label)
}
}
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func normalizeDockerFirewallPolicy(policy dockerfirewall.Policy) (dockerfirewall.Policy, error) {
policy.Family = strings.ToLower(strings.TrimSpace(policy.Family))
policy.HostIP = strings.TrimSpace(policy.HostIP)
policy.Protocol = strings.ToLower(strings.TrimSpace(policy.Protocol))
policy.Mode = strings.ToLower(strings.TrimSpace(policy.Mode))
if policy.HostPort == 0 ||
(policy.Protocol != "tcp" && policy.Protocol != "udp") ||
(policy.Family != dockerfirewall.FamilyIPv4 && policy.Family != dockerfirewall.FamilyIPv6) ||
(policy.Mode != dockerfirewall.ModeAll && policy.Mode != dockerfirewall.ModeSources && policy.Mode != dockerfirewall.ModeAllow) {
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "invalid policy fields", nil)
}
address, err := netip.ParseAddr(policy.HostIP)
if err != nil || (policy.Family == dockerfirewall.FamilyIPv4) != address.Is4() {
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "host IP does not match address family", nil)
}
normalizedSources := make([]string, 0, len(policy.Sources))
seen := make(map[string]struct{}, len(policy.Sources))
for _, source := range policy.Sources {
source = strings.TrimSpace(source)
if source == "" {
continue
}
prefix, err := netip.ParsePrefix(source)
if err != nil {
if sourceAddress, addressErr := netip.ParseAddr(source); addressErr == nil {
bits := 128
if sourceAddress.Is4() {
bits = 32
}
prefix = netip.PrefixFrom(sourceAddress, bits)
} else {
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("invalid source address %q", source), nil)
}
}
if (policy.Family == dockerfirewall.FamilyIPv4) != prefix.Addr().Is4() {
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("source %q does not match address family", source), nil)
}
canonical := prefix.Masked().String()
if _, exists := seen[canonical]; !exists {
seen[canonical] = struct{}{}
normalizedSources = append(normalizedSources, canonical)
}
}
if policy.Mode != dockerfirewall.ModeAll && len(normalizedSources) == 0 {
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "source-based modes require at least one source", nil)
}
if policy.Mode == dockerfirewall.ModeAll {
normalizedSources = []string{}
}
sort.Strings(normalizedSources)
policy.Sources = normalizedSources
return policy, nil
}
func (i forwardingInventoryItem) SyncStatus() string {
switch {
case i.IsDesired && i.IsRuntime:
return forwardingSyncConverged
case i.IsDesired:
return forwardingSyncMissing
default:
return forwardingSyncRuntimeOnly
}
}
func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool {
if len(operations) == 0 {
return false
}
for _, operation := range operations {
if operation.Operation != string(forwarding.OperationRemove) {
return false
}
}
return true
}
func (e *firewallDockerRestartError) Error() string {
return fmt.Sprintf("failed to restart Docker: %v", e.Err)
}
func (e *firewallDockerRestartError) Unwrap() error {
return e.Err
}
func (e *firewallCompletedOperationError) Error() string {
return fmt.Sprintf("firewall %s completed with recovery errors: %v", e.Operation, e.Err)
}
func (e *firewallCompletedOperationError) Unwrap() error {
return e.Err
}
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
entries = slices.Clone(entries)
var sshPort string
for i := range entries {
entry := &entries[i]
if entry.Type == "" || entry.Port != "" {
continue
}
switch entry.Type {
case firewall.PortWhitelistTypePanel:
entry.Port = LoadPanelPort()
case firewall.PortWhitelistTypeSSH:
if sshPort == "" {
var err error
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
if err != nil {
return nil, err
}
}
entry.Port = sshPort
}
}
return firewall.ValidatePortWhitelist(entries)
}