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

1169 lines
40 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
"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/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
"gorm.io/gorm"
)
var (
firewallRuleSyncTaskMu sync.Mutex
firewallRuleSyncTaskID string
)
type firewallDatabaseSyncAdapter interface {
previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error)
syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error)
}
type firewallSyncRule struct {
dto.FirewallRuleSyncItem
desired filter.DesiredRule
observed *filter.ObservedRule
done bool
}
func firewallSyncSubsystem(value string) string {
if value = strings.TrimSpace(value); value == "" {
return "system"
}
return value
}
func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
switch firewallSyncSubsystem(request.Subsystem) {
case "forwarding":
return s.forwardingRuleSyncService().previewRuleSync(ctx, request)
case "docker":
return s.dockerRuleSyncService().previewRuleSync(ctx, request)
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
_, rules, _, err := s.loadFirewallSyncRules(ctx, request)
preview := dto.FirewallRuleSyncPreview{Subsystem: "system", TargetProvider: request.TargetProvider, Items: make([]dto.FirewallRuleSyncItem, 0, len(rules))}
for _, rule := range rules {
preview.Add(rule.FirewallRuleSyncItem)
}
return preview, err
}
func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (*filterruntime.Engine, []*firewallSyncRule, []filter.Snapshot, 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.adapters.Resolve(request.TargetProvider)
if err != nil {
return nil, nil, nil, err
}
stored, err := s.rules.List(ctx)
if err != nil {
return nil, nil, nil, err
}
model.SortFirewallRules(stored, request.TargetProvider)
rules := make([]*firewallSyncRule, 0, len(stored))
preservedMarkers := make(map[string]bool)
compileFailed := false
for _, record := range stored {
desired, preserved, err := s.compileRestorableFirewallRules(ctx, record, request.TargetProvider)
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
rules = append(rules, &firewallSyncRule{desired: native, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Rule: &rule}})
}
}
snapshots := make([]filter.Snapshot, 0)
for _, scope := range filter.ManagedInputScopes(request.TargetProvider) {
desired := make([]filter.DesiredRule, 0)
scoped := make([]*firewallSyncRule, 0)
byUUID := make(map[string]*firewallSyncRule)
for _, rule := range rules {
if rule.Rule != nil && rule.Rule.Scope.Key() == scope.Key() {
scoped = append(scoped, rule)
desired = append(desired, rule.desired)
byUUID[rule.desired.Rule.UUID] = rule
}
}
if compileFailed && len(desired) == 0 {
continue
}
snapshot, err := runtime.ObserveMutation(ctx, scope)
if errors.Is(err, filter.ErrFamilyUnavailable) {
for _, rule := range scoped {
rule.Status, rule.Reason = firewallsync.StatusBlocked, err.Error()
}
continue
}
if err != nil {
return nil, nil, nil, err
}
snapshots = append(snapshots, snapshot)
inventory, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
if err != nil {
return nil, nil, nil, err
}
matched := make(map[string]filter.InventoryItem)
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
rule := &firewallSyncRule{observed: observed, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{
SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &observed.Rule, Status: firewallsync.StatusRemove,
ReasonCode: firewallsync.ReasonManagedOnlyInTarget, Reason: firewallsync.ReasonMessage(firewallsync.ReasonManagedOnlyInTarget),
}}
if observed.Protected || observed.ParseStatus == filter.ParseStatusOpaque {
rule.Status, rule.ReasonCode = firewallsync.StatusBlocked, firewallsync.ReasonUnsafeRemoval
rule.Reason = firewallsync.ReasonMessage(rule.ReasonCode)
}
rules = append(rules, rule)
continue
}
rule := byUUID[item.Desired.Rule.UUID]
matched[item.Desired.Rule.UUID] = item
rule.observed = item.Observed
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)
}
}
ordered := make([]filter.InventoryItem, 0, len(scoped))
for _, rule := range scoped {
ordered = append(ordered, matched[rule.desired.Rule.UUID])
}
drifted := firewallsync.RuleOrder(snapshot, ordered)
for _, rule := range scoped {
if !drifted[rule.desired.Marker] {
continue
}
if rule.Status == firewallsync.StatusExisting {
rule.Status, rule.Reason = firewallsync.StatusReady, "managed rule order differs from database sequence"
}
}
}
return runtime, rules, snapshots, nil
}
func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.FirewallRuleSyncRequest, t *task.Task) (result dto.FirewallRuleSyncResult, err error) {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
result = dto.FirewallRuleSyncResult{Subsystem: "system", TargetProvider: request.TargetProvider}
if err := ctx.Err(); err != nil {
return result, err
}
runtime, rules, snapshots, err := s.loadFirewallSyncRules(ctx, request)
if err != nil {
return result, err
}
created, removed, unexecuted := 0, 0, 0
stopped := make(map[string]error)
failedRemovals := make(map[string]error)
var firewalldFinalSnapshot *filter.Snapshot
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)
}
}
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}))
}
}()
blocked := false
for _, rule := range rules {
if rule.Status != firewallsync.StatusRemove {
result.Total++
}
if rule.Status == firewallsync.StatusBlocked {
blocked = true
record("TaskSync", rule, errors.New(rule.Reason), false)
}
}
if blocked {
for _, rule := range rules {
if !rule.done {
record("TaskSync", rule, filter.ErrRuleOperation, true)
}
}
return result, nil
}
for _, rule := range rules {
if rule.Status == firewallsync.StatusExisting {
record("TaskSync", rule, nil, false)
}
}
for _, operation := range []filter.ChangeOperation{filter.ChangeDelete, filter.ChangeCreate} {
name := "TaskCreate"
if operation == filter.ChangeDelete {
name = "TaskDelete"
}
for _, initial := range snapshots {
scope := initial.Scope
queue := make([]*firewallSyncRule, 0)
for _, rule := range rules {
if rule.done || rule.Rule.Scope.Key() != scope.Key() {
continue
}
if operation == filter.ChangeDelete && rule.observed != nil || operation == filter.ChangeCreate && rule.Status == firewallsync.StatusReady {
queue = append(queue, rule)
}
}
if operation == filter.ChangeDelete {
sort.SliceStable(queue, func(i, j int) bool {
return syncObservedPosition(queue[i].observed) > syncObservedPosition(queue[j].observed)
})
}
if scope.Provider == filter.ProviderFirewalld && operation == filter.ChangeCreate && len(failedRemovals) == 0 && stopped[scope.Key()] == nil {
firewalldFinalSnapshot = syncFirewalldCreates(ctx, runtime, initial, removed > 0, queue, t, record)
continue
}
for start := 0; start < len(queue); {
rule := queue[start]
cause := stopped[scope.Key()]
if operation == filter.ChangeCreate && cause == nil {
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)
start++
continue
}
snapshot := initial
err := ctx.Err()
if err == nil {
snapshot, err = runtime.ObserveMutation(ctx, scope)
}
unreadable := err != nil
end := start + 1
if supportsNativeRuleBatch(scope.Provider) && (operation == filter.ChangeDelete || len(snapshot.Rules) == 0) && len(failedRemovals) == 0 {
end = min(start+filter.MaxAtomicExpansion, len(queue))
}
batch := queue[start:end]
changes := make([]filter.DesiredChange, 0, len(batch))
for _, entry := range batch {
if err != nil {
break
}
if operation == filter.ChangeDelete {
var change filter.DesiredChange
change, err = firewallsync.DeleteChange(snapshot, *entry.observed, entry.desired)
changes = append(changes, change)
} else {
after := *entry.Rule
after.OrderIndex = nil
if len(batch) == 1 && scope.Provider != filter.ProviderFirewalld {
markers := make([]string, 0)
for _, candidate := range rules {
if candidate.Rule != nil && candidate.Rule.Scope.Key() == scope.Key() && candidate.desired.Marker != "" {
markers = append(markers, candidate.desired.Marker)
}
}
after.OrderIndex = firewallsync.InsertionPosition(snapshot, markers, entry.desired.Marker)
}
changes = append(changes, filter.DesiredChange{Operation: operation, After: &after, Append: scope.Provider == filter.ProviderUFW && after.OrderIndex == nil})
}
}
if err == nil {
_, err = runtime.ExecuteSync(ctx, snapshot, changes)
}
for _, entry := range batch {
record(name, entry, err, false)
}
if err != nil && (operation == filter.ChangeDelete || scope.Provider != filter.ProviderFirewalld || unreadable || firewallCreateUnavailable(err)) {
stopped[scope.Key()] = err
}
start = end
}
}
}
if firewalldFinalSnapshot != nil && result.Failed == 0 && created > 0 {
verifyErr := verifyFirewalldSyncSnapshot(*firewalldFinalSnapshot, rules)
if t != nil {
t.LogWithStatus(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), verifyErr)
}
if verifyErr != nil {
return result, verifyErr
}
}
return result, nil
}
func syncFirewalldCreates(
ctx context.Context,
runtime *filterruntime.Engine,
initial filter.Snapshot,
refresh bool,
queue []*firewallSyncRule,
t *task.Task,
record func(string, *firewallSyncRule, error, bool),
) *filter.Snapshot {
if len(queue) == 0 {
return nil
}
snapshot := initial
snapshot.Rules = append([]filter.ObservedRule(nil), initial.Rules...)
indices := make(map[string]int)
indexRules := func() {
clear(indices)
for index, rule := range snapshot.Rules {
indices[rule.Locator.Canonical] = index
}
}
indexRules()
pending := make([]*firewallSyncRule, 0, len(queue))
var readErr error
for index, entry := range queue {
if refresh {
snapshot, readErr = runtime.ObserveMutation(ctx, initial.Scope)
if readErr != nil {
record(task.TaskCreate, entry, readErr, false)
for _, remaining := range queue[index+1:] {
record(task.TaskCreate, remaining, readErr, true)
}
break
}
indexRules()
refresh = false
}
if t != nil {
t.Logf("[%d/%d] %s %s", index+1, len(queue), i18n.GetMsgByKey(task.TaskCreate), entry.SourceUUID)
}
after := *entry.Rule
after.OrderIndex = nil
applied, err := runtime.ExecuteSync(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &after}})
if err == nil && len(applied.Applied) != 1 {
err = filter.ErrVerificationFailed
}
if err == nil {
observed := applied.Applied[0]
if position, exists := indices[observed.Locator.Canonical]; exists {
snapshot.Rules[position] = observed
} else {
indices[observed.Locator.Canonical] = len(snapshot.Rules)
snapshot.Rules = append(snapshot.Rules, observed)
}
snapshot, err = filter.NewSnapshot(snapshot.Scope, snapshot.Rules)
}
if err != nil {
record(task.TaskCreate, entry, err, false)
if firewallCreateUnavailable(err) {
for _, remaining := range queue[index+1:] {
record(task.TaskCreate, remaining, err, true)
}
break
}
refresh = true
continue
}
pending = append(pending, entry)
}
var actual filter.Snapshot
if readErr == nil {
actual, readErr = runtime.ObserveMutation(ctx, initial.Scope)
}
if readErr != nil {
for _, entry := range pending {
record(task.TaskCreate, entry, readErr, false)
}
return nil
}
states := firewalldRuleStates(actual)
for _, entry := range pending {
key, err := filter.RuleKey(*entry.Rule)
if err == nil && states[key] != 1 {
err = filter.ErrVerificationFailed
}
record(task.TaskCreate, entry, err, false)
}
return &actual
}
func firewalldRuleStates(snapshot filter.Snapshot) map[string]int {
states := make(map[string]int, len(snapshot.Rules))
for _, observed := range snapshot.Rules {
if observed.ParseStatus != filter.ParseStatusSupported || observed.Persistence != filter.PersistenceStatusConverged {
continue
}
if key, err := filter.RuleKey(observed.Rule); err == nil {
states[key]++
}
}
return states
}
func verifyFirewalldSyncSnapshot(snapshot filter.Snapshot, rules []*firewallSyncRule) error {
states := firewalldRuleStates(snapshot)
for _, entry := range rules {
if entry.Status == firewallsync.StatusRemove {
continue
}
if entry.Rule == nil {
return filter.ErrVerificationFailed
}
key, err := filter.RuleKey(*entry.Rule)
if err != nil {
return err
}
if states[key] != 1 {
return fmt.Errorf("%w: %s", filter.ErrVerificationFailed, entry.SourceUUID)
}
}
return nil
}
func syncObservedPosition(rule *filter.ObservedRule) int {
if rule.Locator.Position != nil {
return *rule.Locator.Position
}
return 0
}
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 (s *FirewallService) SyncRules(
ctx context.Context,
clientIP string,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncResult, error) {
switch firewallSyncSubsystem(request.Subsystem) {
case "forwarding":
return s.forwardingRuleSyncService().syncRules(ctx, request)
case "docker":
return s.dockerRuleSyncService().syncRules(ctx, request)
default:
return s.syncSystemRules(ctx, clientIP, request)
}
}
func (s *FirewallService) syncSystemRules(
ctx context.Context,
clientIP string,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncResult, error) {
if err := lockFirewallLifecycleIdle(); err != nil {
return dto.FirewallRuleSyncResult{}, err
}
defer firewallLifecycleTaskMu.Unlock()
firewallRuleSyncTaskMu.Lock()
defer firewallRuleSyncTaskMu.Unlock()
running, err := currentFirewallRuleSyncTaskLocked()
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
if running.Executing {
return runningFirewallRuleSyncResult(request, running.TaskID), nil
}
if firewallSyncSubsystem(request.Subsystem) != "system" {
return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule)
}
taskItem, err := task.NewTask(firewallTaskName(task.TaskSync, firewallTaskHost, string(request.TargetProvider)), task.TaskSync, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall sync task: %w", err)
}
taskItem.AddSubTaskWithOps(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error {
result, err := s.syncRules(t.TaskCtx, clientIP, request, t)
if err != nil {
return err
}
if result.Failed > 0 {
return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": result.Failed}))
}
return nil
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall sync task: %w", err)
}
firewallRuleSyncTaskID = taskItem.TaskID
go func() {
defer func() {
firewallRuleSyncTaskMu.Lock()
if firewallRuleSyncTaskID == taskItem.TaskID {
firewallRuleSyncTaskID = ""
}
firewallRuleSyncTaskMu.Unlock()
}()
if err := taskItem.Execute(); err != nil && global.LOG != nil {
global.LOG.Errorf("firewall sync task %s failed: %v", taskItem.TaskID, err)
}
}()
return dto.FirewallRuleSyncResult{
Subsystem: "system",
TargetProvider: request.TargetProvider,
TaskID: taskItem.TaskID,
Queued: true,
}, nil
}
func (s *FirewallService) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) {
firewallRuleSyncTaskMu.Lock()
defer firewallRuleSyncTaskMu.Unlock()
return currentFirewallRuleSyncTaskLocked()
}
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 runningFirewallRuleSyncResult(request dto.FirewallRuleSyncRequest, taskID string) dto.FirewallRuleSyncResult {
return dto.FirewallRuleSyncResult{
Subsystem: firewallSyncSubsystem(request.Subsystem),
TargetProvider: request.TargetProvider,
TaskID: taskID,
Queued: true,
}
}
func (s *FirewallService) syncConfiguredFirewallPorts(ctx context.Context) error {
configured, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
return err
}
required, err := LoadRequiredFirewallPortWhiteList()
if err != nil {
return err
}
ports := excludeFirewallPorts(configured, required)
return s.SyncSystemPorts(ctx, nil, systemPorts(ports))
}
func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error {
previousSet, err := normalizeSystemPorts(previous)
if err != nil {
return err
}
currentSet, err := normalizeSystemPorts(current)
if err != nil {
return err
}
provider, err := s.selectedProvider(ctx)
if err != nil {
return err
}
if !supportsNativeRuleBatch(provider) {
var syncErrors []error
for _, key := range sortedSystemPortKeys(currentSet) {
if _, exists := previousSet[key]; exists {
continue
}
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
wrapped := fmt.Errorf("restore accepted firewall port %s: %w", key, err)
syncErrors = append(syncErrors, wrapped)
if global.LOG != nil {
global.LOG.Errorf("%v", wrapped)
}
}
}
for _, key := range sortedSystemPortKeys(previousSet) {
if _, exists := currentSet[key]; exists {
continue
}
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
wrapped := fmt.Errorf("release accepted firewall port %s: %w", key, err)
syncErrors = append(syncErrors, wrapped)
if global.LOG != nil {
global.LOG.Errorf("%v", wrapped)
}
}
}
return errors.Join(syncErrors...)
}
for _, key := range sortedSystemPortKeys(currentSet) {
if _, exists := previousSet[key]; exists {
continue
}
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
return err
}
}
for _, key := range sortedSystemPortKeys(previousSet) {
if _, exists := currentSet[key]; exists {
continue
}
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
return err
}
}
return nil
}
func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error {
return newFirewallService().
SyncSystemPorts(context.Background(), systemPorts(previous), systemPorts(current))
}
func syncPanelRequiredPorts(provider string, ports []firewall.PortWhitelist) error {
loadPorts := func() ([]firewall.PortWhitelist, error) { return ports, nil }
if provider == constant.FirewallProviderIptables {
manager := newIptablesHelperManager()
manager.LoadRequiredPorts = loadPorts
return manager.SyncRequiredPorts(true)
}
initialized := false
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
familyInitialized, _, err := nftables_helper.LoadFamilyInitStatus(family, "base")
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return err
}
initialized = initialized || familyInitialized
}
if !initialized {
return nil
}
manager := newNftablesHelperManager()
manager.LoadRequiredPorts = loadPorts
return manager.SyncRequiredPorts()
}
type forwardingRuleSyncCandidate struct {
rule forwarding.Rule
err error
}
func (s *ForwardingService) loadRuleSyncCandidates(
ctx context.Context,
targetProvider filter.Provider,
) (*forwarding.Manager, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) {
target, err := s.manager()
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})
}
targetStatus, err := target.Status()
if err != nil {
return nil, nil, nil, false, err
}
targetRules := make([]forwarding.Rule, 0)
if targetStatus.IsInit {
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, targetStatus.IsInit, nil
}
func verifyForwardingRuleSync(target *forwarding.Manager, desired []forwarding.Rule) error {
actual, err := target.List("", "")
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
actual, err = normalizeForwardingRuntimeRules(actual)
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
if !firewallsync.StatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
}
return 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 forwardingRuleSyncDTO(rule forwarding.Rule) *dto.ForwardRule {
return &dto.ForwardRule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
}
}
func (s *ForwardingService) previewRuleSync(
ctx context.Context,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncPreview, error) {
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
target, candidates, targetRules, _, err := s.loadRuleSyncCandidates(ctx, targetProvider)
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
return forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules), nil
}
func (s *ForwardingService) syncRules(
ctx context.Context,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncResult, error) {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
target, candidates, targetRules, targetInitialized, err := s.loadRuleSyncCandidates(ctx, targetProvider)
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
preview := forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules)
desired := make([]forwarding.Rule, 0, len(candidates))
for _, candidate := range candidates {
if candidate.err == nil {
desired = append(desired, candidate.rule)
}
}
if preview.Blocked > 0 {
return firewallSyncResult(preview, nil, false), nil
}
if len(desired) == 0 && !targetInitialized {
return firewallSyncResult(preview, nil, true), nil
}
reconcileErr := func() error {
if len(desired) > 0 {
if err := s.persistForwardingEnabled(); err != nil {
return err
}
if err := s.activateManager(target); err != nil {
return err
}
}
if err := target.Reconcile(desired); err != nil {
return err
}
return verifyForwardingRuleSync(target, desired)
}()
result := firewallSyncResult(preview, reconcileErr, true)
recordForwardingSyncError(reconcileErr)
if reconcileErr != nil {
if preview.Ready == 0 {
return result, reconcileErr
}
return result, nil
}
return result, nil
}
func forwardingSyncPreview(
target filter.Provider,
candidates []forwardingRuleSyncCandidate,
actual []forwarding.Rule,
) dto.FirewallRuleSyncPreview {
desired := make([]firewallsync.Desired[forwarding.Rule, dto.FirewallRuleSyncItem], 0, len(candidates))
for _, candidate := range candidates {
desired = append(desired, firewallsync.Desired[forwarding.Rule, dto.FirewallRuleSyncItem]{
Value: candidate.rule,
Payload: dto.FirewallRuleSyncItem{
SourceUUID: candidate.rule.Identity(), ForwardRule: forwardingRuleSyncDTO(candidate.rule),
},
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: forwardingRuleSyncDTO(rule)}
},
)
}
func (s *FirewallService) forwardingRuleSyncService() firewallDatabaseSyncAdapter {
if s.forwardingSync == nil {
return newForwardingService()
}
return s.forwardingSync
}
func (s *DockerPortGuardService) loadRuleSyncCandidates(
ctx context.Context,
request dto.FirewallRuleSyncRequest,
) (string, []model.DockerPortGuardPolicy, dockerGuardRuntime, 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) {
if global.DB != nil {
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 "", fmt.Errorf("%w: Docker firewall backend is unavailable", ErrDockerUnavailable)
}
cli, err := s.client()
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
}
return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil
}
func dockerGuardPoliciesFromModels(policies []model.DockerPortGuardPolicy) []docker_guard.Policy {
result := make([]docker_guard.Policy, 0, len(policies))
for _, policy := range policies {
result = append(result, dockerGuardPolicyFromModel(policy))
}
return result
}
func dockerGuardRuleSyncDTO(policy model.DockerPortGuardPolicy) *dto.DockerPortGuardEndpoint {
return &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
}
}
func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGuardEndpoint {
return &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,
}
}
func dockerGuardReadOnlyRuleSyncDTO(policy docker_guard.ReadOnlyPolicy) *dto.DockerPortGuardEndpoint {
return &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,
}
}
func (s *DockerPortGuardService) previewRuleSync(
ctx context.Context,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncPreview, error) {
target, policies, runtime, err := s.loadRuleSyncCandidates(ctx, request)
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
targetInventory, err := runtime.ListPolicies()
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
return dockerSyncPreview(filter.Provider(target), policies, targetInventory), nil
}
func (s *DockerPortGuardService) syncRules(
ctx context.Context,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncResult, error) {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
target, policies, targetRuntime, err := s.loadRuleSyncCandidates(ctx, request)
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
runtimePolicies := dockerGuardPoliciesFromModels(policies)
targetInventory, err := targetRuntime.ListPolicies()
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
preview := dockerSyncPreview(filter.Provider(target), policies, targetInventory)
for _, item := range preview.Items {
if item.Status == firewallsync.StatusBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule {
return firewallSyncResult(preview, nil, false), nil
}
}
if preview.Ready == 0 && preview.Removed == 0 {
return firewallSyncResult(preview, nil, false), nil
}
reconcileErr := func() error {
if err := s.replaceRuntimeReadOnlyPolicies(ctx, targetInventory.ReadOnly); err != nil {
return err
}
if err := docker_guard.ReconcileTarget(target, runtimePolicies, targetRuntime); err != nil {
return err
}
if err := docker_guard.Verify(targetRuntime, runtimePolicies, targetInventory.ReadOnly); err != nil {
return err
}
if len(policies) == 0 {
return nil
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, target); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
}()
result := firewallSyncResult(preview, reconcileErr, true)
recordDockerPortGuardReconcileError(reconcileErr)
if reconcileErr != nil {
return result, reconcileErr
}
return result, nil
}
func dockerSyncPreview(
target filter.Provider,
policies []model.DockerPortGuardPolicy,
inventory docker_guard.PolicyInventory,
) dto.FirewallRuleSyncPreview {
desired := make([]firewallsync.Desired[docker_guard.Policy, dto.FirewallRuleSyncItem], 0, len(policies))
for _, policy := range policies {
desired = append(desired, firewallsync.Desired[docker_guard.Policy, dto.FirewallRuleSyncItem]{
Value: dockerGuardPolicyFromModel(policy),
Payload: dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: dockerGuardRuleSyncDTO(policy)},
})
}
preview := firewallDiffPreview(
"docker", target, desired, inventory.Policies, docker_guard.PolicySyncKey,
func(policy docker_guard.Policy) dto.FirewallRuleSyncItem {
return dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: dockerGuardRuntimeRuleSyncDTO(policy)}
},
)
for _, policy := range inventory.ReadOnly {
preview.Add(dto.FirewallRuleSyncItem{
SourceUUID: dockerGuardReadOnlyPolicyUUID(policy),
DockerRule: dockerGuardReadOnlyRuleSyncDTO(policy),
Status: firewallsync.StatusBlocked,
ReasonCode: firewallsync.ReasonReadOnlyRule,
Reason: firewallsync.ReasonMessage(firewallsync.ReasonReadOnlyRule),
})
}
return preview
}
func (s *FirewallService) dockerRuleSyncService() firewallDatabaseSyncAdapter {
if s.dockerSync == nil {
return newDockerPortGuardService()
}
return s.dockerSync
}
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 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 firewallDiffPreview[T any](subsystem string, target filter.Provider, desired []firewallsync.Desired[T, dto.FirewallRuleSyncItem], 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)}
for _, item := range firewallsync.Diff(desired, actual, key, actualItem) {
row := item.Payload
row.Status, row.ReasonCode, row.Reason = item.Status, item.ReasonCode, item.Reason
preview.Add(row)
}
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
}