diff --git a/agent/app/api/v2/firewall.go b/agent/app/api/v2/firewall.go index d2b3b059af81..40fd43bdef0a 100644 --- a/agent/app/api/v2/firewall.go +++ b/agent/app/api/v2/firewall.go @@ -60,7 +60,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) { // @Summary Operate firewall // @Accept json // @Param request body dto.FirewallLifecycleOperation true "request" -// @Success 200 +// @Success 200 {object} dto.FirewallLifecycleOperationResponse // @Security ApiKeyAuth // @Security Timestamp // @Router /hosts/firewall/operate [post] @@ -71,12 +71,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) { return } - if err := firewallService.OperateFirewall(request); err != nil { + result, err := firewallService.QueueFirewallOperation(request) + if err != nil { helper.InternalServer(c, err) return } - helper.Success(c) + helper.SuccessWithData(c, result) } // @Tags Firewall diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index e1471c048b0d..da4434748cc2 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -18,6 +18,7 @@ type FirewallSubsystemStatus struct { Message string `json:"message,omitempty"` Reason string `json:"reason,omitempty"` SyncError string `json:"syncError,omitempty"` + LifecycleTaskID string `json:"lifecycleTaskID,omitempty"` IPv4 FirewallBackendFamilyStatus `json:"ipv4"` IPv6 FirewallBackendFamilyStatus `json:"ipv6"` } @@ -27,6 +28,11 @@ type FirewallLifecycleOperation struct { WithDockerRestart bool `json:"withDockerRestart"` } +type FirewallLifecycleOperationResponse struct { + TaskID string `json:"taskID,omitempty"` + Queued bool `json:"queued"` +} + type FirewallBackendOption struct { Name string `json:"name"` Installed bool `json:"installed"` diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index 5f729d2d857e..53ecf92010f7 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -9,11 +9,13 @@ import ( "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" @@ -55,10 +57,16 @@ type firewallRuleRuntimeResolver interface { var firewallRuleMutationMu sync.Mutex +var ( + firewallLifecycleTaskMu sync.Mutex + firewallLifecycleTaskID string + firewallLifecycleRequest dto.FirewallLifecycleOperation +) + type IFirewallService interface { UpdatePanelPort(context.Context, uint, uint) error LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) - OperateFirewall(request dto.FirewallLifecycleOperation) 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) @@ -103,6 +111,7 @@ func newFirewallService() *FirewallService { 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 } @@ -163,6 +172,157 @@ func (c firewallLifecycleClient) Restart() error { 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.OperationRestart)) { + return response, s.OperateFirewall(request) + } + operation, label := task.TaskExec, "Start" + if request.Operation == string(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 { + ports, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + return s.SyncSystemPorts(ctx, nil, systemPorts(ports)) + }) + return errors.Join(rulesErr, whitelistErr) + }) + 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 = func() (bool, error) { return controller.CheckActive("docker") } + } + 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": @@ -248,7 +408,7 @@ func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) e return nil } ctx := context.Background() - if err := s.restoreStoredFirewallRules(ctx, filter.Provider(provider)); err != nil { + if err := s.restoreStoredFirewallRules(ctx, filter.Provider(provider), nil); err != nil { return err } return s.syncConfiguredFirewallPorts(ctx) @@ -280,7 +440,7 @@ func (s *FirewallService) QueueFilterChainInitialization( return s.operateFilterChainBase(provider, request) }, nil) taskItem.AddSubTask(i18n.GetWithName("FirewallRestoreRulesStep", provider), func(t *task.Task) error { - return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider)) + return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider), nil) }, nil) taskItem.AddSubTask(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func(t *task.Task) error { return s.syncConfiguredFirewallPorts(t.TaskCtx) @@ -318,6 +478,10 @@ func (s *FirewallService) operateFilterChainBaseLocked(provider string, request } func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) { + if err := lockFirewallLifecycleIdle(); err != nil { + return dto.FirewallRuleResetResponse{}, err + } + defer firewallLifecycleTaskMu.Unlock() firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() @@ -2791,7 +2955,7 @@ func (s *FirewallService) addPortsBeforeStart(client lifecycle.Client) error { return nil } } - if err := s.restoreStoredFirewallRules(ctx, provider); err != nil { + if err := s.restoreStoredFirewallRules(ctx, provider, nil); err != nil { recordFailure("restore stored firewall rules", err) } if provider == filter.ProviderIptables || provider == filter.ProviderNftables { diff --git a/agent/app/service/firewall_setting.go b/agent/app/service/firewall_setting.go index 599c34cf674f..5efebf8d6009 100644 --- a/agent/app/service/firewall_setting.go +++ b/agent/app/service/firewall_setting.go @@ -372,6 +372,10 @@ func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFa } func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error { + if err := lockFirewallLifecycleIdle(); err != nil { + return err + } + defer firewallLifecycleTaskMu.Unlock() if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables { return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem) } @@ -385,7 +389,7 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa } if request.Operation == "initialize" { service := newFirewallService() - if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend)); err != nil { + if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil); err != nil { return err } return service.syncConfiguredFirewallPorts(ctx) diff --git a/agent/app/service/firewall_sync.go b/agent/app/service/firewall_sync.go index 05f64c02618b..3214f002099f 100644 --- a/agent/app/service/firewall_sync.go +++ b/agent/app/service/firewall_sync.go @@ -195,6 +195,7 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F created, removed, unexecuted := 0, 0, 0 stopped := make(map[string]error) failedRemovals := make(map[string]error) + var firewalldFinalSnapshot *filter.Snapshot record := func(operation string, rule *firewallSyncRule, cause error, skipped bool) { item := rule.FirewallRuleSyncItem if operation == "TaskDelete" && rule.observed != nil { @@ -234,13 +235,15 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F } if skipped { t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause) + } else if operation == task.TaskSync && cause == nil { + t.Logf("%s %s", label, i18n.GetMsgByKey("FirewallSyncRuleUnchanged")) } else { t.LogWithStatus(label, cause) } } defer func() { if t != nil { - t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted})) + t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted, "unchanged": result.Skipped - unexecuted})) } }() blocked := false @@ -287,6 +290,10 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F return syncObservedPosition(queue[i].observed) > syncObservedPosition(queue[j].observed) }) } + if scope.Provider == filter.ProviderFirewalld && operation == filter.ChangeCreate && len(failedRemovals) == 0 && stopped[scope.Key()] == nil { + firewalldFinalSnapshot = syncFirewalldCreates(ctx, runtime, initial, removed > 0, queue, t, record) + continue + } for start := 0; start < len(queue); { rule := queue[start] cause := stopped[scope.Key()] @@ -325,21 +332,21 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F changes = append(changes, change) } else { after := *entry.Rule - markers := make([]string, 0) - for _, candidate := range rules { - if candidate.Rule != nil && candidate.Rule.Scope.Key() == scope.Key() && candidate.desired.Marker != "" { - markers = append(markers, candidate.desired.Marker) - } - } after.OrderIndex = nil - if len(batch) == 1 { + if len(batch) == 1 && scope.Provider != filter.ProviderFirewalld { + markers := make([]string, 0) + for _, candidate := range rules { + if candidate.Rule != nil && candidate.Rule.Scope.Key() == scope.Key() && candidate.desired.Marker != "" { + markers = append(markers, candidate.desired.Marker) + } + } after.OrderIndex = firewallsync.InsertionPosition(snapshot, markers, entry.desired.Marker) } changes = append(changes, filter.DesiredChange{Operation: operation, After: &after, Append: scope.Provider == filter.ProviderUFW && after.OrderIndex == nil}) } } if err == nil { - err = runtime.ExecuteSync(ctx, snapshot, changes) + _, err = runtime.ExecuteSync(ctx, snapshot, changes) } for _, entry := range batch { record(name, entry, err, false) @@ -351,9 +358,141 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F } } } + if firewalldFinalSnapshot != nil && result.Failed == 0 && created > 0 { + verifyErr := verifyFirewalldSyncSnapshot(*firewalldFinalSnapshot, rules) + if t != nil { + t.LogWithStatus(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), verifyErr) + } + if verifyErr != nil { + return result, verifyErr + } + } return result, nil } +func syncFirewalldCreates( + ctx context.Context, + runtime *filterruntime.Engine, + initial filter.Snapshot, + refresh bool, + queue []*firewallSyncRule, + t *task.Task, + record func(string, *firewallSyncRule, error, bool), +) *filter.Snapshot { + if len(queue) == 0 { + return nil + } + snapshot := initial + snapshot.Rules = append([]filter.ObservedRule(nil), initial.Rules...) + indices := make(map[string]int) + indexRules := func() { + clear(indices) + for index, rule := range snapshot.Rules { + indices[rule.Locator.Canonical] = index + } + } + indexRules() + pending := make([]*firewallSyncRule, 0, len(queue)) + var readErr error + for index, entry := range queue { + if refresh { + snapshot, readErr = runtime.ObserveMutation(ctx, initial.Scope) + if readErr != nil { + record(task.TaskCreate, entry, readErr, false) + for _, remaining := range queue[index+1:] { + record(task.TaskCreate, remaining, readErr, true) + } + break + } + indexRules() + refresh = false + } + if t != nil { + t.Logf("[%d/%d] %s %s", index+1, len(queue), i18n.GetMsgByKey(task.TaskCreate), entry.SourceUUID) + } + after := *entry.Rule + after.OrderIndex = nil + applied, err := runtime.ExecuteSync(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &after}}) + if err == nil && len(applied.Applied) != 1 { + err = filter.ErrVerificationFailed + } + if err == nil { + observed := applied.Applied[0] + if position, exists := indices[observed.Locator.Canonical]; exists { + snapshot.Rules[position] = observed + } else { + indices[observed.Locator.Canonical] = len(snapshot.Rules) + snapshot.Rules = append(snapshot.Rules, observed) + } + snapshot, err = filter.NewSnapshot(snapshot.Scope, snapshot.Rules) + } + if err != nil { + record(task.TaskCreate, entry, err, false) + if firewallCreateUnavailable(err) { + for _, remaining := range queue[index+1:] { + record(task.TaskCreate, remaining, err, true) + } + break + } + refresh = true + continue + } + pending = append(pending, entry) + } + var actual filter.Snapshot + if readErr == nil { + actual, readErr = runtime.ObserveMutation(ctx, initial.Scope) + } + if readErr != nil { + for _, entry := range pending { + record(task.TaskCreate, entry, readErr, false) + } + return nil + } + states := firewalldRuleStates(actual) + for _, entry := range pending { + key, err := filter.RuleKey(*entry.Rule) + if err == nil && states[key] != 1 { + err = filter.ErrVerificationFailed + } + record(task.TaskCreate, entry, err, false) + } + return &actual +} + +func firewalldRuleStates(snapshot filter.Snapshot) map[string]int { + states := make(map[string]int, len(snapshot.Rules)) + for _, observed := range snapshot.Rules { + if observed.ParseStatus != filter.ParseStatusSupported || observed.Persistence != filter.PersistenceStatusConverged { + continue + } + if key, err := filter.RuleKey(observed.Rule); err == nil { + states[key]++ + } + } + return states +} + +func verifyFirewalldSyncSnapshot(snapshot filter.Snapshot, rules []*firewallSyncRule) error { + states := firewalldRuleStates(snapshot) + for _, entry := range rules { + if entry.Status == firewallsync.StatusRemove { + continue + } + if entry.Rule == nil { + return filter.ErrVerificationFailed + } + key, err := filter.RuleKey(*entry.Rule) + if err != nil { + return err + } + if states[key] != 1 { + return fmt.Errorf("%w: %s", filter.ErrVerificationFailed, entry.SourceUUID) + } + } + return nil +} + func syncObservedPosition(rule *filter.ObservedRule) int { if rule.Locator.Position != nil { return *rule.Locator.Position @@ -361,8 +500,8 @@ func syncObservedPosition(rule *filter.ObservedRule) int { return 0 } -func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider) error { - result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, nil) +func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider, t *task.Task) error { + result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, t) if err != nil { return fmt.Errorf("restore database firewall rules: %w", err) } @@ -393,6 +532,10 @@ func (s *FirewallService) syncSystemRules( clientIP string, request dto.FirewallRuleSyncRequest, ) (dto.FirewallRuleSyncResult, error) { + if err := lockFirewallLifecycleIdle(); err != nil { + return dto.FirewallRuleSyncResult{}, err + } + defer firewallLifecycleTaskMu.Unlock() firewallRuleSyncTaskMu.Lock() defer firewallRuleSyncTaskMu.Unlock() diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 04e8340e95af..5dc091a57e9e 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back' FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved' FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved' -FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed' +FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed, {{ .unchanged }} already matching (no changes needed)' +FirewallSyncRuleUnchanged: 'Already matching; no changes needed' FirewallSyncStep: 'Synchronize rules to {{ .name }}' FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize' FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 962252bf5075..089f1b26c4fa 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados' FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión' FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión' -FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar' +FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar, {{ .unchanged }} ya coinciden (sin cambios necesarios)' +FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios' FirewallSyncStep: 'Sincronizar reglas con {{ .name }}' FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall' FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}' diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index af07bc6e4cc6..5789f6bc94ad 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند' FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود' FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد' -FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده' +FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)' +FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست' FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}' FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود' FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 153753e3ac69..a73b8d9f0c8f 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません' FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました' FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました' -FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件' +FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件' +FirewallSyncRuleUnchanged: '一致済み、変更不要' FirewallSyncStep: '{{ .name }} にルールを同期' FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした' FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index a9c268a04db8..1e2962b8f5e9 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다' FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다' FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다' -FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개' +FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개' +FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음' FirewallSyncStep: '{{ .name }}에 규칙 동기화' FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다' FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화' diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index ac2aaa44a8a7..6f897eecae4f 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -693,7 +693,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ' FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ' FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້' -FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}' +FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)' +FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ' FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}' FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ' FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 33febae6e3a5..45b6fcd9eff1 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan' FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan' FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan' -FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan' +FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)' +FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan' FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}' FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan' FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 885454506646..a06e39e62d77 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos' FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento' FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento' -FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas' +FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas, {{ .unchanged }} já correspondem (sem alterações necessárias)' +FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária' FirewallSyncStep: 'Sincronizar regras com {{ .name }}' FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização' FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 0779f56771a0..099d8a57d4aa 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены' FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления' FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось' -FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}' +FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}' +FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны' FirewallSyncStep: 'Синхронизировать правила с {{ .name }}' FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}' FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index a82a3f19e3b2..4f32768dd297 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı' FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi' FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi' -FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi' +FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)' +FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor' FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle' FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi' FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 4bb14ed1fba3..00269ccfde85 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復' FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗' FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗' -FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條' +FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條' +FirewallSyncRuleUnchanged: '已一致,無需變更' FirewallSyncStep: '同步規則到 {{ .name }}' FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗' FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index c3c9c9964cb1..c1260ef08c0e 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚' FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败' FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败' -FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条' +FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条' +FirewallSyncRuleUnchanged: '已一致,无需变更' FirewallSyncStep: "同步规则到 {{ .name }}" FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败" FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}" diff --git a/agent/utils/firewall/filter/providers/firewalld/adapter.go b/agent/utils/firewall/filter/providers/firewalld/adapter.go index 940efba1a301..1e28e0030624 100644 --- a/agent/utils/firewall/filter/providers/firewalld/adapter.go +++ b/agent/utils/firewall/filter/providers/firewalld/adapter.go @@ -409,13 +409,17 @@ func nativeCanonical(rule filter.FirewallRule) string { func missingRuleCommands(snapshot filter.Snapshot, rule filter.FirewallRule) ([]filter.NativeCommand, []filter.NativeCommand) { commands, rollback := pairedCommands(rule, "add", "remove") + canonical := nativeCanonical(rule) var runtimeExists, permanentExists bool for _, observed := range snapshot.Rules { - if observed.Locator.Canonical != nativeCanonical(rule) { + if observed.Locator.Canonical != canonical { continue } runtimeExists = runtimeExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusRuntimeOnly permanentExists = permanentExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusPermanentOnly + if runtimeExists && permanentExists { + break + } } var changes, inverses []filter.NativeCommand for index, exists := range []bool{runtimeExists, permanentExists} { diff --git a/agent/utils/firewall/filter/runtime/runtime.go b/agent/utils/firewall/filter/runtime/runtime.go index 3913ec88fee7..4980484c0109 100644 --- a/agent/utils/firewall/filter/runtime/runtime.go +++ b/agent/utils/firewall/filter/runtime/runtime.go @@ -249,9 +249,9 @@ func (e *Engine) ExecuteCreate(ctx context.Context, snapshot filter.Snapshot, ch return err } -func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error { +func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.ApplyResult, error) { if err := ctx.Err(); err != nil { - return err + return filter.ApplyResult{}, err } changes = append([]filter.DesiredChange(nil), changes...) for index := range changes { @@ -259,11 +259,10 @@ func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, chan } plan, err := e.adapter.Compile(snapshot, changes) if err != nil { - return err + return filter.ApplyResult{}, err } plan.CommandOnly = true - _, err = e.adapter.Apply(ctx, plan) - return err + return e.adapter.Apply(ctx, plan) } func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, filter.VerifyResult, error) { diff --git a/agent/utils/firewall/lifecycle/operator.go b/agent/utils/firewall/lifecycle/operator.go index 20152f9ca97c..185ec012c0b3 100644 --- a/agent/utils/firewall/lifecycle/operator.go +++ b/agent/utils/firewall/lifecycle/operator.go @@ -20,7 +20,8 @@ const ( ) type Operator struct { - client Client + client Client + RunAction func(operation, name string, action func() error) error } // DockerRestartError reports that the requested firewall operation completed, @@ -54,11 +55,18 @@ func NewOperator(client Client) *Operator { return &Operator{client: client} } +func (o *Operator) runAction(operation, name string, action func() error) error { + if o.RunAction != nil { + return o.RunAction(operation, name, action) + } + return action() +} + func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareStart func(Client) error) error { var recoveryErrors []error switch operation { case OperationStart: - if err := o.client.Start(); err != nil { + if err := o.runAction("Start", o.client.Name(), o.client.Start); err != nil { return err } if prepareStart != nil { @@ -69,7 +77,7 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS case OperationStop: return o.StopWithPrepare(withDockerRestart, nil) case OperationRestart: - if err := o.client.Restart(); err != nil { + if err := o.runAction("TaskRestart", o.client.Name(), o.client.Restart); err != nil { return err } if prepareStart != nil { @@ -82,12 +90,12 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS } if withDockerRestart { - if err := controller.HandleRestart("docker"); err != nil { + if err := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil { recoveryErrors = append(recoveryErrors, &DockerRestartError{Err: err}) } } if o.client.Name() == ProviderFirewalld && operation == OperationStart { - if err := restoreFail2BanAfterFirewallStart(); err != nil { + if err := o.runAction("TaskRecover", "Fail2Ban", restoreFail2BanAfterFirewallStart); err != nil { recoveryErrors = append(recoveryErrors, err) } } diff --git a/frontend/src/api/interface/firewall.ts b/frontend/src/api/interface/firewall.ts index 9ba4750357ab..c69dad2c1fd1 100644 --- a/frontend/src/api/interface/firewall.ts +++ b/frontend/src/api/interface/firewall.ts @@ -45,6 +45,7 @@ export namespace Firewall { queued?: boolean; } export interface FirewallBase { + lifecycleTaskID?: string; name: string; backend: string; conflictBackend?: string; diff --git a/frontend/src/api/modules/firewall.ts b/frontend/src/api/modules/firewall.ts index 47a7281ed7b0..31e6fa1a1053 100644 --- a/frontend/src/api/modules/firewall.ts +++ b/frontend/src/api/modules/firewall.ts @@ -16,7 +16,11 @@ export const searchForwardRule = (request: Firewall.ForwardRuleSearch) => http.post>('/hosts/firewall/forward/search', request, TimeoutEnum.T_40S); export const operateFire = (operation: string, withDockerRestart: boolean) => - http.post('/hosts/firewall/operate', { operation, withDockerRestart }, TimeoutEnum.T_10M); + http.post( + '/hosts/firewall/operate', + { operation, withDockerRestart }, + TimeoutEnum.T_10M, + ); export const operateForwardRule = (request: { rules: Firewall.RuleForward[]; forceDelete?: boolean }) => http.postWithConfig('/hosts/firewall/forward/operate', request, { diff --git a/frontend/src/views/host/firewall/status/index.vue b/frontend/src/views/host/firewall/status/index.vue index fb9537cf5f73..2ff0a1d5c8ed 100644 --- a/frontend/src/views/host/firewall/status/index.vue +++ b/frontend/src/views/host/firewall/status/index.vue @@ -69,6 +69,7 @@ v-node-admin type="primary" v-if="baseInfo.isActive" + :disabled="lifecycleBusy" @click="onOperate('stop')" link > @@ -79,14 +80,35 @@ :content="$t('firewall.firewallNotStart')" placement="bottom" > - + {{ $t('commons.button.start') }} - + {{ $t('commons.button.restart') }} +