package service import ( "context" "errors" "fmt" "sort" "strconv" "strings" "sync" "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/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw" filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime" "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" "github.com/google/uuid" "gorm.io/gorm" ) type FirewallService struct { rules repo.IFirewallRuleRepo adapters firewallRuleRuntimeResolver forwardingSync firewallDatabaseSyncAdapter dockerSync firewallDatabaseSyncAdapter selectedProvider func(context.Context) (filter.Provider, error) requiredPorts func() ([]firewall.PortWhitelist, error) iptablesHelper *iptables_helper.Manager cleanupBackend func(string) error cleanupInactiveBackend func(string) error resetBackend func(string, bool) error dockerActive func() (bool, error) restoreForwarding func(context.Context) error restoreDockerGuard func(context.Context) error baseClient func() (lifecycle.Client, error) } type firewallRuleRuntimeResolver interface { Resolve(filter.Provider) (*filterruntime.Engine, error) Providers() []filter.Provider } var firewallRuleMutationMu sync.Mutex var ( firewallLifecycleTaskMu sync.Mutex firewallLifecycleTaskID string firewallLifecycleRequest dto.FirewallLifecycleOperation ) type IFirewallService interface { SyncPortWhitelist(context.Context) error UpdatePanelPort(context.Context, uint, uint) error LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) OperateFilterChain(request dto.FilterChainOperation) error QueueFilterChainInitialization(request dto.FilterChainOperation) (dto.FilterChainOperationResponse, error) Reset(context.Context, dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) Inventory(context.Context, dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) LoadFirewallNativeDetail(context.Context, dto.FirewallNativeDetail) (string, error) Adopt(context.Context, dto.FirewallRuleAdopt) error Create(context.Context, dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) Delete(context.Context, dto.FirewallRuleDelete) (dto.FirewallRuleDeleteResponse, error) Update(context.Context, string, dto.FirewallRuleUpdate) error Reorder(context.Context, string, dto.FirewallRuleReorder) error PreviewRuleSync(context.Context, string, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) SyncRules(context.Context, string, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) } func NewIFirewallService() IFirewallService { return newFirewallService() } func newFirewallService() *FirewallService { return &FirewallService{ rules: repo.NewIFirewallRuleRepo(), adapters: filterruntime.NewRegistry(firewallRuleSnapshotPolicy), forwardingSync: newForwardingService(), dockerSync: newDockerPortGuardService(), selectedProvider: firewallRuleSelectedProvider, requiredPorts: LoadRequiredFirewallPortWhiteList, iptablesHelper: newIptablesHelperManager(), cleanupBackend: cleanupSystemBackend, cleanupInactiveBackend: cleanupInactiveSystemBackend, resetBackend: resetServiceFirewallBackend, dockerActive: firewallDockerActive, restoreForwarding: func(ctx context.Context) error { return newForwardingService().Restore(ctx) }, restoreDockerGuard: ReconcileDockerPortGuard, baseClient: selectedSystemFirewallClient, } } func firewallDockerActive() (bool, error) { if !cmd.Which("docker") { return false, nil } return controller.CheckActive("docker") } func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) { status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"} status.LifecycleTaskID = currentFirewallLifecycleTaskID() if selected := configuredSystemFirewallBackend(); selected != "" { status.Name, status.Backend = selected, selected } loadClient := s.baseClient if loadClient == nil { loadClient = selectedSystemFirewallClient } client, err := loadClient() if err != nil { if global.LOG != nil { global.LOG.Errorf("load firewall failed, err: %v", err) } if errors.Is(err, lifecycle.ErrNotInstalled) { status.Reason = constant.FirewallBackendNotInstalled return status, nil } status.IsExist = true status.Message = err.Error() return status, nil } status.IsExist = true runtimeStatus, err := lifecycle.LoadStatus(client) if err != nil { return status, err } status.Name, status.Backend = runtimeStatus.Name, runtimeStatus.Name status.Version, status.PingStatus = runtimeStatus.Version, firewall.LoadPingStatus() status.IsActive = runtimeStatus.IsActive if supportsManagedFilterChains(runtimeStatus.Name) { initialized, bound, err := loadFirewallInitStatus(runtimeStatus.Name, chainGroup) if err != nil { return status, err } status.IsInit, status.IsBind = initialized, bound status.IPv4 = loadSystemFirewallFamilyInfo(status.Name, constant.FirewallFamilyIPv4) status.IPv6 = loadSystemFirewallFamilyInfo(status.Name, constant.FirewallFamilyIPv6) } return status, nil } type firewallLifecycleClient struct{ lifecycle.Client } func (c firewallLifecycleClient) Start() error { filterruntime.InvalidateInventory() defer filterruntime.InvalidateInventory() firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() return c.Client.Start() } func (c firewallLifecycleClient) Stop() error { filterruntime.InvalidateInventory() defer filterruntime.InvalidateInventory() firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() return c.Client.Stop() } func (c firewallLifecycleClient) Restart() error { filterruntime.InvalidateInventory() defer filterruntime.InvalidateInventory() firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() return c.Client.Restart() } func currentFirewallLifecycleTaskID() string { firewallLifecycleTaskMu.Lock() defer firewallLifecycleTaskMu.Unlock() return firewallLifecycleTaskID } func lockFirewallLifecycleIdle() error { if !firewallLifecycleTaskMu.TryLock() { return buserr.New("TaskIsExecuting") } if firewallLifecycleTaskID != "" { firewallLifecycleTaskMu.Unlock() return buserr.New("TaskIsExecuting") } return nil } func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) { response := dto.FirewallLifecycleOperationResponse{} if request.Operation == "disableBanPing" || request.Operation == "enableBanPing" { return response, s.OperateFirewall(request) } if !firewallLifecycleTaskMu.TryLock() { return response, buserr.New("TaskIsExecuting") } defer firewallLifecycleTaskMu.Unlock() if firewallLifecycleTaskID != "" { if request == firewallLifecycleRequest { return dto.FirewallLifecycleOperationResponse{TaskID: firewallLifecycleTaskID, Queued: true}, nil } return response, buserr.New("TaskIsExecuting") } running, err := s.CurrentRuleSyncTask() if err != nil { return response, err } if running.Executing { return response, buserr.New("TaskIsExecuting") } loadClient := s.baseClient if loadClient == nil { loadClient = selectedSystemFirewallClient } client, err := loadClient() if err != nil { return response, err } if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) || (request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationStop) && request.Operation != string(lifecycle.OperationRestart)) { return response, s.OperateFirewall(request) } operation, label := task.TaskExec, "Start" switch lifecycle.Operation(request.Operation) { case lifecycle.OperationStop: label = "Stop" case lifecycle.OperationRestart: operation, label = task.TaskRestart, task.TaskRestart } name := task.GetTaskName(client.Name(), label, task.TaskScopeFirewall) taskItem, err := task.NewTask(name, operation, task.TaskScopeFirewall, "", 0) if err != nil { return response, err } taskItem.AddSubTaskWithOps(name, func(t *task.Task) error { return s.runFirewallLifecycleTask(t, client, request) }, nil, 0, 0) if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { closeUnstartedFirewallTask(taskItem) return response, err } firewallLifecycleTaskID, firewallLifecycleRequest = taskItem.TaskID, request go func() { defer func() { closeUnstartedFirewallTask(taskItem) firewallLifecycleTaskMu.Lock() defer firewallLifecycleTaskMu.Unlock() if firewallLifecycleTaskID == taskItem.TaskID { firewallLifecycleTaskID = "" } }() if err := taskItem.Execute(); err != nil && taskItem.Task.Status == constant.StatusExecuting { taskItem.LogFailedWithErr(name, err) taskItem.Task.Status = constant.StatusFailed taskItem.Task.ErrorMsg = err.Error() taskItem.Task.EndAt = time.Now() _ = repo.NewITaskRepo().Update(context.Background(), taskItem.Task) } }() return dto.FirewallLifecycleOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil } 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 (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation) error { ctx := t.TaskCtx provider := filter.Provider(client.Name()) operator := lifecycle.NewOperator(firewallLifecycleClient{client}) operator.RunAction = func(operation, name string, action func() error) error { return runFirewallLifecycleAction(t, task.GetTaskName(name, operation, ""), action) } operationErr := operator.Operate(lifecycle.Operation(request.Operation), request.WithDockerRestart, func(lifecycle.Client) error { rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", client.Name()), func() error { return s.restoreStoredFirewallRules(ctx, provider, t) }) whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { return s.SyncPortWhitelist(ctx) }) return errors.Join(rulesErr, whitelistErr) }) if request.Operation == string(lifecycle.OperationStop) { return operationErr } var recoveryErr *lifecycle.CompletedOperationError if operationErr != nil && !errors.As(operationErr, &recoveryErr) { return operationErr } var forwardingErr error if provider == filter.ProviderFirewalld { forwardingErr = runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func() error { if s.restoreForwarding != nil { return s.restoreForwarding(ctx) } return newForwardingService().Restore(ctx) }) } dockerErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func() error { if provider == filter.ProviderFirewalld { active := s.dockerActive if active == nil { active = firewallDockerActive } running, err := active() if err != nil || !running { return err } } if s.restoreDockerGuard != nil { return s.restoreDockerGuard(ctx) } return ReconcileDockerPortGuard(ctx) }) return errors.Join(operationErr, forwardingErr, dockerErr) } func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation) error { switch request.Operation { case "disableBanPing": if err := firewall.UpdatePingStatus("0"); err != nil { return err } return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusDisable) case "enableBanPing": if err := firewall.UpdatePingStatus("1"); err != nil { return err } return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusEnable) } baseClient := s.baseClient if baseClient == nil { baseClient = selectedSystemFirewallClient } client, err := baseClient() if err != nil { return err } operation := lifecycle.Operation(request.Operation) operationErr := lifecycle.NewOperator(firewallLifecycleClient{client}).Operate(operation, request.WithDockerRestart, s.restoreFirewallAfterStart) restoreFirewalld := client.Name() == lifecycle.ProviderFirewalld && (operation == lifecycle.OperationStart || operation == lifecycle.OperationRestart) if operation != lifecycle.OperationStart && operation != lifecycle.OperationRestart { return operationErr } if operationErr != nil { var completedErr *lifecycle.CompletedOperationError var dockerRestartErr *lifecycle.DockerRestartError if !errors.As(operationErr, &completedErr) && !errors.As(operationErr, &dockerRestartErr) { return operationErr } if global.LOG != nil { global.LOG.Warnf("firewall %s completed with post-start recovery errors: %v", operation, operationErr) } } if restoreFirewalld { restoreErr := s.restoreFirewalldRuntimeDependents(context.Background(), operation) if restoreErr != nil && global.LOG != nil { global.LOG.Errorf("restore firewalld runtime dependents after %s failed: %v", operation, restoreErr) } return nil } ReconcileDockerPortGuardBestEffort(context.Background()) return nil } func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error { if oldPort == 0 || oldPort > 65535 || port == 0 || port > 65535 { return fmt.Errorf("invalid panel port transition %d -> %d", oldPort, port) } if LoadPanelPort() != strconv.Itoa(int(oldPort)) { return fmt.Errorf("panel port changed before firewall update") } if oldPort == port { return nil } return updateSystemAccessPortWhitelist(ctx, firewall.PortWhitelistTypePanel, []string{strconv.Itoa(int(port))}) } func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, operation lifecycle.Operation) error { restoreForwarding := s.restoreForwarding if restoreForwarding == nil { restoreForwarding = func(ctx context.Context) error { return newForwardingService().Restore(ctx) } } restoreDockerGuard := s.restoreDockerGuard if restoreDockerGuard == nil { restoreDockerGuard = ReconcileDockerPortGuard } dockerActive := s.dockerActive if dockerActive == nil { dockerActive = firewallDockerActive } active, err := dockerActive() restoreErr := restoreFirewalldDependents( ctx, fmt.Sprintf("after firewalld %s", operation), err == nil && active, restoreForwarding, restoreDockerGuard, ) if err != nil { return errors.Join(fmt.Errorf("check Docker status after firewalld %s: %w", operation, err), restoreErr) } return restoreErr } func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) error { provider, err := selectedSystemFirewallProvider() if err != nil { return err } if err := s.operateFilterChainBase(provider, request); err != nil { return err } if request.Operate != string(firewall.BaseOperationInit) && request.Operate != string(firewall.BaseOperationBind) { return nil } ctx := context.Background() rulesErr := s.restoreStoredFirewallRules(ctx, filter.Provider(provider), nil) whitelistErr := s.SyncPortWhitelist(ctx) return errors.Join(rulesErr, whitelistErr) } func (s *FirewallService) QueueFilterChainInitialization( request dto.FilterChainOperation, ) (dto.FilterChainOperationResponse, error) { if request.Operate != string(firewall.BaseOperationInit) { return dto.FilterChainOperationResponse{}, fmt.Errorf("only filter chain initialization can be queued") } provider, err := selectedSystemFirewallProvider() if err != nil { return dto.FilterChainOperationResponse{}, err } if !supportsManagedFilterChains(provider) { return dto.FilterChainOperationResponse{}, fmt.Errorf("filter chain operations are not supported for %s", provider) } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { return dto.FilterChainOperationResponse{}, err } taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskHost, provider), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0) if err != nil { return dto.FilterChainOperationResponse{}, fmt.Errorf("create firewall initialization task: %w", err) } taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeChainsStep", provider), func(t *task.Task) error { t.Logf("backend=%s", provider) return s.operateFilterChainBase(provider, request) }, nil) taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error { rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", provider), func() error { return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider), t) }) whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { return s.SyncPortWhitelist(t.TaskCtx) }) return errors.Join(rulesErr, whitelistErr) }, nil) if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall initialization task: %w", err) } go func() { _ = taskItem.Execute() }() return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: 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 { filterruntime.InvalidateInventory() defer filterruntime.InvalidateInventory() if err := s.checkSelectedProvider(context.Background(), filter.Provider(provider)); err != nil { return err } if !supportsManagedFilterChains(provider) { return fmt.Errorf("filter chain operations are not supported for %s", provider) } if provider == constant.FirewallProviderNftables { if err := newNftablesHelperManager().Operate(firewall.BaseOperation(request.Operate)); err != nil { return err } } else if err := s.iptablesHelper.Operate(firewall.BaseOperation(request.Operate)); err != nil { return err } return nil } func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) { filterruntime.InvalidateInventory() defer filterruntime.InvalidateInventory() if err := lockFirewallLifecycleIdle(); err != nil { return dto.FirewallRuleResetResponse{}, err } defer firewallLifecycleTaskMu.Unlock() firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() provider := request.Provider selected := provider if provider == "" { var err error selected, err = s.selectedProvider(ctx) if err != nil { return dto.FirewallRuleResetResponse{}, err } provider = selected } else if isDirectFirewallProvider(provider) { var err error selected, err = s.selectedProvider(ctx) if err != nil { return dto.FirewallRuleResetResponse{}, err } } stored, err := s.rules.List(ctx) if err != nil { return dto.FirewallRuleResetResponse{}, err } if provider == filter.ProviderIptables || provider == filter.ProviderNftables { cleanup := s.cleanupBackend if isDirectFirewallProvider(selected) && selected != provider { cleanup = s.cleanupInactiveBackend if cleanup == nil { cleanup = cleanupInactiveSystemBackend } } else if cleanup == nil { cleanup = cleanupSystemBackend } if err := cleanup(string(provider)); err != nil { return dto.FirewallRuleResetResponse{}, err } return dto.FirewallRuleResetResponse{Removed: len(stored), Disabled: true}, nil } if provider != filter.ProviderUFW && provider != filter.ProviderFirewalld { return dto.FirewallRuleResetResponse{}, fmt.Errorf("%w: unsupported firewall provider %s", filter.ErrProviderUnavailable, provider) } reset := s.resetBackend if reset == nil { reset = resetServiceFirewallBackend } restartDocker := false if provider == filter.ProviderFirewalld && request.WithDockerRestart { dockerActive := s.dockerActive if dockerActive == nil { dockerActive = firewallDockerActive } active, err := dockerActive() if err != nil { return dto.FirewallRuleResetResponse{}, fmt.Errorf("check Docker status before resetting firewalld: %w", err) } restartDocker = active } resetErr := reset(string(provider), restartDocker) if resetErr != nil { var dockerRestartErr *lifecycle.DockerRestartError if provider != filter.ProviderFirewalld || !errors.As(resetErr, &dockerRestartErr) { return dto.FirewallRuleResetResponse{}, resetErr } } if provider == filter.ProviderFirewalld { restoreForwarding := s.restoreForwarding if restoreForwarding == nil { restoreForwarding = func(ctx context.Context) error { return newForwardingService().Restore(ctx) } } restoreDockerGuard := s.restoreDockerGuard if restoreDockerGuard == nil { restoreDockerGuard = ReconcileDockerPortGuard } restoreErr := restoreFirewalldDependents( ctx, "after resetting firewalld", restartDocker, restoreForwarding, restoreDockerGuard, ) if err := errors.Join(resetErr, restoreErr); err != nil { return dto.FirewallRuleResetResponse{}, err } } return dto.FirewallRuleResetResponse{Removed: len(stored), Disabled: true}, nil } func isDirectFirewallProvider(provider filter.Provider) bool { return provider == filter.ProviderIptables || provider == filter.ProviderNftables } func resetServiceFirewallBackend(provider string, withDockerRestart bool) error { client, err := lifecycle.NewClientFor(provider) if err != nil { return err } return resetServiceFirewallClient(client, withDockerRestart, func( client lifecycle.Client, restartDocker bool, prepareStop func() error, ) error { return lifecycle.NewOperator(client).StopWithPrepare(restartDocker, prepareStop) }) } 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 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 (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) { requestedScopes := request.Scopes if len(requestedScopes) == 0 && request.Scope.Provider != "" { requestedScopes = []filter.Scope{request.Scope} } if len(requestedScopes) == 0 { return dto.FirewallRuleInventoryResponse{}, filter.ErrInvalidScope } scopes := make([]filter.Scope, len(requestedScopes)) for index, requested := range requestedScopes { scopes[index] = requested.Normalize() } if len(scopes) == 1 && isCombinedUFWInventoryScope(scopes[0]) { scope := scopes[0] if err := s.checkSelectedProvider(ctx, scope.Provider); err != nil { return dto.FirewallRuleInventoryResponse{}, err } runtime, err := s.adapters.Resolve(scope.Provider) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } response, err := s.combinedUFWInventory(ctx, runtime, scope, request.Refresh) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } return finalizeFirewallInventory(response, request), nil } provider := scopes[0].Provider for _, scope := range scopes { if err := scope.ValidateMVP(); err != nil { return dto.FirewallRuleInventoryResponse{}, err } if scope.Provider != provider { return dto.FirewallRuleInventoryResponse{}, fmt.Errorf( "%w: inventory scopes must use the same provider", filter.ErrInvalidScope, ) } } if err := s.checkSelectedProvider(ctx, provider); err != nil { return dto.FirewallRuleInventoryResponse{}, err } runtime, err := s.adapters.Resolve(provider) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } stored, err := s.rules.List(ctx) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, provider) response := dto.FirewallRuleInventoryResponse{Items: failures} runtime = runtime.NewObservationSession() unavailable := make(map[filter.Family]error) for _, scope := range scopes { var snapshot filter.Snapshot err := unavailable[scope.Family] if err == nil { snapshot, err = runtime.ObserveInventory(ctx, scope, request.Refresh) } if errors.Is(err, filter.ErrFamilyUnavailable) { if unavailable[scope.Family] == nil { response.Notices = append(response.Notices, filter.ScopeNotice{Code: filter.ScopeNoticeFamilyUnavailable, Values: []string{string(scope.Family), err.Error()}}) unavailable[scope.Family] = err } for _, desired := range desiredByScope[scope.Key()] { response.Items = append(response.Items, filter.InventoryItem{ Rule: desired.Rule, Desired: &desired, State: filter.InventoryStateDrifted, Match: filter.InventoryMatchNone, Error: err.Error(), }) } continue } if err != nil { return dto.FirewallRuleInventoryResponse{}, err } desired := desiredByScope[scope.Key()] items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } response.Items = append(response.Items, items...) response.Notices = append(response.Notices, snapshot.Notices...) } return finalizeFirewallInventory(response, request), nil } func finalizeFirewallInventory( response dto.FirewallRuleInventoryResponse, request dto.FirewallRuleInventory, ) dto.FirewallRuleInventoryResponse { provider := request.Scope.Provider if len(request.Scopes) > 0 { provider = request.Scopes[0].Provider } response.IPv4Range, response.IPv6Range = filter.InventoryPositionRanges(provider, response.Items) response.AllTotal = int64(len(response.Items)) for _, item := range response.Items { if isDeletableManagedInventoryItem(item) { response.ManagedTotal++ } } filtered := make([]filter.InventoryItem, 0, len(response.Items)) for _, item := range response.Items { if matchesFirewallInventoryRequest(item, request) { filtered = append(filtered, item) } } response.Total = int64(len(filtered)) if request.All { response.Items = filtered return response } page, pageSize := max(1, request.Page), max(1, request.PageSize) start := (page - 1) * pageSize if start >= len(filtered) { response.Items = make([]filter.InventoryItem, 0) return response } end := min(start+pageSize, len(filtered)) response.Items = filtered[start:end] return response } func matchesFirewallInventoryRequest(item filter.InventoryItem, request dto.FirewallRuleInventory) bool { if slicesContains(request.ExcludeChains, item.Rule.Scope.Chain) { return false } if len(request.Families) > 0 && !matchesFirewallInventoryFamily(item.Rule, request.Families) { return false } if len(request.Actions) > 0 && !matchesFirewallInventoryAction(item.Rule.Action, request.Actions) { return false } if len(request.States) > 0 && !slicesContains(request.States, item.State) { return false } keyword := strings.ToLower(strings.TrimSpace(request.Info)) if keyword == "" { return true } rule := item.Rule values := []string{ firewallInventoryProtocol(rule), rule.SourceAddress, rule.SourcePort, rule.DestinationAddress, rule.DestinationPort, rule.Description, string(rule.Action), string(item.State), } if item.Observed != nil { values = append(values, item.Observed.Rule.Description) } if item.Desired != nil { values = append(values, item.Desired.Rule.Description) } for _, value := range values { if strings.Contains(strings.ToLower(value), keyword) { return true } } return false } func matchesFirewallInventoryFamily(rule filter.FirewallRule, families []filter.Family) bool { for _, family := range families { if rule.Scope.Family != filter.FamilyInet && rule.Scope.Family == family { return true } if rule.Scope.Family == filter.FamilyInet && (rule.SourceAddress == "" || (family == filter.FamilyIPv6) == strings.Contains(rule.SourceAddress, ":")) { return true } } return false } func matchesFirewallInventoryAction(action filter.Action, actions []string) bool { for _, requested := range actions { if requested == "accept" && action == filter.ActionAccept { return true } if requested == "deny" && (action == filter.ActionDrop || action == filter.ActionReject) { return true } } return false } func firewallInventoryProtocol(rule filter.FirewallRule) string { if rule.NativeKind == filter.NativeKindZoneService { return "service" } if rule.NativeKind == filter.NativeKindUFWApplication && rule.Protocol == "" { return "app" } if rule.Scope.Provider == filter.ProviderUFW && rule.Protocol == "all" && rule.DestinationPort != "" { return "tcp/udp" } return rule.Protocol } func slicesContains[T comparable](values []T, target T) bool { for _, value := range values { if value == target { return true } } return false } func isDeletableManagedInventoryItem(item filter.InventoryItem) bool { if item.Desired == nil || item.Desired.Protected || item.State == filter.InventoryStateProtected { return false } if item.Desired.Origin != filter.RuleOriginCreated && item.Desired.Origin != filter.RuleOriginAdopted { return false } if isIptablesSystemPresetInventoryScope(item.Rule.Scope) { return false } return item.State != filter.InventoryStateDrifted || (item.Match == filter.InventoryMatchMissing && item.Observed == nil) } func isIptablesSystemPresetInventoryScope(scope filter.Scope) bool { return (scope.Provider == filter.ProviderIptables || scope.Provider == filter.ProviderNftables) && (scope.Chain == filter.BasicBeforeChain || scope.Chain == filter.BasicAfterChain) } func isCombinedUFWInventoryScope(scope filter.Scope) bool { scope = scope.Normalize() return scope.Provider == filter.ProviderUFW && scope.Family == filter.FamilyInet && scope.Table == "" && scope.Zone == "" && scope.Chain == filter.UFWInputChain && scope.Direction == filter.DirectionInput } func (s *FirewallService) combinedUFWInventory( ctx context.Context, runtime *filterruntime.Engine, scope filter.Scope, refresh bool, ) (dto.FirewallRuleInventoryResponse, error) { scopes := []filter.Scope{scope, scope} scopes[0].Family = filter.FamilyIPv4 scopes[1].Family = filter.FamilyIPv6 snapshots, err := runtime.ObserveInventoryScopes(ctx, scopes, refresh) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } if len(snapshots) != len(scopes) { return dto.FirewallRuleInventoryResponse{}, fmt.Errorf("%w: incomplete UFW multi-family inventory", filter.ErrAdapterUnavailable) } stored, err := s.rules.List(ctx) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, scope.Provider) response := dto.FirewallRuleInventoryResponse{Items: failures} seenNotices := make(map[string]struct{}) for index, snapshot := range snapshots { if snapshot.Scope.Key() != scopes[index].Key() { return dto.FirewallRuleInventoryResponse{}, fmt.Errorf("%w: unexpected UFW inventory scope %q", filter.ErrInvalidScope, snapshot.Scope.Key()) } desired := desiredByScope[snapshot.Scope.Key()] items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) if err != nil { return dto.FirewallRuleInventoryResponse{}, err } response.Items = append(response.Items, items...) for _, notice := range snapshot.Notices { key := string(notice.Code) + "\x00" + strings.Join(notice.Values, "\x00") if _, exists := seenNotices[key]; exists { continue } seenNotices[key] = struct{}{} response.Notices = append(response.Notices, notice) } } return response, nil } func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request dto.FirewallNativeDetail) (string, error) { provider := filter.Provider(strings.ToLower(strings.TrimSpace(string(request.Provider)))) nativeKind := filter.NativeKind(strings.ToLower(strings.TrimSpace(string(request.NativeKind)))) switch provider { case filter.ProviderFirewalld: if nativeKind != filter.NativeKindZoneService { return "", fmt.Errorf("%w: firewalld detail kind %q", filter.ErrInvalidRule, nativeKind) } case filter.ProviderUFW: if nativeKind != filter.NativeKindUFWApplication { return "", fmt.Errorf("%w: UFW detail kind %q", filter.ErrInvalidRule, nativeKind) } default: return "", fmt.Errorf("%w: native details for %s", filter.ErrUnsupportedScope, provider) } if err := s.checkSelectedProvider(ctx, provider); err != nil { return "", err } runtime, err := s.adapters.Resolve(provider) if err != nil { return "", err } return runtime.NativeDetail(ctx, request.Name, request.Permanent) } 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 == "" { scope.Family = defaultFirewallRuleFamily(rule, selected) } 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 defaultFirewallRuleFamily(rule filter.FirewallRule, provider filter.Provider) filter.Family { if strings.EqualFold(strings.TrimSpace(rule.Protocol), "icmpv6") || strings.Contains(rule.SourceAddress, ":") || strings.Contains(rule.DestinationAddress, ":") { return filter.FamilyIPv6 } if provider == filter.ProviderFirewalld { return filter.FamilyInet } return filter.FamilyIPv4 } type preparedFirewallRuleCreate struct { request dto.FirewallRuleCreateItem runtime *filterruntime.Engine } func (s *FirewallService) Create( ctx context.Context, request dto.FirewallRuleCreate, ) (dto.FirewallRuleCreateResponse, error) { taskItem, err := task.NewTask(firewallTaskName(task.TaskCreate, firewallTaskHost, ""), task.TaskCreate, task.TaskScopeFirewall, "", 0) if err != nil { return dto.FirewallRuleCreateResponse{}, err } taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() _, err := s.createRules(t.TaskCtx, request, t) 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.FirewallRuleCreateResponse{}, fmt.Errorf("save firewall creation task: %w", err) } go func() { if err := taskItem.Execute(); err != nil && global.LOG != nil { global.LOG.Errorf("firewall creation task %s failed: %v", taskItem.TaskID, err) } }() return dto.FirewallRuleCreateResponse{TaskID: taskItem.TaskID, Queued: true}, 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 itemOrigin struct { index, part, count int rule filter.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(origin itemOrigin, status string, err error) { rule := origin.rule label := fmt.Sprintf("[%d/%d]", origin.index+1, len(request.Items)) if origin.count > 1 { label += fmt.Sprintf("[%d/%d]", origin.part+1, origin.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: origin.index, Status: status, Rule: rule, Error: err.Error(), }) } } selected, err := s.selectedProvider(ctx) if err != nil { for index := range request.Items { record(itemOrigin{index: index, rule: request.Items[index].Rule}, "skipped", err) } return result, err } var stop error var prepared []preparedFirewallRuleCreate var origins []itemOrigin flush := func() { if len(prepared) == 0 { return } defer func() { prepared, origins = nil, nil }() if stop == nil { stop = ctx.Err() } if stop != nil { for _, origin := range origins { record(origin, "skipped", stop) } return } runtime := prepared[0].runtime scope := prepared[0].request.Rule.Scope snapshot, err := runtime.ObserveMutation(ctx, scope) if err != nil { for _, origin := range origins { record(origin, "failed", err) } if !errors.Is(err, filter.ErrFamilyUnavailable) { stop = err } return } stored, err := s.rules.List(ctx) if err != nil { for _, origin := range origins { record(origin, "failed", err) } stop = err return } identities, err := firewallRuleCollisions(stored, runtime.Provider(), "") if err != nil { for _, origin := range origins { record(origin, "failed", err) } stop = err return } observedIdentities, err := filter.ObservedRuleCollisionIndex(snapshot) if err != nil { for _, origin := range origins { record(origin, "failed", err) } stop = err return } valid := prepared[:0] validOrigins := origins[:0] for index, entry := range prepared { rule := entry.request.Rule checkErr := identities.Check(rule) if checkErr == nil { checkErr = observedIdentities.Check(rule) } if checkErr == nil { checkErr = identities.Add(rule) } if checkErr != nil { record(origins[index], "failed", checkErr) continue } valid = append(valid, entry) validOrigins = append(validOrigins, origins[index]) } if len(valid) == 0 { return } if len(valid) > 1 && t != nil { t.Log(i18n.GetMsgWithMap("FirewallCreateBatchStep", map[string]interface{}{ "backend": runtime.Provider(), "count": len(valid), })) } itemErrors := s.applyCreateRules(ctx, runtime, snapshot, stored, valid) for offset, origin := range validOrigins { if err := itemErrors[offset]; err != nil { record(origin, "failed", err) if firewallCreateUnavailable(err) { stop = err } } else { record(origin, "succeeded", nil) } } } for index, item := range request.Items { if stop == nil { stop = ctx.Err() } origin := itemOrigin{index: index, rule: item.Rule} if stop != nil { record(origin, "skipped", stop) continue } rules := []filter.FirewallRule{item.Rule} if item.SourceKind == constant.FirewallRuleSourceImported { rules, err = convertImportedFirewallRule(item.Rule, selected) if err != nil { record(origin, "failed", err) continue } if 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), })) } } else if selected == filter.ProviderUFW && strings.TrimSpace(item.Rule.DestinationPort) != "" { protocol := strings.ToLower(strings.TrimSpace(item.Rule.Protocol)) if protocol == "" || protocol == "all" || protocol == "any" { rules, err = filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(item.Rule, selected)) if err != nil { record(origin, "failed", err) continue } } } for part, rule := range rules { origin := itemOrigin{index: index, part: part, count: len(rules), rule: rule} scope := applySelectedProviderScopeDefaults(rule, selected).Scope.Normalize() if len(prepared) > 0 && (prepared[0].request.Rule.Scope != scope || rule.OrderIndex != nil) { flush() } if stop == nil { stop = ctx.Err() } if stop != nil { record(origin, "skipped", stop) continue } child := item child.Rule = rule entry, prepareErr := s.prepareCreate(ctx, selected, child) if prepareErr != nil { record(origin, "failed", prepareErr) if firewallCreateUnavailable(prepareErr) { stop = prepareErr } continue } origin.rule = entry.request.Rule prepared = append(prepared, entry) origins = append(origins, origin) if !supportsNativeRuleBatch(selected) || rule.OrderIndex != nil { flush() } } } flush() return result, stop } func convertImportedFirewallRule(rule filter.FirewallRule, selected filter.Provider) ([]filter.FirewallRule, error) { source := rule.Scope.Normalize().Provider if source == "" { source = selected } rules, err := filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(rule, source)) if err != nil { return nil, err } var converted []filter.FirewallRule for _, sourceRule := range rules { policy, err := model.FirewallRuleFromDomain(sourceRule) if err != nil { return nil, err } targetRules, err := policy.RulesForProvider(selected) if err != nil { return nil, err } converted = append(converted, targetRules...) } return converted, nil } func firewallCreateUnavailable(err error) bool { return errors.Is(err, filter.ErrProviderUnavailable) || errors.Is(err, filter.ErrAdapterUnavailable) || errors.Is(err, filter.ErrInventoryUnavailable) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) } func (s *FirewallService) prepareCreate(ctx context.Context, selected filter.Provider, request dto.FirewallRuleCreateItem) (preparedFirewallRuleCreate, error) { rule, err := filter.NormalizeRule(applySelectedProviderScopeDefaults(request.Rule, selected)) if err != nil { return preparedFirewallRuleCreate{}, err } if rule.Scope.Provider != selected { return preparedFirewallRuleCreate{}, fmt.Errorf("%w: selected provider is %s", filter.ErrInvalidRule, selected) } runtime, err := s.adapters.Resolve(selected) if err != nil { return preparedFirewallRuleCreate{}, err } rule, err = runtime.Prepare(rule) if err != nil { return preparedFirewallRuleCreate{}, err } if err := runtime.CheckRule(ctx, rule); err != nil { return preparedFirewallRuleCreate{}, err } rule.UUID = "" request.Rule = rule if request.SourceKind == "" { request.SourceKind = constant.FirewallRuleSourceUser } return preparedFirewallRuleCreate{request: request, runtime: runtime}, nil } func (s *FirewallService) applyCreateRules(ctx context.Context, runtime *filterruntime.Engine, snapshot filter.Snapshot, stored []model.FirewallRule, prepared []preparedFirewallRuleCreate) []error { results := make([]error, len(prepared)) failAll := func(err error) []error { for index := range results { results[index] = err } return results } var maximumSequence int64 for _, record := range stored { if record.Sequence != nil && *record.Sequence > maximumSequence { maximumSequence = *record.Sequence } } records := make([]model.FirewallRule, 0, len(prepared)) changes := make([]filter.DesiredChange, 0, len(prepared)) for _, entry := range prepared { rule := entry.request.Rule appendRule := false if rule.Scope.Provider == filter.ProviderUFW && rule.OrderIndex == nil { position, err := runtime.AppendPosition(ctx, snapshot, rule) if err != nil { return failAll(err) } rule.OrderIndex, appendRule = &position, true } else if rule.OrderIndex != nil { maximum, err := runtime.MaxPosition(ctx, snapshot, rule) if err != nil { return failAll(err) } if *rule.OrderIndex < 1 || *rule.OrderIndex > maximum+1 { return failAll(fmt.Errorf("%w: create target position %d is out of range 1-%d", filter.ErrInvalidRule, *rule.OrderIndex, maximum+1)) } appendRule = rule.Scope.Provider == filter.ProviderUFW && *rule.OrderIndex == maximum+1 } record, err := firewallRuleModelForCreate(rule, entry.request, constant.FirewallRuleOriginCreated) if err != nil { return failAll(err) } if rule.Scope.Provider != filter.ProviderFirewalld { maximumSequence += model.FirewallRuleSequenceStep sequence := maximumSequence if rule.OrderIndex != nil { sequence, err = s.sequenceForCreatedFirewallRule(ctx, snapshot, rule) if err != nil { return failAll(err) } } record.Sequence = &sequence } record.UUID = uuid.NewString() rule.UUID = record.UUID records = append(records, record) changes = append(changes, filter.DesiredChange{Operation: filter.ChangeCreate, After: &rule, Append: appendRule}) } if err := runtime.ExecuteCreate(ctx, snapshot, changes); err != nil { return failAll(firewallCreateExecutionError(err)) } for index := range records { results[index] = s.saveFirewallRule(ctx, &records[index]) } return results } func firewallCreateExecutionError(err error) error { return fmt.Errorf("%s: %w", i18n.GetMsgByKey("FirewallCreateRuleExecutionFailed"), err) } 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 } type preparedFirewallRuleDelete struct { index int stored model.FirewallRule desired filter.DesiredRule runtime *filterruntime.Engine compiled int } func (s *FirewallService) Delete(ctx context.Context, request dto.FirewallRuleDelete) (dto.FirewallRuleDeleteResponse, error) { if err := ctx.Err(); err != nil { return dto.FirewallRuleDeleteResponse{}, err } if len(request.UUIDs) == 0 && len(request.BeforeRules) == 0 { return dto.FirewallRuleDeleteResponse{}, fmt.Errorf("%w: rules are required", filter.ErrInvalidRule) } request.UUIDs = append([]string(nil), request.UUIDs...) request.BeforeRules = append([]dto.FirewallRuleDeleteTarget(nil), request.BeforeRules...) taskItem, err := task.NewTask(firewallTaskName(task.TaskDelete, firewallTaskHost, ""), task.TaskDelete, task.TaskScopeFirewall, "", 0) if err != nil { return dto.FirewallRuleDeleteResponse{}, err } taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { t.Logf("rules=%d", len(request.UUIDs)+len(request.BeforeRules)) firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() if err := t.TaskCtx.Err(); err != nil { return err } _, err := s.deleteRules(t.TaskCtx, request, t) 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.FirewallRuleDeleteResponse{}, fmt.Errorf("save firewall deletion task: %w", err) } go func() { if err := taskItem.Execute(); err != nil && global.LOG != nil { global.LOG.Errorf("firewall deletion task %s failed: %v", taskItem.TaskID, err) } }() return dto.FirewallRuleDeleteResponse{TaskID: taskItem.TaskID, Queued: true}, nil } func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallRuleDelete, t *task.Task) (result dto.FirewallRuleDeleteResponse, taskErr error) { var firstFailure error defer func() { sort.SliceStable(result.Errors, func(i, j int) bool { return result.Errors[i].Index < result.Errors[j].Index }) if t != nil { t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{ "succeeded": result.Succeeded, "failed": result.Failed, })) } if taskErr == nil { taskErr = firstFailure } }() record := func(index int, ruleUUID string, err error) { if err != nil { result.Failed++ result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{Index: index, UUID: ruleUUID, Error: err.Error()}) if firstFailure == nil { firstFailure = err } } else { result.Succeeded++ } if t != nil { label := fmt.Sprintf("[%d/%d] %s", result.Succeeded+result.Failed, len(request.UUIDs)+len(request.BeforeRules), ruleUUID) t.LogWithStatus(label, err) } } selectedProvider, err := s.selectedProvider(ctx) if err != nil { return result, err } type beforeGroup struct { targets []dto.FirewallRuleDeleteTarget indexes []int } beforeGroups := make(map[string]*beforeGroup) for index, target := range request.BeforeRules { key := target.Scope.Normalize().Key() if beforeGroups[key] == nil { beforeGroups[key] = &beforeGroup{} } group := beforeGroups[key] group.targets = append(group.targets, target) group.indexes = append(group.indexes, len(request.UUIDs)+index) } for _, group := range beforeGroups { err := s.deleteBeforeRules(ctx, selectedProvider, group.targets) for index, target := range group.targets { record(group.indexes[index], target.InstanceKey, err) } } type deleteGroup struct{ items []preparedFirewallRuleDelete } groups := make([]deleteGroup, 0) groupIndexes := make(map[string]int) seen := make(map[string]struct{}, len(request.UUIDs)) for index, value := range request.UUIDs { ruleUUID := strings.TrimSpace(value) if err := ctx.Err(); err != nil { record(index, ruleUUID, err) continue } if _, exists := seen[ruleUUID]; exists { record(index, ruleUUID, fmt.Errorf("duplicate firewall rule UUID")) continue } seen[ruleUUID] = struct{}{} prepared, err := s.prepareDelete(ctx, index, ruleUUID, selectedProvider) if err != nil { record(index, ruleUUID, err) continue } groupKey := string(prepared.desired.Rule.Scope.Provider) + ":" + prepared.desired.Rule.Scope.Key() if prepared.compiled != 1 { groupKey += ":" + prepared.stored.UUID } groupIndex, exists := groupIndexes[groupKey] if !exists { groupIndex = len(groups) groupIndexes[groupKey] = groupIndex groups = append(groups, deleteGroup{}) } groups[groupIndex].items = append(groups[groupIndex].items, prepared) } for _, group := range groups { if len(group.items) > 1 && group.items[0].compiled == 1 && supportsNativeRuleBatch(group.items[0].desired.Rule.Scope.Provider) { err := ctx.Err() if err == nil { if t != nil { t.Logf("%s: rules=%d", i18n.GetMsgByKey(task.TaskDelete), len(group.items)) } err = s.deleteNativeRuleBatch(ctx, group.items) } for _, item := range group.items { record(item.index, item.stored.UUID, err) } continue } for _, item := range group.items { err := ctx.Err() if err == nil { if t != nil { t.Logf("%s %s", i18n.GetMsgByKey(task.TaskDelete), item.stored.UUID) } err = s.deleteRule(ctx, item.stored.UUID) } record(item.index, item.stored.UUID, err) } } return result, nil } func (s *FirewallService) deleteBeforeRules(ctx context.Context, provider filter.Provider, targets []dto.FirewallRuleDeleteTarget) error { scope := targets[0].Scope.Normalize() if scope.Provider != provider || !isDirectFirewallProvider(provider) || scope.Chain != filter.BasicBeforeChain { return fmt.Errorf("%w: native deletion only supports the selected firewall before chain", filter.ErrUnsupportedScope) } runtime, err := s.adapters.Resolve(provider) if err != nil { return err } snapshot, err := runtime.ObserveMutation(ctx, scope) if err != nil { return err } changes := make([]filter.DesiredChange, 0, len(targets)) seen := make(map[string]bool, len(targets)) for _, target := range targets { if target.Scope.Normalize().Key() != scope.Key() || seen[target.InstanceKey] { return fmt.Errorf("%w: duplicate or mismatched before rule", filter.ErrInvalidRule) } seen[target.InstanceKey] = true observed, err := filter.FindCandidate(snapshot.Rules, target.InstanceKey) if err != nil { return filter.ErrRuleStale } if err := filter.GuardMutation(observed); err != nil { return err } if observed.ParseStatus != filter.ParseStatusSupported || observed.Locator.Position == nil { return fmt.Errorf("%w: before rule cannot be deleted", filter.ErrUnsupportedScope) } rule := firewallsync.ObservedRule(observed) if rule.UUID == "" { rule.UUID = uuid.NewString() } locator := observed.Locator changes = append(changes, filter.DesiredChange{ Operation: filter.ChangeDelete, Before: &rule, Locator: &locator, UnmarkedAdopted: observed.Marker == "", }) } sort.Slice(changes, func(i, j int) bool { return *changes[i].Locator.Position > *changes[j].Locator.Position }) plan, verification, err := runtime.Execute(ctx, snapshot, changes) if err != nil { return err } if !verification.Matched { return filter.ErrVerificationFailed } if len(verification.Snapshot.Rules) != len(snapshot.Rules)-len(changes) { return rollbackFirewallPlan(ctx, runtime, plan, filter.ErrVerificationFailed) } return nil } func (s *FirewallService) prepareDelete( ctx context.Context, index int, ruleUUID string, selectedProvider filter.Provider, ) (preparedFirewallRuleDelete, error) { if ruleUUID == "" { return preparedFirewallRuleDelete{}, fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid) } stored, err := s.rules.GetByUUID(ctx, ruleUUID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return preparedFirewallRuleDelete{}, fmt.Errorf("%w: managed rule %q was not found", filter.ErrInvalidRule, ruleUUID) } return preparedFirewallRuleDelete{}, err } if err := checkFirewallRuleWhitelistProtection(selectedProvider, stored); err != nil { return preparedFirewallRuleDelete{}, err } if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { return preparedFirewallRuleDelete{}, fmt.Errorf("%w: only created or adopted rules can be deleted", filter.ErrInvalidRule) } desiredRules, err := s.compileStoredFirewallRules(ctx, stored, selectedProvider) if err != nil { return preparedFirewallRuleDelete{}, err } if len(desiredRules) == 0 { return preparedFirewallRuleDelete{}, fmt.Errorf("%w: policy %q has no compiled target rules", filter.ErrInvalidRule, ruleUUID) } desired := desiredRules[0] runtime, err := s.adapters.Resolve(desired.Rule.Scope.Provider) if err != nil { return preparedFirewallRuleDelete{}, err } return preparedFirewallRuleDelete{ index: index, stored: stored, desired: desired, runtime: runtime, compiled: len(desiredRules), }, nil } func (s *FirewallService) deleteNativeRuleBatch(ctx context.Context, prepared []preparedFirewallRuleDelete) error { runtime := prepared[0].runtime snapshot, err := runtime.ObserveMutation(ctx, prepared[0].desired.Rule.Scope) if err != nil { return err } type positionedDelete struct { position int change filter.DesiredChange } positioned := make([]positionedDelete, 0, len(prepared)) for _, item := range prepared { observed, observeErr := filter.ManagedObserved(snapshot, item.desired) if observeErr != nil { if errors.Is(observeErr, filter.ErrRuleStale) { missing, mergeErr := managedFirewallRuleMissing(snapshot, item.desired) if mergeErr != nil { return mergeErr } if missing { continue } } return observeErr } if observed.Locator.Position == nil { return fmt.Errorf("%w: managed native firewall rule has no position", filter.ErrRuleStale) } before := item.desired.Rule locator := observed.Locator positioned = append(positioned, positionedDelete{ position: *observed.Locator.Position, change: filter.DesiredChange{ Operation: filter.ChangeDelete, Before: &before, Locator: &locator, }, }) } sort.Slice(positioned, func(i, j int) bool { return positioned[i].position > positioned[j].position }) changes := make([]filter.DesiredChange, 0, len(positioned)) for _, item := range positioned { changes = append(changes, item.change) } var backendPlan filter.BackendPlan if len(changes) > 0 { var verification filter.VerifyResult backendPlan, verification, err = runtime.Execute(ctx, snapshot, changes) if err != nil { return err } if !verification.Matched { return filter.ErrVerificationFailed } } deleted := make([]model.FirewallRule, 0, len(prepared)) for _, item := range prepared { if err = s.rules.DeleteWithRevision(ctx, item.stored.UUID, item.stored.Revision); err != nil { if len(changes) > 0 { err = rollbackFirewallPlan(ctx, runtime, backendPlan, err) } return s.restoreDeletedFirewallRecords(ctx, deleted, err) } deleted = append(deleted, item.stored) } return nil } func supportsNativeRuleBatch(provider filter.Provider) bool { return provider == filter.ProviderIptables || provider == filter.ProviderNftables } func (s *FirewallService) restoreDeletedFirewallRecords( ctx context.Context, deleted []model.FirewallRule, cause error, ) error { restoreErrors := make([]error, 0) for index := range deleted { record := deleted[index] if err := s.rules.Create(ctx, &record); err != nil { restoreErrors = append(restoreErrors, fmt.Errorf("restore deleted firewall rule %q: %w", record.UUID, err)) } } if len(restoreErrors) == 0 { return cause } return errors.Join(append([]error{cause}, restoreErrors...)...) } func (s *FirewallService) Update(ctx context.Context, clientIP string, request dto.FirewallRuleUpdate) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() metadata := request.Description != nil || request.OrderIndex != nil || request.Priority != nil if (request.Rule != nil) == metadata || request.OrderIndex != nil && request.Priority != nil { return fmt.Errorf("%w: provide a rule or description/ordering fields", filter.ErrInvalidRule) } if request.Rule != nil { rule := *request.Rule rule.UUID = request.UUID return s.updateRule(ctx, clientIP, request.UUID, rule) } if request.OrderIndex != nil || request.Priority != nil { return s.updateRuleOrder(ctx, request.UUID, request.OrderIndex, request.Priority, request.Description) } return s.updateRuleDescription(ctx, request.UUID, *request.Description) } 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) Reorder(ctx context.Context, clientIP string, request dto.FirewallRuleReorder) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() return s.updateRuleOrder(ctx, request.UUID, request.TargetPosition, request.Priority, 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) Adopt(ctx context.Context, request dto.FirewallRuleAdopt) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() if err := s.checkSelectedProvider(ctx, request.Scope.Provider); err != nil { return err } runtime, err := s.adapters.Resolve(request.Scope.Provider) if err != nil { return err } snapshot, err := runtime.ObserveMutation(ctx, request.Scope) if err != nil { return err } observed, err := filter.FindCandidate(snapshot.Rules, request.InstanceKey) if err != nil { return filter.ErrRuleStale } return s.adoptRule(ctx, runtime, snapshot, observed, dto.FirewallRuleCreateItem{SourceKind: constant.FirewallRuleSourceUser}) } func (s *FirewallService) adoptRule(ctx context.Context, runtime *filterruntime.Engine, snapshot filter.Snapshot, observed filter.ObservedRule, source dto.FirewallRuleCreateItem) error { if isIptablesSystemPresetInventoryScope(observed.Rule.Scope) { 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 := runtime.Prepare(observed.Rule) if err != nil { return err } if err := runtime.CheckRule(ctx, rule); 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 err := filter.CheckAdoptDuplicates(snapshot, rule); err != nil { return err } 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 filter.ErrDuplicateAdoption } return err } if rule.Scope.Provider != filter.ProviderFirewalld { sequence, err := s.sequenceForCreatedFirewallRule(ctx, snapshot, rule) if err != nil { return err } record.Sequence = &sequence } record.UUID = uuid.NewString() rule.UUID = record.UUID plan, verification, err := runtime.Execute(ctx, snapshot, []filter.DesiredChange{{ 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.Snapshot, rule, plan); err != nil { return rollbackFirewallPlan(ctx, runtime, plan, err) } return s.saveFirewallRule(ctx, &record) } func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string) error { if ruleUUID == "" { return fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid) } stored, err := s.rules.GetByUUID(ctx, ruleUUID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: managed rule %q was not found", filter.ErrInvalidRule, ruleUUID) } return err } if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { return fmt.Errorf("%w: only created or adopted rules can be deleted", filter.ErrInvalidRule) } selected, err := s.selectedProviderForStoredRule(ctx, stored) if err != nil { return err } desiredRules, err := s.compileStoredFirewallRules(ctx, stored, selected) if err != nil { return err } if err := checkFirewallRuleWhitelistProtection(selected, stored); err != nil { return err } type appliedDelete struct { runtime *filterruntime.Engine plan filter.BackendPlan } applied := make([]appliedDelete, 0, len(desiredRules)) rollback := func(cause error) error { for index := len(applied) - 1; index >= 0; index-- { cause = rollbackFirewallPlan(ctx, applied[index].runtime, applied[index].plan, cause) } return cause } for _, desired := range desiredRules { runtime, runtimeErr := s.resolveRuntime(ctx, desired.Rule.Scope.Provider) if runtimeErr != nil { return rollback(runtimeErr) } snapshot, observeErr := runtime.ObserveMutation(ctx, desired.Rule.Scope) if observeErr != nil { return rollback(observeErr) } observed, managedErr := filter.ManagedObserved(snapshot, desired) if managedErr != nil { if errors.Is(managedErr, filter.ErrRuleStale) { missing, mergeErr := managedFirewallRuleMissing(snapshot, desired) if mergeErr != nil { return rollback(mergeErr) } if missing { continue } } return rollback(managedErr) } restoreAtEnd := false if desired.Rule.Scope.Provider == filter.ProviderUFW && observed.Locator.Position != nil { maxPosition := maxObservedFirewallPosition(snapshot) restoreAtEnd = int64(*observed.Locator.Position) == maxPosition } locator := observed.Locator before := desired.Rule backendPlan, verification, executeErr := runtime.Execute(ctx, snapshot, []filter.DesiredChange{{ Operation: filter.ChangeDelete, Before: &before, Locator: &locator, RestoreAtEnd: restoreAtEnd, }}) if executeErr != nil { return rollback(executeErr) } if !verification.Matched { return rollback(filter.ErrVerificationFailed) } applied = append(applied, appliedDelete{runtime: runtime, plan: backendPlan}) } if err := s.rules.DeleteWithRevision(ctx, stored.UUID, stored.Revision); err != nil { return rollback(err) } return nil } func managedFirewallRuleMissing(snapshot filter.Snapshot, desired filter.DesiredRule) (bool, error) { items, err := filter.MergeInventory(filter.InventoryMergeInput{ Observed: snapshot.Rules, Desired: []filter.DesiredRule{desired}, }) if err != nil { return false, err } for _, item := range items { if item.Desired != nil && item.Desired.UUID == desired.UUID { return item.Match == filter.InventoryMatchMissing && item.Observed == nil, nil } } return false, nil } func (s *FirewallService) updateRule(ctx context.Context, clientIP, ruleUUID string, requestedRule filter.FirewallRule) error { requestedRule, err := filter.NormalizeRule(requestedRule) if err != nil { return err } stored, err := s.rules.GetByUUID(ctx, ruleUUID) if err != nil { return err } previousRules, compileErr := stored.RulesForProvider(requestedRule.Scope.Provider) if compileErr == nil && len(previousRules) == 1 { sameContent, err := filter.SameRuleContent(previousRules[0], requestedRule) if err != nil { return err } if sameContent { if requestedRule.Scope.Provider == filter.ProviderFirewalld { beforePriority, afterPriority := 0, 0 if stored.Priority != nil { beforePriority = *stored.Priority } if requestedRule.Priority != nil { afterPriority = *requestedRule.Priority } if beforePriority != afterPriority { return s.updateRuleOrder(ctx, ruleUUID, nil, &afterPriority, &requestedRule.Description) } } else if requestedRule.OrderIndex != nil { return s.updateRuleOrder(ctx, ruleUUID, requestedRule.OrderIndex, nil, &requestedRule.Description) } return s.updateRuleDescription(ctx, ruleUUID, requestedRule.Description) } } prepared, err := s.prepareManagedUpdate(ctx, clientIP, ruleUUID, requestedRule) if err != nil { return err } metadataOnly, err := isFirewallMetadataOnlyUpdate(prepared.Before.Rule, prepared.After, prepared.Observed.Locator) if err != nil { return err } if metadataOnly { if prepared.After.Description == prepared.Before.Rule.Description { return nil } return s.rules.UpdateWithRevision(ctx, prepared.Stored.UUID, prepared.Stored.Revision, map[string]interface{}{ "description": prepared.After.Description, }) } return s.executeManagedMutation(ctx, managedMutationRequest{ Stored: prepared.Stored, Before: prepared.Before.Rule, After: prepared.After, Snapshot: prepared.Snapshot, Locator: prepared.Observed.Locator, AdapterOperation: filter.ChangeUpdate, Runtime: prepared.Runtime, }) } 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 (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, snapshot, observed, runtime, err := s.loadManagedMutation(ctx, ruleUUID) 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 err := runtime.ValidatePosition(ctx, 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 = runtime.Prepare(after) if err != nil { return err } if err := runtime.CheckRule(ctx, after); 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 err := filter.GuardMutation(observed); err != nil { return err } return s.executeManagedMutation(ctx, managedMutationRequest{ Stored: stored, Before: before.Rule, After: after, Snapshot: snapshot, Locator: observed.Locator, AdapterOperation: adapterOperation, Runtime: runtime, }) } type managedMutationRequest struct { Stored model.FirewallRule Before filter.FirewallRule After filter.FirewallRule Snapshot filter.Snapshot Locator filter.Locator AdapterOperation filter.ChangeOperation Runtime *filterruntime.Engine } type preparedManagedUpdate struct { Stored model.FirewallRule Before filter.DesiredRule After filter.FirewallRule Snapshot filter.Snapshot Observed filter.ObservedRule Runtime *filterruntime.Engine } func (s *FirewallService) prepareManagedUpdate( ctx context.Context, clientIP string, ruleUUID string, requestedRule filter.FirewallRule, ) (preparedManagedUpdate, error) { ruleUUID = strings.TrimSpace(ruleUUID) if ruleUUID == "" { return preparedManagedUpdate{}, fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid) } stored, before, snapshot, observed, runtime, err := s.loadManagedMutation(ctx, ruleUUID) if err != nil { return preparedManagedUpdate{}, err } after, err := filter.NormalizeRule(requestedRule) if err != nil { return preparedManagedUpdate{}, err } after.UUID = stored.UUID after, err = runtime.Prepare(after) if err != nil { return preparedManagedUpdate{}, err } if err := runtime.CheckRule(ctx, after); err != nil { return preparedManagedUpdate{}, err } if after.Scope.Key() != before.Rule.Scope.Key() { return preparedManagedUpdate{}, filter.ErrManagedScopeChange } if !supportsManagedNativeKindTransition(before.Rule, after) { return preparedManagedUpdate{}, fmt.Errorf("%w: native rule conversion requires an explicit workflow", filter.ErrUnsupportedScope) } capabilities, err := runtime.Capabilities(ctx) if err != nil { return preparedManagedUpdate{}, err } if capabilities.ExplicitPosition || capabilities.OwnedChains { if observed.Locator.Position == nil { return preparedManagedUpdate{}, fmt.Errorf("%w: managed rule has no positional locator", filter.ErrInvalidRule) } currentPosition := int64(*observed.Locator.Position) if after.OrderIndex == nil { after.OrderIndex = ¤tPosition } else if *after.OrderIndex != currentPosition { if err := runtime.ValidatePosition(ctx, snapshot, before.Rule, *after.OrderIndex); err != nil { return preparedManagedUpdate{}, err } } } if err := filter.GuardMutation(observed); err != nil { return preparedManagedUpdate{}, err } if err := s.checkManagedMutationCollisions(ctx, before.Rule, after, snapshot, observed.Locator, stored.UUID); err != nil { return preparedManagedUpdate{}, err } return preparedManagedUpdate{ Stored: stored, Before: before, After: after, Snapshot: snapshot, Observed: observed, Runtime: runtime, }, nil } func supportsManagedNativeKindTransition(before, after filter.FirewallRule) bool { if before.NativeKind == after.NativeKind { return true } if before.Scope.Key() != after.Scope.Key() || before.Scope.Provider != filter.ProviderFirewalld { return false } return before.NativeKind == filter.NativeKindZonePort && after.NativeKind == filter.NativeKindRichRule || before.NativeKind == filter.NativeKindRichRule && after.NativeKind == filter.NativeKindZonePort } func (s *FirewallService) loadManagedMutation( ctx context.Context, ruleUUID string, ) (model.FirewallRule, filter.DesiredRule, filter.Snapshot, filter.ObservedRule, *filterruntime.Engine, error) { stored, err := s.rules.GetByUUID(ctx, ruleUUID) if err != nil { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, 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{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } if err := checkFirewallRuleWhitelistProtection(selected, stored); err != nil { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } desiredRules, err := s.compileStoredFirewallRules(ctx, stored, selected) if err != nil { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } if len(desiredRules) != 1 { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, fmt.Errorf("%w: policy %q expands to %d target rules and cannot be edited atomically", filter.ErrUnsupportedScope, ruleUUID, len(desiredRules)) } desired := desiredRules[0] runtime, err := s.resolveRuntime(ctx, desired.Rule.Scope.Provider) if err != nil { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } snapshot, err := runtime.ObserveMutation(ctx, desired.Rule.Scope) if err != nil { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } observed, err := filter.ManagedObserved(snapshot, desired) if err != nil { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } return stored, desired, snapshot, observed, 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 := s.adapters.Providers() if len(providers) == 1 { return providers[0], nil } } return "", fmt.Errorf("%w: selected provider is unavailable", filter.ErrProviderUnavailable) } func (s *FirewallService) executeManagedMutation(ctx context.Context, request managedMutationRequest) error { before, after := request.Before, request.After appendRule, restoreAtEnd := false, false if after.Scope.Provider == filter.ProviderUFW && (request.AdapterOperation == filter.ChangeUpdate || request.AdapterOperation == filter.ChangeReorder) { maxPosition := maxObservedFirewallPosition(request.Snapshot) appendRule = after.OrderIndex != nil && *after.OrderIndex == maxPosition restoreAtEnd = request.Locator.Position != nil && int64(*request.Locator.Position) == maxPosition } backendPlan, verification, err := request.Runtime.Execute(ctx, request.Snapshot, []filter.DesiredChange{{ Operation: request.AdapterOperation, Before: &before, After: &after, Locator: &request.Locator, Append: appendRule, RestoreAtEnd: restoreAtEnd, }}) if err != nil { return err } if !verification.Matched { return filter.ErrVerificationFailed } _, err = filter.FindCommittedObserved(verification.Snapshot, request.After, backendPlan) if err != nil { return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, err) } updates, err := firewallRuleSemanticUpdates(request.After) if err != nil { return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, err) } if request.After.Scope.Provider == filter.ProviderFirewalld { updates["sequence"] = nil } else { position, positionErr := firewallRuleMarkerPosition(verification.Snapshot, request.Stored.UUID) if positionErr != nil { return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, positionErr) } sequence, sequenceErr := s.sequenceForFirewallRulePosition( ctx, verification.Snapshot, position, request.Stored.UUID, request.Stored.Sequence, ) if sequenceErr != nil { return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, sequenceErr) } updates["sequence"] = sequence } if err := s.rules.UpdateWithRevision(ctx, request.Stored.UUID, request.Stored.Revision, updates); err != nil { return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, err) } return nil } func maxObservedFirewallPosition(snapshot filter.Snapshot) int64 { var maximum int64 for _, observed := range snapshot.Rules { if observed.Locator.Position != nil && int64(*observed.Locator.Position) > maximum { maximum = int64(*observed.Locator.Position) } } return maximum } func (s *FirewallService) checkManagedMutationCollisions( ctx context.Context, before, after filter.FirewallRule, snapshot filter.Snapshot, locator filter.Locator, excludedUUID string, ) error { sameContent, err := filter.SameRuleContent(before, after) if err != nil { return err } if sameContent { return nil } if err := filter.CheckObservedRuleCollisions(snapshot, after, &locator); err != nil { return err } return s.ensureFirewallRuleIdentityAvailable(ctx, after, excludedUUID) } func (s *FirewallService) ensureFirewallRuleIdentityAvailable(ctx context.Context, requested filter.FirewallRule, excludedUUID string) error { stored, err := s.rules.List(ctx) if err != nil { return err } identities, err := firewallRuleCollisions(stored, requested.Scope.Provider, excludedUUID) if err != nil { return err } return identities.Check(requested) } func firewallRuleCollisions(stored []model.FirewallRule, provider filter.Provider, excludedUUID string) (filter.RuleCollisionIndex, error) { identities := make(filter.RuleCollisionIndex, len(stored)) for _, candidate := range stored { if candidate.UUID == excludedUUID { continue } rules, err := candidate.RulesForProvider(provider) if err != nil { continue } for _, rule := range rules { if err := identities.Add(rule); err != nil { return nil, err } } } return identities, nil } func (s *FirewallService) resolveRuntime(ctx context.Context, provider filter.Provider) (*filterruntime.Engine, 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) } } if s.adapters == nil { return nil, filter.ErrAdapterUnavailable } return s.adapters.Resolve(provider) } func (s *FirewallService) ensureSystemPort(ctx context.Context, port dto.FirewallSystemPort) error { firewallRuleMutationMu.Lock() err := s.ensureSystemPortLocked(ctx, port) firewallRuleMutationMu.Unlock() if err == nil { return nil } if errors.Is(err, filter.ErrInventoryUnavailable) { err = s.appendUFWSystemPortUnverified(ctx, port, err) } if port.Family == constant.FirewallFamilyIPv6 && filterufw.IsIPv6Unavailable(err) { if global.LOG != nil { global.LOG.Warnf("skip accepted UFW IPv6 port %s/%s: %v", port.Port, port.Protocol, err) } return nil } return err } func (s *FirewallService) appendUFWSystemPortUnverified( ctx context.Context, port dto.FirewallSystemPort, cause error, ) error { if s.selectedProvider == nil || s.adapters == nil { return cause } provider, providerErr := s.selectedProvider(ctx) if providerErr != nil { return errors.Join(cause, providerErr) } if provider != filter.ProviderUFW { return cause } if global.LOG != nil { global.LOG.Warnf( "UFW inventory is unavailable while restoring accepted port %s/%s; attempting a restricted direct allow: %v", port.Port, port.Protocol, cause, ) } runtime, resolveErr := s.adapters.Resolve(provider) if resolveErr != nil { return errors.Join(cause, resolveErr) } comment := "1panel-system-port:" + systemPortKey(port) if appendErr := runtime.AppendUnverified(ctx, systemPortRule(provider, port), comment); appendErr != nil { if global.LOG != nil { global.LOG.Errorf( "restore accepted UFW port %s/%s without rule inventory failed: %v; original error: %v", port.Port, port.Protocol, appendErr, cause, ) } return errors.Join(cause, fmt.Errorf("append accepted UFW port without rule inventory: %w", appendErr)) } if global.LOG != nil { global.LOG.Warnf( "restored accepted UFW port %s/%s without rule inventory; normal rule management failed: %v", port.Port, port.Protocol, cause, ) } return nil } func (s *FirewallService) ensureSystemPortLocked(ctx context.Context, port dto.FirewallSystemPort) error { provider, err := s.selectedProvider(ctx) if err != nil { return err } source := dto.FirewallRuleCreateItem{ Rule: systemPortRule(provider, port), SourceKind: constant.FirewallRuleSourceSecurity, SourceID: systemPortSourceID(port), } prepared, err := s.prepareCreate(ctx, provider, source) if err != nil { return err } rule, runtime := prepared.request.Rule, prepared.runtime snapshot, err := runtime.ObserveMutation(ctx, rule.Scope) if err != nil { return err } stored, err := s.rules.List(ctx) if err != nil { return err } matchKey, err := filter.RuleMatchKey(rule) if err != nil { return err } for _, record := range stored { candidates, err := record.RulesForProvider(provider) if err != nil { continue } matched := false for _, candidate := range candidates { key, err := filter.RuleMatchKey(candidate) if err != nil { return err } if key == matchKey { matched = true break } } if !matched { continue } if record.Action != string(rule.Action) { return filter.ErrRuleConflict } desired, err := s.compileStoredFirewallRules(ctx, record, provider) if err != nil { return err } items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) if err != nil { return err } for _, item := range items { if item.Desired != nil && item.Match == filter.InventoryMatchExact && item.State != filter.InventoryStateDrifted && item.Observed != nil { return nil } } return filter.ErrRuleStale } matches, err := filter.MatchObservedByRuleKey(snapshot.Rules, rule) if err != nil { return err } if len(matches) > 0 { if matches[0].Protected { if matches[0].Persistence != "" && matches[0].Persistence != filter.PersistenceStatusConverged { return filter.ErrRuleStale } return nil } return s.adoptRule(ctx, runtime, snapshot, matches[0], source) } _, err = s.createRules(ctx, dto.FirewallRuleCreate{Items: []dto.FirewallRuleCreateItem{source}}, nil) return err } func hasSystemFirewallRuleOwner(rule model.FirewallRule) bool { acceptedPrefix := model.FirewallRuleOwner( constant.FirewallRuleSourceSecurity, constant.FirewallSystemAcceptedPortSourcePrefix, ) return strings.HasPrefix(rule.Owner, acceptedPrefix) } func systemPortRule(provider filter.Provider, port dto.FirewallSystemPort) filter.FirewallRule { return firewall.RuleForSystemPort(provider, firewall.SystemPort(port)) } func systemPortKey(port dto.FirewallSystemPort) string { return firewall.SystemPortKey(firewall.SystemPort(port)) } func systemPortSourceID(port dto.FirewallSystemPort) string { return constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port) } func firewallRuleModelForCreate(rule filter.FirewallRule, request dto.FirewallRuleCreateItem, origin string) (model.FirewallRule, error) { record, err := model.FirewallRuleFromDomain(rule) if err != nil { return model.FirewallRule{}, err } record.Origin = origin record.Owner = model.FirewallRuleOwner(request.SourceKind, request.SourceID) return record, nil } func firewallRuleSemanticUpdates(rule filter.FirewallRule) (map[string]interface{}, error) { record, err := model.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": "", "priority": record.Priority, }, nil } func (s *FirewallService) nextFirewallRuleSequence(ctx context.Context) (int64, error) { stored, err := s.rules.List(ctx) if err != nil { return 0, err } var maximum int64 for _, record := range stored { if record.Sequence != nil && *record.Sequence > maximum { maximum = *record.Sequence } } return maximum + model.FirewallRuleSequenceStep, nil } func (s *FirewallService) sequenceForCreatedFirewallRule( ctx context.Context, snapshot filter.Snapshot, rule filter.FirewallRule, ) (int64, error) { if rule.OrderIndex == nil { return s.nextFirewallRuleSequence(ctx) } return s.sequenceForFirewallRulePosition(ctx, snapshot, int(*rule.OrderIndex), "", nil) } func (s *FirewallService) sequenceForFirewallRulePosition( ctx context.Context, snapshot filter.Snapshot, targetPosition int, excludedUUID string, current *int64, ) (int64, error) { stored, err := s.rules.List(ctx) if err != nil { return 0, err } byUUID := make(map[string]model.FirewallRule, len(stored)) for _, record := range stored { byUUID[record.UUID] = record compiled, err := s.compileStoredFirewallRules(ctx, record, snapshot.Scope.Provider) if err != nil { if isFirewallPolicyIncompatible(err) { continue } return 0, err } for _, desired := range compiled { if desired.Rule.Scope.Key() == snapshot.Scope.Key() { byUUID[desired.Rule.UUID] = record } } } var previous, next *model.FirewallRule for _, observed := range snapshot.Rules { uuid := strings.TrimPrefix(observed.Marker, "1panel-rule:") if observed.Marker == uuid || uuid == excludedUUID || observed.Locator.Position == nil { continue } record, exists := byUUID[uuid] if !exists || record.UUID == excludedUUID { continue } position := *observed.Locator.Position if position < targetPosition { copy := record previous = © } else if position > targetPosition || excludedUUID == "" { copy := record next = © break } } if previous != nil && previous.Sequence == nil || next != nil && next.Sequence == nil { return s.rebalanceFirewallRuleSequences(ctx, snapshot, targetPosition, excludedUUID, byUUID) } if current != nil && (previous == nil || *previous.Sequence < *current) && (next == nil || *current < *next.Sequence) { return *current, nil } switch { case previous == nil && next == nil: return model.FirewallRuleSequenceStep, nil case previous == nil: return *next.Sequence - model.FirewallRuleSequenceStep, nil case next == nil: return *previous.Sequence + model.FirewallRuleSequenceStep, nil case *next.Sequence-*previous.Sequence > 1: return *previous.Sequence + (*next.Sequence-*previous.Sequence)/2, nil default: return s.rebalanceFirewallRuleSequences(ctx, snapshot, targetPosition, excludedUUID, byUUID) } } func (s *FirewallService) rebalanceFirewallRuleSequences( ctx context.Context, snapshot filter.Snapshot, targetPosition int, excludedUUID string, byUUID map[string]model.FirewallRule, ) (int64, error) { targetSequence := int64(targetPosition) * model.FirewallRuleSequenceStep updated := make(map[string]bool) for _, observed := range snapshot.Rules { if observed.Locator.Position == nil { continue } uuid := strings.TrimPrefix(observed.Marker, "1panel-rule:") if observed.Marker == uuid || uuid == excludedUUID { continue } record, exists := byUUID[uuid] if !exists || record.UUID == excludedUUID || updated[record.UUID] { continue } updated[record.UUID] = true position := *observed.Locator.Position if excludedUUID == "" && position >= targetPosition { position++ } sequence := int64(position) * model.FirewallRuleSequenceStep if record.Sequence != nil && *record.Sequence == sequence { continue } if err := s.rules.UpdateWithRevision(ctx, record.UUID, record.Revision, map[string]interface{}{ "sequence": sequence, }); err != nil { return 0, err } } return targetSequence, nil } func firewallRuleMarkerPosition(snapshot filter.Snapshot, ruleUUID string) (int, error) { marker := "1panel-rule:" + ruleUUID for _, observed := range snapshot.Rules { if observed.Marker == marker && observed.Locator.Position != nil { return *observed.Locator.Position, nil } } return 0, fmt.Errorf("%w: committed firewall rule %q has no position", filter.ErrVerificationFailed, ruleUUID) } func (s *FirewallService) compileStoredFirewallRules( ctx context.Context, stored model.FirewallRule, target filter.Provider, ) ([]filter.DesiredRule, error) { rules, err := stored.RulesForProvider(target) if err != nil { return nil, err } runtime, err := s.adapters.Resolve(target) if err != nil { return nil, err } return runtime.CompileDesired(ctx, stored.UUID, filter.RuleOrigin(stored.Origin), rules) } 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 (s *FirewallService) compileRestorableFirewallRules( ctx context.Context, stored model.FirewallRule, provider filter.Provider, ) (restorable, preserved []filter.DesiredRule, err error) { compiled, err := s.compileStoredFirewallRules(ctx, stored, provider) if err != nil { return nil, nil, err } if !supportsManagedFilterChains(string(provider)) || !hasSystemFirewallRuleOwner(stored) { return compiled, nil, nil } loadRequired := s.requiredPorts if loadRequired == nil { loadRequired = LoadRequiredFirewallPortWhiteList } required, err := loadRequired() if err != nil { return nil, nil, err } requiredPorts := firewall.ExpandPortWhitelist(required) for _, desired := range compiled { covered := false for _, port := range requiredPorts { covered, err = filter.SameRuleContent(desired.Rule, systemPortRule(provider, 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) desiredFirewallRulesByScope( ctx context.Context, stored []model.FirewallRule, provider filter.Provider, ) (map[string][]filter.DesiredRule, []filter.InventoryItem) { model.SortFirewallRules(stored, provider) desired := make(map[string][]filter.DesiredRule) var failures []filter.InventoryItem ports, protectionErr := loadFirewallPortWhiteList() for _, record := range stored { compiled, _, err := s.compileRestorableFirewallRules(ctx, record, provider) 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 || filter.RuleMatchesPortWhitelist(rule, ports)}, Error: fmt.Sprintf("policy %s: %v", record.UUID, err), }) continue } for _, rule := range compiled { rule.Protected = filter.RuleMatchesPortWhitelist(rule.Rule, ports) rule.Expanded = len(compiled) > 1 key := rule.Rule.Scope.Key() desired[key] = append(desired[key], rule) } } return desired, failures } func firewallRuleSnapshotPolicy(ctx context.Context, snapshot filter.Snapshot) (filter.Snapshot, error) { ports, err := loadFirewallPortWhiteList() if err != nil { return filter.Snapshot{}, err } return filter.ProtectSnapshot(snapshot, ports) } func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) { provider, err := selectedSystemFirewallProvider() if err != nil { return "", fmt.Errorf("%w: %v", filter.ErrProviderUnavailable, err) } return filter.Provider(provider), nil } func rollbackFirewallPlan(ctx context.Context, runtime *filterruntime.Engine, plan filter.BackendPlan, cause error) error { if runtime == nil { return cause } if err := runtime.Rollback(ctx, plan); err != nil { return errors.Join(cause, fmt.Errorf("rollback applied firewall plan: %w", err)) } return cause } func ensureFirewallPorts(ports []int) error { filterruntime.InvalidateInventory() defer filterruntime.InvalidateInventory() client, err := selectedSystemFirewallClient() if err != nil { return err } state, err := lifecycle.LoadState(client) if err != nil { return err } if state.Name == constant.FirewallProviderIptables || state.Name == constant.FirewallProviderNftables { isInit, _, err := loadFirewallInitStatus(state.Name, "base") if err != nil { return err } if !isInit { return nil } } else if !state.IsActive { return nil } added := make([]firewall.PortWhitelist, 0, len(ports)) for _, port := range ports { added = append(added, firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"}) } normalized, err := firewall.NormalizeSystemPorts(firewall.ExpandPortWhitelist(added)) if err != nil { return err } service := newFirewallService() var failures []error for _, key := range firewall.SortedSystemPortKeys(normalized) { if err := service.ensureSystemPort(context.Background(), normalized[key]); err != nil { wrapped := fmt.Errorf("allow firewall port %s: %w", key, err) if supportsNativeRuleBatch(filter.Provider(state.Name)) { return wrapped } failures = append(failures, wrapped) if global.LOG != nil { global.LOG.Errorf("%v", wrapped) } } } return errors.Join(failures...) } 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 loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) { ports, err := loadPortWhitelistSetting(global.DB) if err != nil { return nil, err } return firewall.ValidatePortWhitelist(ports) } func LoadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) { ports, err := loadFirewallPortWhiteList() if err != nil { return nil, err } return firewall.RequiredPortWhitelist(ports) } func newIptablesHelperManager() *iptables_helper.Manager { return &iptables_helper.Manager{ UpdateSetting: settingRepo.Update, LoadRequiredPorts: LoadRequiredFirewallPortWhiteList, } } func newNftablesHelperManager() *nftables_helper.Manager { return &nftables_helper.Manager{ UpdateSetting: settingRepo.Update, LoadRequiredPorts: LoadRequiredFirewallPortWhiteList, } } 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 supportsManagedFilterChains(provider string) bool { return provider == constant.FirewallProviderIptables || provider == constant.FirewallProviderNftables } func (s *FirewallService) restoreFirewallAfterStart(client lifecycle.Client) error { ctx := context.Background() provider := filter.Provider(client.Name()) var recoveryErrors []error recordFailure := func(stage string, err error) { if err == nil { return } wrapped := fmt.Errorf("%s for %s: %w", stage, provider, err) recoveryErrors = append(recoveryErrors, wrapped) if global.LOG != nil { global.LOG.Errorf("firewall post-start recovery failed: %v", wrapped) } } if provider == filter.ProviderIptables || provider == filter.ProviderNftables { isInit, _, err := loadFirewallInitStatus(string(provider), "base") if err != nil { recordFailure("load managed chain status", err) return errors.Join(recoveryErrors...) } if !isInit { return nil } } if err := s.restoreStoredFirewallRules(ctx, provider, nil); err != nil { recordFailure("restore stored firewall rules", err) } recordFailure("restore whitelist allowances", s.SyncPortWhitelist(ctx)) return errors.Join(recoveryErrors...) } func AdoptLegacyHostFirewallRuleOwnership(ctx context.Context) error { return newFirewallService().adoptLegacyHostFirewallRuleOwnership(ctx) } func (s *FirewallService) adoptLegacyHostFirewallRuleOwnership(ctx context.Context) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() selected, err := s.selectedProvider(ctx) if err != nil { return err } if selected != filter.ProviderIptables && selected != filter.ProviderUFW { return fmt.Errorf("%w: selected provider %s does not require legacy ownership transfer", filter.ErrProviderUnavailable, selected) } runtime, err := s.adapters.Resolve(selected) if err != nil { return err } stored, err := s.rules.List(ctx) if err != nil { return err } desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, selected) if len(failures) > 0 { return errors.New(failures[0].Error) } for _, scope := range filter.ManagedInputScopes(selected) { desired := desiredByScope[scope.Key()] if len(desired) == 0 { continue } snapshot, err := runtime.ObserveMutation(ctx, scope) if err != nil { return err } for { items, err := filter.MergeInventory(filter.InventoryMergeInput{ Observed: snapshot.Rules, Desired: desired, }) if err != nil { return err } var candidate *filter.InventoryItem for index := range items { item := &items[index] if item.Match != filter.InventoryMatchChanged || item.Desired == nil || item.Observed == nil || item.Desired.Origin != filter.RuleOriginAdopted || strings.TrimSpace(item.Desired.Marker) == "" || strings.TrimSpace(item.Observed.Marker) != "" || item.Observed.Protected || !filter.ObservedRuleMatchesExpected(*item.Observed, item.Desired.Rule) { continue } candidate = item break } if candidate == nil { break } after := candidate.Desired.Rule before := firewallsync.ObservedRule(*candidate.Observed) locator := candidate.Observed.Locator _, verification, err := runtime.Execute(ctx, snapshot, []filter.DesiredChange{{ Operation: filter.ChangeAdopt, Before: &before, After: &after, Locator: &locator, PreviousMarker: candidate.Observed.Marker, }}) if err != nil { return err } if !verification.Matched { return filter.ErrVerificationFailed } snapshot = verification.Snapshot } } if selected == filter.ProviderIptables { if err := iptables_helper.CleanupLegacyAdvancedChains(ctx); err != nil { return err } } return nil }