From 2d7150621ee2470a4cd6ace6e8dff7db352c5424 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Thu, 10 Sep 2026 23:43:43 +0800 Subject: [PATCH] refactor(firewall): queue rule operations and simplify synchronization --- agent/app/api/v2/firewall.go | 46 +- agent/app/dto/firewall.go | 75 +- agent/app/dto/forwarding.go | 4 +- agent/app/model/firewall.go | 13 - agent/app/service/firewall.go | 1303 ++++++-------- agent/app/service/firewall_docker.go | 203 +-- agent/app/service/firewall_panel_port.go | 28 - agent/app/service/firewall_rule_task.go | 58 + agent/app/service/firewall_sync.go | 1552 +++++++---------- agent/app/service/forward.go | 110 +- agent/constant/firewall.go | 1 - agent/i18n/lang/en.yaml | 16 +- agent/i18n/lang/es-ES.yaml | 16 +- agent/i18n/lang/fa.yaml | 16 +- agent/i18n/lang/ja.yaml | 16 +- agent/i18n/lang/ko.yaml | 16 +- agent/i18n/lang/lo.yaml | 16 +- agent/i18n/lang/ms.yaml | 16 +- agent/i18n/lang/pt-BR.yaml | 16 +- agent/i18n/lang/ru.yaml | 16 +- agent/i18n/lang/tr.yaml | 16 +- agent/i18n/lang/zh-Hant.yaml | 16 +- agent/i18n/lang/zh.yaml | 16 +- agent/router/ro_host.go | 2 +- .../firewall/docker_guard/forward_policy.go | 42 + agent/utils/firewall/docker_guard/nftables.go | 15 +- agent/utils/firewall/filter/adapter.go | 29 +- agent/utils/firewall/filter/adopt.go | 72 - agent/utils/firewall/filter/check.go | 420 ----- agent/utils/firewall/filter/check_flag.go | 170 -- agent/utils/firewall/filter/identity.go | 17 + agent/utils/firewall/filter/inventory.go | 1 + .../filter/providers/firewalld/adapter.go | 43 +- .../filter/providers/iptables/adapter.go | 27 +- .../filter/providers/nftables/adapter.go | 125 +- .../firewall/filter/providers/ufw/adapter.go | 25 +- .../utils/firewall/filter/runtime/runtime.go | 39 +- agent/utils/firewall/filter/safety.go | 122 ++ .../firewall/forwarding/providers/iptables.go | 96 +- .../firewall/forwarding/providers/nftables.go | 5 +- agent/utils/firewall/iptables_helper/read.go | 29 + agent/utils/firewall/nftables_helper/read.go | 16 + agent/utils/firewall/sync/diff.go | 9 - agent/utils/firewall/sync/order.go | 151 +- core/cmd/server/docs/docs.go | 260 +-- core/cmd/server/docs/swagger.json | 260 +-- frontend/src/api/interface/firewall.ts | 43 +- frontend/src/api/modules/firewall.ts | 13 +- frontend/src/lang/modules/en.ts | 14 +- frontend/src/lang/modules/es-es.ts | 16 +- frontend/src/lang/modules/fa.ts | 14 +- frontend/src/lang/modules/ja.ts | 14 +- frontend/src/lang/modules/ko.ts | 13 +- frontend/src/lang/modules/lo.ts | 13 +- frontend/src/lang/modules/ms.ts | 14 +- frontend/src/lang/modules/pt-br.ts | 15 +- frontend/src/lang/modules/ru.ts | 14 +- frontend/src/lang/modules/tr.ts | 14 +- frontend/src/lang/modules/zh-Hant.ts | 13 +- frontend/src/lang/modules/zh.ts | 14 +- .../host/firewall/docker/detail/index.vue | 89 +- .../host/firewall/docker/import/index.vue | 59 +- .../src/views/host/firewall/docker/index.vue | 32 +- .../host/firewall/forward/import/index.vue | 40 +- .../src/views/host/firewall/forward/index.vue | 38 +- .../host/firewall/forward/operate/index.vue | 81 +- .../views/host/firewall/rule/import/index.vue | 113 +- .../src/views/host/firewall/rule/index.vue | 82 +- .../firewall/rule/operate/err-message.vue | 147 -- .../host/firewall/rule/operate/index.vue | 566 +----- .../src/views/host/firewall/sync/index.vue | 2 +- 71 files changed, 2592 insertions(+), 4441 deletions(-) create mode 100644 agent/app/service/firewall_rule_task.go create mode 100644 agent/utils/firewall/docker_guard/forward_policy.go delete mode 100644 agent/utils/firewall/filter/check.go delete mode 100644 agent/utils/firewall/filter/check_flag.go create mode 100644 agent/utils/firewall/iptables_helper/read.go create mode 100644 agent/utils/firewall/nftables_helper/read.go delete mode 100644 frontend/src/views/host/firewall/rule/operate/err-message.vue diff --git a/agent/app/api/v2/firewall.go b/agent/app/api/v2/firewall.go index f0a102d2c9ce..62460970557e 100644 --- a/agent/app/api/v2/firewall.go +++ b/agent/app/api/v2/firewall.go @@ -121,7 +121,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) { // @Summary Operate forwarding rules // @Accept json // @Param request body dto.ForwardRuleOperate true "request" -// @Success 200 +// @Success 200 {object} dto.FilterChainOperationResponse // @Security ApiKeyAuth // @Security Timestamp // @Router /hosts/firewall/forward/operate [post] @@ -132,11 +132,12 @@ func (b *BaseApi) OperateForwardingRules(c *gin.Context) { return } - if err := forwardingService.OperateRules(request); err != nil { + result, err := forwardingService.OperateRules(request) + if err != nil { helper.InternalServer(c, err) return } - helper.Success(c) + helper.SuccessWithData(c, result) } // @Tags Firewall @@ -259,29 +260,30 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) { } // @Tags Firewall -// @Summary Check unified firewall v2 rules for duplicates and conflicts +// @Summary Adopt an external firewall rule // @Accept json -// @Param request body dto.FirewallRuleCheck true "request" -// @Success 200 {object} dto.FirewallRuleCheckResponse +// @Param request body dto.FirewallRuleAdopt true "request" +// @Success 200 // @Failure 400 {object} dto.Response // @Security ApiKeyAuth // @Security Timestamp -// @Router /hosts/firewall/rules/check [post] -func (b *BaseApi) CheckFirewallRules(c *gin.Context) { - var request dto.FirewallRuleCheck +// @Router /hosts/firewall/rules/adopt [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"} +func (b *BaseApi) AdoptFirewallRule(c *gin.Context) { + var request dto.FirewallRuleAdopt if err := helper.CheckBindAndValidate(&request, c); err != nil { return } - result, err := firewallService.Check(c.Request.Context(), c.ClientIP(), request) - if err != nil { + if err := firewallService.Adopt(c.Request.Context(), request); err != nil { handleFirewallRuleError(c, err) return } - helper.SuccessWithData(c, result) + helper.Success(c) } // @Tags Firewall -// @Summary Create unified firewall v2 rules +// @Summary Queue firewall rule creation +// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log. // @Accept json // @Param request body dto.FirewallRuleCreate true "request" // @Success 200 {object} dto.FirewallRuleCreateResponse @@ -458,8 +460,6 @@ func handleFirewallRuleError(c *gin.Context, err error) { helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err) case errors.Is(err, repo.ErrFirewallRuleRevisionConflict): helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err) - case errors.Is(err, filter.ErrRuleCheckRequired): - helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_CHECK_REQUIRED", "ErrInvalidParams", err) case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope), errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable): helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err) @@ -598,7 +598,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) { // @Summary Delete Docker port guard policies // @Accept json // @Param request body dto.DockerPortGuardPolicyBatchDelete true "request" -// @Success 200 +// @Success 200 {object} dto.FilterChainOperationResponse // @Security ApiKeyAuth // @Security Timestamp // @Router /hosts/firewall/docker/policies/delete/batch [post] @@ -608,32 +608,34 @@ func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) { if err := helper.CheckBindAndValidate(&request, c); err != nil { return } - if err := dockerPortGuardService.DeletePolicies(c.Request.Context(), request); err != nil { + result, err := dockerPortGuardService.DeletePolicies(request) + if err != nil { handleDockerPortGuardError(c, err) return } - helper.Success(c) + helper.SuccessWithData(c, result) } // @Tags Firewall // @Summary Batch upsert Docker port guard policies // @Accept json // @Param request body dto.DockerPortGuardPolicyBatch true "request" -// @Success 200 +// @Success 200 {object} dto.FilterChainOperationResponse // @Security ApiKeyAuth // @Security Timestamp // @Router /hosts/firewall/docker/policies/batch [post] -// @x-panel-log {"bodyKeys":["mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略 [mode]","formatEN":"batch update Docker port guard policies [mode]"} +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略","formatEN":"batch update Docker port guard policies"} func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) { var request dto.DockerPortGuardPolicyBatch if err := helper.CheckBindAndValidate(&request, c); err != nil { return } - if err := dockerPortGuardService.UpsertPolicies(c.Request.Context(), request); err != nil { + result, err := dockerPortGuardService.UpsertPolicies(request) + if err != nil { handleDockerPortGuardError(c, err) return } - helper.Success(c) + helper.SuccessWithData(c, result) } func handleDockerPortGuardError(c *gin.Context, err error) { diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index 65d645fec08a..9cadc2571aa2 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -109,18 +109,6 @@ type FirewallRuleReset struct { WithDockerRestart bool `json:"withDockerRestart"` } -type FirewallRuleCheckResult struct { - Decision filter.CheckDecision `json:"decision"` - Classification filter.CheckClassification `json:"classification"` - Reason string `json:"reason"` - RequestedRule filter.FirewallRule `json:"requestedRule"` - RequestedRuleKey string `json:"requestedRuleKey"` - ExistingRuleUUID string `json:"existingRuleUUID,omitempty"` - Candidates []filter.ObservedRule `json:"candidates,omitempty"` - AllowedActions []filter.CheckAction `json:"allowedActions,omitempty"` - CheckFlag string `json:"checkFlag"` -} - type FirewallRuleInventory struct { PageInfo Scope filter.Scope `json:"scope,omitempty"` @@ -213,14 +201,18 @@ type DockerPortGuardEndpointIdentity struct { } type DockerPortGuardPolicyBatch struct { - Endpoints []DockerPortGuardEndpointIdentity `json:"endpoints" validate:"required,min=1,max=256,dive"` - Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"` - Sources []string `json:"sources" validate:"max=256,dive,required,max=64"` - Description string `json:"description" validate:"max=256"` + Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"` } type DockerPortGuardPolicyBatchDelete struct { - UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"` + UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"` +} + +type DockerPortGuardPolicy struct { + DockerPortGuardEndpointIdentity + Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"` + Sources []string `json:"sources" validate:"dive,required,max=64"` + Description string `json:"description" validate:"max=256"` } type DockerPortGuardOperation struct { @@ -228,34 +220,24 @@ type DockerPortGuardOperation struct { TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` } -type FirewallRuleCheckItem struct { - AdoptLocator *filter.Locator `json:"adoptLocator,omitempty" validate:"excluded_with=UUID"` - UUID string `json:"uuid" validate:"omitempty,max=64"` - Rule filter.FirewallRule `json:"rule" validate:"required"` -} - -type FirewallRuleCheck struct { - Items []FirewallRuleCheckItem `json:"items" validate:"required,min=1,max=256,dive"` -} - -type FirewallRuleCheckResponse struct { - Items []FirewallRuleCheckResult `json:"items"` +type FirewallRuleAdopt struct { + Scope filter.Scope `json:"scope" validate:"required"` + InstanceKey string `json:"instanceKey" validate:"required,max=128"` } type FirewallRuleCreateItem struct { - Rule filter.FirewallRule `json:"rule" validate:"required"` - CheckFlag string `json:"checkFlag"` - Action filter.CheckAction `json:"action"` - AdoptInstanceKey string `json:"adoptInstanceKey"` - SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"` - SourceID string `json:"sourceID"` + Rule filter.FirewallRule `json:"rule" validate:"required"` + SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"` + SourceID string `json:"sourceID"` } type FirewallRuleCreate struct { - Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,max=256,dive"` + Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"` } type FirewallRuleCreateResponse struct { + TaskID string `json:"taskID,omitempty"` + Queued bool `json:"queued,omitempty"` Succeeded int `json:"succeeded"` Failed int `json:"failed"` Skipped int `json:"skipped"` @@ -327,7 +309,7 @@ type FirewallRuleSyncFailure struct { } type FirewallRuleDelete struct { - UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"` + UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"` } type FirewallRuleDeleteResponse struct { @@ -355,3 +337,22 @@ type FirewallRuleReorder struct { TargetPosition *int64 `json:"targetPosition"` Priority *int `json:"priority"` } + +func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) { + p.Items = append(p.Items, item) + switch item.Status { + case firewallsync.StatusReady: + p.Ready++ + p.Total++ + case firewallsync.StatusExisting: + p.Existing++ + p.Total++ + case firewallsync.StatusRemove: + p.Removed++ + case firewallsync.StatusBlocked: + p.Blocked++ + if item.ReasonCode != firewallsync.ReasonReadOnlyRule { + p.Total++ + } + } +} diff --git a/agent/app/dto/forwarding.go b/agent/app/dto/forwarding.go index 5f87bb8d0bc3..03e65b19b525 100644 --- a/agent/app/dto/forwarding.go +++ b/agent/app/dto/forwarding.go @@ -8,8 +8,6 @@ type ForwardRuleSearch struct { Strategy string `json:"strategy"` } -// ForwardRule preserves the existing firewall search response shape while -// keeping forwarding data separate from the filter client model. type ForwardRule struct { ID uint `json:"id"` Chain string `json:"chain"` @@ -34,7 +32,7 @@ type ForwardRule struct { type ForwardRuleOperate struct { ForceDelete bool `json:"forceDelete"` - Rules []ForwardRuleOperation `json:"rules"` + Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"` } type ForwardRuleOperation struct { diff --git a/agent/app/model/firewall.go b/agent/app/model/firewall.go index 2478cc35f93b..b41b5396f965 100644 --- a/agent/app/model/firewall.go +++ b/agent/app/model/firewall.go @@ -72,19 +72,6 @@ func FirewallRuleOwner(sourceKind, sourceID string) string { return sourceKind + ":" + sourceID } -func FirewallRulesRevision(rules []FirewallRule) (string, error) { - ordered := append([]FirewallRule(nil), rules...) - sort.Slice(ordered, func(i, j int) bool { - return ordered[i].UUID < ordered[j].UUID - }) - payload, err := json.Marshal(ordered) - if err != nil { - return "", err - } - sum := sha256.Sum256(payload) - return hex.EncodeToString(sum[:]), nil -} - func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) { normalized, err := filter.NormalizeRule(rule) if err != nil { diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index b66c90f6c60a..e8c5a2d08017 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -26,6 +26,8 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" + firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" + "github.com/google/uuid" "gorm.io/gorm" ) @@ -62,7 +64,7 @@ type IFirewallService interface { Reset(context.Context, dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) Inventory(context.Context, dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) LoadFirewallNativeDetail(context.Context, dto.FirewallNativeDetail) (string, error) - Check(context.Context, string, dto.FirewallRuleCheck) (dto.FirewallRuleCheckResponse, 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 @@ -316,19 +318,6 @@ func (s *FirewallService) operateFilterChainBaseLocked(provider string, request return nil } -func (s *FirewallService) syncConfiguredFirewallPorts(ctx context.Context) error { - configured, err := loadConfiguredFirewallPortWhiteList() - if err != nil { - return err - } - required, err := LoadRequiredFirewallPortWhiteList() - if err != nil { - return err - } - ports := excludeFirewallPorts(configured, required) - return s.SyncSystemPorts(ctx, nil, systemPorts(ports)) -} - func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() @@ -644,7 +633,7 @@ func matchesFirewallInventoryAction(action filter.Action, actions []string) bool if requested == "accept" && action == filter.ActionAccept { return true } - if requested == "deny" && action != filter.ActionAccept { + if requested == "deny" && (action == filter.ActionDrop || action == filter.ActionReject) { return true } } @@ -768,170 +757,6 @@ func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request return runtime.NativeDetail(ctx, request.Name, request.Permanent) } -func (s *FirewallService) checkUpdate( - ctx context.Context, - clientIP string, - ruleUUID string, - requestedRule filter.FirewallRule, -) (dto.FirewallRuleCheckResult, error) { - prepared, err := s.prepareManagedUpdate(ctx, clientIP, ruleUUID, requestedRule) - if err != nil { - return dto.FirewallRuleCheckResult{}, err - } - semantic, err := model.FirewallRuleFromDomain(prepared.After) - if err != nil { - return dto.FirewallRuleCheckResult{}, err - } - return dto.FirewallRuleCheckResult{ - Decision: filter.CheckDecisionReady, - Classification: filter.CheckClassificationNone, - Reason: "update_ready", - RequestedRule: prepared.After, - RequestedRuleKey: semantic.PolicyKey(), - }, nil -} - -func (s *FirewallService) Check( - ctx context.Context, - clientIP string, - request dto.FirewallRuleCheck, -) (dto.FirewallRuleCheckResponse, error) { - response := dto.FirewallRuleCheckResponse{Items: make([]dto.FirewallRuleCheckResult, 0, len(request.Items))} - var selectedProvider filter.Provider - createStateLoaded := false - var desiredByScope map[string][]filter.DesiredRule - var incompatiblePolicies []filter.InventoryItem - var managedRevision string - type checkState struct { - snapshot filter.Snapshot - desired []filter.DesiredRule - managedRevision string - } - states := make(map[string]checkState) - pending := make([]filter.FirewallRule, 0, len(request.Items)) - for _, item := range request.Items { - if ruleUUID := strings.TrimSpace(item.UUID); ruleUUID != "" { - result, updateErr := s.checkUpdate(ctx, clientIP, ruleUUID, item.Rule) - if updateErr != nil { - return dto.FirewallRuleCheckResponse{}, updateErr - } - response.Items = append(response.Items, result) - continue - } - if !createStateLoaded { - var err error - selectedProvider, err = s.selectedProvider(ctx) - if err != nil { - return dto.FirewallRuleCheckResponse{}, err - } - createStateLoaded = true - } - item.Rule = applySelectedProviderScopeDefaults(item.Rule, selectedProvider) - rule, err := filter.NormalizeRule(item.Rule) - if err != nil { - return dto.FirewallRuleCheckResponse{}, err - } - if rule.Scope.Provider != selectedProvider { - return dto.FirewallRuleCheckResponse{}, fmt.Errorf( - "%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selectedProvider, rule.Scope.Provider, - ) - } - runtime, err := s.adapters.Resolve(rule.Scope.Provider) - if err != nil { - return dto.FirewallRuleCheckResponse{}, err - } - rule, err = runtime.Prepare(rule) - if err != nil { - return dto.FirewallRuleCheckResponse{}, err - } - if err = runtime.CheckRule(ctx, rule); err != nil { - return dto.FirewallRuleCheckResponse{}, err - } - - scopeKey := rule.Scope.Key() - state, exists := states[scopeKey] - if !exists { - snapshot, observeErr := runtime.ObserveMutation(ctx, rule.Scope) - if observeErr != nil { - return dto.FirewallRuleCheckResponse{}, observeErr - } - if desiredByScope == nil { - stored, listErr := s.rules.List(ctx) - if listErr != nil { - return dto.FirewallRuleCheckResponse{}, listErr - } - desiredByScope, incompatiblePolicies = s.desiredFirewallRulesByScope(ctx, stored, selectedProvider) - for _, failure := range incompatiblePolicies { - if !failure.Incompatible { - return dto.FirewallRuleCheckResponse{}, errors.New(failure.Error) - } - } - managedRevision, err = model.FirewallRulesRevision(stored) - if err != nil { - return dto.FirewallRuleCheckResponse{}, err - } - } - for index := range snapshot.Rules { - for _, failure := range incompatiblePolicies { - marker := "1panel-rule:" + failure.Desired.UUID - if snapshot.Rules[index].Marker == marker || strings.HasPrefix(snapshot.Rules[index].Marker, marker+"-") { - snapshot.Rules[index].Protected = true - } - } - } - state = checkState{snapshot: snapshot, desired: desiredByScope[scopeKey], managedRevision: managedRevision} - states[scopeKey] = state - } - var checked filter.RuleCheckResult - var checkErr error - if item.AdoptLocator != nil { - checked, checkErr = filter.CheckAdopt(state.snapshot, rule, state.desired, *item.AdoptLocator) - } else { - checked, checkErr = filter.CheckCreate(state.snapshot, rule, state.desired) - } - if checkErr != nil { - return dto.FirewallRuleCheckResponse{}, checkErr - } - adopting := checked.Decision == filter.CheckDecisionConfirmationRequired && checked.Classification == filter.CheckClassificationExactExternal - if checked.Decision == filter.CheckDecisionReady || adopting { - for _, previous := range pending { - if collision := filter.CheckRuleCollision(rule, previous); collision != nil { - if adopting && errors.Is(collision, filter.ErrRuleConflict) { - continue - } - if errors.Is(collision, filter.ErrRuleConflict) { - checked.Decision, checked.Classification, checked.Reason = filter.CheckDecisionBlocked, filter.CheckClassificationConflict, "exact_rule_conflict" - } else if errors.Is(collision, filter.ErrRuleOperation) { - checked.Decision, checked.Classification, checked.Reason = filter.CheckDecisionNoChange, filter.CheckClassificationExactManaged, "equivalent_batch_rule" - if adopting { - checked.Decision = filter.CheckDecisionBlocked - } - } else { - return dto.FirewallRuleCheckResponse{}, collision - } - checked.AllowedActions = nil - break - } - } - if checked.Decision == filter.CheckDecisionReady || checked.Decision == filter.CheckDecisionConfirmationRequired { - pending = append(pending, rule) - } - } - checkFlag, signErr := signFirewallRuleCheck(checked, state.snapshot, state.managedRevision) - if signErr != nil { - return dto.FirewallRuleCheckResponse{}, signErr - } - result := dto.FirewallRuleCheckResult{ - Decision: checked.Decision, Classification: checked.Classification, Reason: checked.Reason, - RequestedRule: checked.RequestedRule, RequestedRuleKey: checked.RequestedRuleKey, - ExistingRuleUUID: checked.ExistingRuleUUID, Candidates: checked.Candidates, - AllowedActions: checked.AllowedActions, CheckFlag: checkFlag, - } - response.Items = append(response.Items, result) - } - return response, nil -} - func applySelectedProviderScopeDefaults(rule filter.FirewallRule, selected filter.Provider) filter.FirewallRule { scope := rule.Scope if scope.Provider == "" { @@ -978,242 +803,306 @@ func defaultFirewallRuleFamily(rule filter.FirewallRule, provider filter.Provide return filter.FamilyIPv4 } -func (s *FirewallService) checkRule( - ctx context.Context, - clientIP string, - item dto.FirewallRuleCheckItem, -) (dto.FirewallRuleCheckResult, error) { - response, err := s.Check(ctx, clientIP, dto.FirewallRuleCheck{Items: []dto.FirewallRuleCheckItem{item}}) - if err != nil { - return dto.FirewallRuleCheckResult{}, err - } - if len(response.Items) != 1 { - return dto.FirewallRuleCheckResult{}, errors.New("firewall rule check returned no result") - } - return response.Items[0], nil -} - type preparedFirewallRuleCreate struct { - request dto.FirewallRuleCreateItem - runtime *filterruntime.Engine - snapshot filter.Snapshot - authorization firewallRuleCreateAuthorization + request dto.FirewallRuleCreateItem + runtime *filterruntime.Engine } func (s *FirewallService) Create( ctx context.Context, request dto.FirewallRuleCreate, ) (dto.FirewallRuleCreateResponse, error) { - result, _ := s.create(ctx, request) - return result, nil + taskItem, err := task.NewTaskWithOps("", task.TaskCreate, task.TaskScopeFirewall, "", 0) + if err != nil { + return dto.FirewallRuleCreateResponse{}, err + } + taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error { + _, err := s.runCreateTask(t, request) + 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) create( - ctx context.Context, - request dto.FirewallRuleCreate, -) (dto.FirewallRuleCreateResponse, error) { +func (s *FirewallService) runCreateTask(t *task.Task, request dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() - - return s.createLocked(ctx, request) + return s.createRules(t.TaskCtx, request, t) } -func (s *FirewallService) createLocked( - ctx context.Context, - request dto.FirewallRuleCreate, -) (dto.FirewallRuleCreateResponse, error) { - prepared, failedIndex, err := s.prepareCreate(ctx, request.Items) - if err != nil { - return firewallCreatePrepareFailure(request.Items, failedIndex, err), err - } - - result := dto.FirewallRuleCreateResponse{} - var createErr error - for index := 0; index < len(prepared); { - batchEnd := nativeCreateBatchEnd(prepared, index) - if batchEnd-index > 1 { - if err := s.createNativeRuleBatch(ctx, prepared[index:batchEnd]); err != nil { - failed := firewallCreateExecutionFailure(request.Items, index, err) - result.Failed += failed.Failed - result.Skipped += failed.Skipped - result.Errors = append(result.Errors, failed.Errors...) - if global.LOG != nil { - global.LOG.Errorf("batch create firewall rules %d-%d failed: %v", index+1, batchEnd, err) - } - createErr = err - break - } - result.Succeeded += batchEnd - index - index = batchEnd - continue +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, + })) } - - entry := prepared[index] - snapshot, err := entry.runtime.ObserveMutation(ctx, entry.request.Rule.Scope) - if err == nil { - entry.authorization, err = refreshCreateAuthorization(snapshot, entry) + if taskErr == nil { + taskErr = firstFailure } - if err == nil { - err = s.createRule(ctx, entry.runtime, snapshot, entry.request, entry.authorization) + }() + 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 { - failed := firewallCreateExecutionFailure(request.Items, index, err) - result.Failed += failed.Failed - result.Skipped += failed.Skipped - result.Errors = append(result.Errors, failed.Errors...) - if global.LOG != nil { - global.LOG.Errorf("batch create firewall rule item %d failed: %v", index+1, err) - } - createErr = err - break + result.Errors = append(result.Errors, dto.FirewallRuleCreateFailure{ + Index: origin.index, Status: status, Rule: rule, Error: err.Error(), + }) } - result.Succeeded++ - index++ } - return result, createErr -} - -func (s *FirewallService) prepareCreate( - ctx context.Context, - items []dto.FirewallRuleCreateItem, -) ([]preparedFirewallRuleCreate, int, error) { - type prepareState struct { - runtime *filterruntime.Engine - snapshot filter.Snapshot - managedRevision string - } - states := make(map[string]prepareState) - prepared := make([]preparedFirewallRuleCreate, 0, len(items)) - selectedProvider, err := s.selectedProvider(ctx) + selected, err := s.selectedProvider(ctx) if err != nil { - return nil, 0, err + for index := range request.Items { + record(itemOrigin{index: index, rule: request.Items[index].Rule}, "skipped", err) + } + return result, err } - for index, request := range items { - if request.CheckFlag == "" { - return nil, index, filter.ErrRuleCheckRequired + var stop error + var prepared []preparedFirewallRuleCreate + var origins []itemOrigin + flush := func() { + if len(prepared) == 0 { + return } - rule, err := filter.NormalizeRule(request.Rule) - if err != nil { - return nil, index, err + defer func() { prepared, origins = nil, nil }() + if stop == nil { + stop = ctx.Err() } - if rule.Scope.Provider != selectedProvider { - return nil, index, fmt.Errorf( - "%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selectedProvider, rule.Scope.Provider, - ) + if stop != nil { + for _, origin := range origins { + record(origin, "skipped", stop) + } + return } - runtime, err := s.adapters.Resolve(rule.Scope.Provider) + runtime := prepared[0].runtime + scope := prepared[0].request.Rule.Scope + snapshot, err := runtime.ObserveMutation(ctx, scope) if err != nil { - return nil, index, err + for _, origin := range origins { + record(origin, "failed", err) + } + if !errors.Is(err, filter.ErrFamilyUnavailable) { + stop = err + } + return } - rule, err = runtime.Prepare(rule) + stored, err := s.rules.List(ctx) if err != nil { - return nil, index, err + for _, origin := range origins { + record(origin, "failed", err) + } + stop = err + return } - if err = runtime.CheckRule(ctx, rule); err != nil { - return nil, index, err + identities, err := firewallRuleCollisions(stored, runtime.Provider(), "") + if err != nil { + for _, origin := range origins { + record(origin, "failed", err) + } + stop = err + return } - - scopeKey := rule.Scope.Key() - state, exists := states[scopeKey] - if !exists { - snapshot, observeErr := runtime.ObserveMutation(ctx, rule.Scope) - if observeErr != nil { - return nil, index, observeErr + valid := prepared[:0] + validOrigins := origins[:0] + for index, entry := range prepared { + rule := entry.request.Rule + checkErr := identities.Check(rule) + if checkErr == nil { + checkErr = filter.CheckObservedRuleCollisions(snapshot, entry.request.Rule, nil) } - stored, listErr := s.rules.List(ctx) - if listErr != nil { - return nil, index, listErr + if checkErr == nil { + checkErr = identities.Add(rule) } - managedRevision, revisionErr := model.FirewallRulesRevision(stored) - if revisionErr != nil { - return nil, index, revisionErr + if checkErr != nil { + record(origins[index], "failed", checkErr) + continue } - state = prepareState{runtime: runtime, snapshot: snapshot, managedRevision: managedRevision} - states[scopeKey] = state + valid = append(valid, entry) + validOrigins = append(validOrigins, origins[index]) } - authorization, authorizeErr := authorizeFirewallRuleCreate( - request.CheckFlag, request.Action, request.AdoptInstanceKey, rule, state.snapshot, state.managedRevision, - ) - if authorizeErr != nil { - return nil, index, authorizeErr - } - sourceKind := request.SourceKind - if sourceKind == "" { - sourceKind = constant.FirewallRuleSourceUser - } - for _, previous := range prepared { - if authorization.Operation == filter.ChangeAdopt || previous.authorization.Operation == filter.ChangeAdopt { - if authorization.Locator != nil && previous.authorization.Locator != nil && - filter.SameLocator(*authorization.Locator, *previous.authorization.Locator) { - return nil, index, filter.ErrRuleOperation + 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) } - if err := filter.CheckRuleCollision(rule, previous.request.Rule); err != nil { - if errors.Is(err, filter.ErrRuleConflict) && (authorization.Operation == filter.ChangeAdopt || previous.authorization.Operation == filter.ChangeAdopt) { - continue + } + } + 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), + })) + } + } + 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 } - return nil, index, err + continue + } + origin.rule = entry.request.Rule + prepared = append(prepared, entry) + origins = append(origins, origin) + if !supportsNativeRuleBatch(selected) || rule.OrderIndex != nil { + flush() } } - request.Rule = rule - request.SourceKind = sourceKind - prepared = append(prepared, preparedFirewallRuleCreate{ - request: request, runtime: state.runtime, snapshot: state.snapshot, authorization: authorization, - }) } - return prepared, -1, nil + flush() + return result, stop } -func nativeCreateBatchEnd(prepared []preparedFirewallRuleCreate, start int) int { - if start < 0 || start >= len(prepared) { - return start +func convertImportedFirewallRule(rule filter.FirewallRule, selected filter.Provider) ([]filter.FirewallRule, error) { + source := rule.Scope.Normalize().Provider + if source == "" { + source = selected } - first := prepared[start] - if first.runtime == nil || !supportsNativeRuleBatch(first.runtime.Provider()) || - first.authorization.Operation != filter.ChangeCreate || first.request.Rule.OrderIndex != nil { - return start + 1 + rules, err := filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(rule, source)) + if err != nil { + return nil, err } - scopeKey := first.request.Rule.Scope.Key() - seen := make(map[string]struct{}, len(prepared)-start) - for index := start; index < len(prepared); index++ { - entry := prepared[index] - if entry.runtime != first.runtime || entry.authorization.Operation != filter.ChangeCreate || - entry.request.Rule.Scope.Key() != scopeKey || entry.request.Rule.OrderIndex != nil { - return index - } - ruleKey, err := filter.RuleKey(entry.request.Rule) + var converted []filter.FirewallRule + for _, sourceRule := range rules { + policy, err := model.FirewallRuleFromDomain(sourceRule) if err != nil { - return index + return nil, err } - if _, exists := seen[ruleKey]; exists { - return index + targetRules, err := policy.RulesForProvider(selected) + if err != nil { + return nil, err } - seen[ruleKey] = struct{}{} + converted = append(converted, targetRules...) } - return len(prepared) + return converted, nil } -type createdFirewallBatchRule struct { - record model.FirewallRule - rule filter.FirewallRule +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) createNativeRuleBatch(ctx context.Context, prepared []preparedFirewallRuleCreate) error { - if len(prepared) < 2 { - return fmt.Errorf("%w: native firewall batch requires at least two rules", filter.ErrInvalidRule) - } - runtime := prepared[0].runtime - snapshot, err := runtime.ObserveMutation(ctx, prepared[0].request.Rule.Scope) +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 err + return preparedFirewallRuleCreate{}, err } - stored, err := s.rules.List(ctx) + 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 err + return preparedFirewallRuleCreate{}, err } - identities, err := firewallRuleCollisions(stored, runtime.Provider(), "") + rule, err = runtime.Prepare(rule) if err != nil { - return err + 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 { @@ -1221,130 +1110,69 @@ func (s *FirewallService) createNativeRuleBatch(ctx context.Context, prepared [] maximumSequence = *record.Sequence } } - nextSequence := maximumSequence + model.FirewallRuleSequenceStep - created := make([]createdFirewallBatchRule, 0, len(prepared)) + records := make([]model.FirewallRule, 0, len(prepared)) + changes := make([]filter.DesiredChange, 0, len(prepared)) for _, entry := range prepared { - domainRule := entry.request.Rule - record, recordErr := firewallRuleModelForCreate(domainRule, entry.request, constant.FirewallRuleOriginCreated) - if recordErr != nil { - return s.cleanupFirewallBatchRecords(ctx, created, recordErr) - } - if domainRule.Scope.Provider != filter.ProviderFirewalld { - sequence := nextSequence - record.Sequence = &sequence - nextSequence += model.FirewallRuleSequenceStep - } - if err := identities.Check(domainRule); err != nil { - return s.cleanupFirewallBatchRecords(ctx, created, err) - } - if err := filter.CheckObservedRuleCollisions(snapshot, domainRule, nil); err != nil { - return s.cleanupFirewallBatchRecords(ctx, created, err) - } - if recordErr = s.rules.Create(ctx, &record); recordErr != nil { - return s.cleanupFirewallBatchRecords(ctx, created, recordErr) - } - if err := identities.Add(domainRule); err != nil { - return s.cleanupFirewallBatchRecords(ctx, append(created, createdFirewallBatchRule{record: record, rule: domainRule}), err) + 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 } - domainRule.UUID = record.UUID - created = append(created, createdFirewallBatchRule{record: record, rule: domainRule}) - } - - changes := make([]filter.DesiredChange, 0, len(created)) - for index := range created { - changes = append(changes, filter.DesiredChange{Operation: filter.ChangeCreate, After: &created[index].rule}) - } - backendPlan, verification, err := runtime.Execute(ctx, snapshot, changes) - if err != nil { - return s.cleanupFirewallBatchRecords(ctx, created, err) - } - if !verification.Matched { - return s.cleanupFirewallBatchRecords(ctx, created, filter.ErrVerificationFailed) - } - - for index := range created { - _, commitErr := findBatchCommittedObserved(verification.Snapshot, created[index].rule.UUID) - if commitErr != nil { - commitErr = rollbackFirewallPlan(ctx, runtime, backendPlan, commitErr) - return s.cleanupFirewallBatchRecords(ctx, created, commitErr) + record, err := firewallRuleModelForCreate(rule, entry.request, constant.FirewallRuleOriginCreated) + if err != nil { + return failAll(err) } - } - return nil -} - -func findBatchCommittedObserved(snapshot filter.Snapshot, ruleUUID string) (filter.ObservedRule, error) { - marker := "1panel-rule:" + ruleUUID - matches := make([]filter.ObservedRule, 0, 1) - for _, observed := range snapshot.Rules { - if observed.Marker == marker { - matches = append(matches, observed) + 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 len(matches) != 1 { - return filter.ObservedRule{}, fmt.Errorf("%w: expected one committed batch rule, found %d", filter.ErrVerificationFailed, len(matches)) + if err := runtime.ExecuteCreate(ctx, snapshot, changes); err != nil { + return failAll(firewallCreateExecutionError(err)) } - return matches[0], nil -} - -func (s *FirewallService) cleanupFirewallBatchRecords( - ctx context.Context, - created []createdFirewallBatchRule, - cause error, -) error { - cleanupErrors := make([]error, 0) - for index := len(created) - 1; index >= 0; index-- { - if err := s.rules.DeleteWithRevision(ctx, created[index].record.UUID, created[index].record.Revision); err != nil { - cleanupErrors = append(cleanupErrors, fmt.Errorf("cleanup failed firewall rule %q: %w", created[index].record.UUID, err)) - } + for index := range records { + results[index] = s.saveFirewallRule(ctx, &records[index]) } - if len(cleanupErrors) == 0 { - return cause - } - return errors.Join(append([]error{cause}, cleanupErrors...)...) + return results } -func firewallCreatePrepareFailure( - items []dto.FirewallRuleCreateItem, - failedIndex int, - cause error, -) dto.FirewallRuleCreateResponse { - result := dto.FirewallRuleCreateResponse{ - Failed: 1, Skipped: len(items) - 1, - Errors: make([]dto.FirewallRuleCreateFailure, 0, len(items)), - } - for index := range items { - failure := dto.FirewallRuleCreateFailure{ - Index: index, Status: "skipped", Rule: items[index].Rule, - } - if index == failedIndex { - failure.Status = "failed" - failure.Error = cause.Error() - } - result.Errors = append(result.Errors, failure) - } - return result +func firewallCreateExecutionError(err error) error { + return fmt.Errorf("%s: %w", i18n.GetMsgByKey("FirewallCreateRuleExecutionFailed"), err) } -func firewallCreateExecutionFailure( - items []dto.FirewallRuleCreateItem, - failedIndex int, - cause error, -) dto.FirewallRuleCreateResponse { - result := dto.FirewallRuleCreateResponse{ - Failed: 1, Skipped: len(items) - failedIndex - 1, - Errors: make([]dto.FirewallRuleCreateFailure, 0, len(items)-failedIndex), - } - for index := failedIndex; index < len(items); index++ { - failure := dto.FirewallRuleCreateFailure{ - Index: index, Status: "skipped", Rule: items[index].Rule, +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" } - if index == failedIndex { - failure.Status = "failed" - failure.Error = cause.Error() - } - result.Errors = append(result.Errors, failure) + return fmt.Errorf("%s: %w", i18n.GetMsgByKey(message), err) } - return result + return nil } type preparedFirewallRuleDelete struct { @@ -1612,100 +1440,95 @@ func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested f return nil } -func (s *FirewallService) createRule( - ctx context.Context, - runtime *filterruntime.Engine, - snapshot filter.Snapshot, - request dto.FirewallRuleCreateItem, - authorization firewallRuleCreateAuthorization, -) error { - domainRule := request.Rule - if authorization.Operation == filter.ChangeAdopt { - if err := filter.CheckAdoptDuplicates(snapshot, domainRule); err != nil { - return err - } +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 } - if authorization.Operation == filter.ChangeCreate { - if err := filter.CheckObservedRuleCollisions(snapshot, domainRule, nil); err != nil { - return err - } + runtime, err := s.adapters.Resolve(request.Scope.Provider) + if err != nil { + return err } - appendRule := false - if authorization.Operation == filter.ChangeCreate && domainRule.Scope.Provider == filter.ProviderUFW && domainRule.OrderIndex == nil { - appendPosition, err := runtime.AppendPosition(ctx, snapshot, domainRule) - if err != nil { - return err - } - domainRule.OrderIndex = &appendPosition - appendRule = true - } else if authorization.Operation == filter.ChangeCreate && domainRule.OrderIndex != nil { - maxPosition, err := runtime.MaxPosition(ctx, snapshot, domainRule) - if err != nil { - return err - } - if *domainRule.OrderIndex < 1 || *domainRule.OrderIndex > maxPosition+1 { - return fmt.Errorf("%w: create target position %d is out of range 1-%d", filter.ErrInvalidRule, *domainRule.OrderIndex, maxPosition+1) - } - appendRule = domainRule.Scope.Provider == filter.ProviderUFW && *domainRule.OrderIndex == maxPosition+1 + snapshot, err := runtime.ObserveMutation(ctx, request.Scope) + if err != nil { + return err } - origin := constant.FirewallRuleOriginCreated - change := filter.DesiredChange{ - Operation: authorization.Operation, - After: &domainRule, - Locator: authorization.Locator, - Append: appendRule, + observed, err := filter.FindCandidate(snapshot.Rules, request.InstanceKey) + if err != nil { + return filter.ErrRuleStale } - if authorization.Operation == filter.ChangeAdopt { - origin = constant.FirewallRuleOriginAdopted + 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 observed.Protected { + return filter.ErrProtectedRule } - ruleRecord, err := firewallRuleModelForCreate(domainRule, request, origin) + 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 authorization.Operation == filter.ChangeCreate { - if err := s.ensureFirewallRuleIdentityAvailable(ctx, domainRule, ""); 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) } - } else if authorization.Operation == filter.ChangeAdopt { - stored, err := s.rules.List(ctx) - if err != nil { - return err + } + if err := identities.CheckDuplicate(rule); err != nil { + if errors.Is(err, filter.ErrRuleOperation) { + return filter.ErrDuplicateAdoption } - identities, err := firewallRuleCollisions(stored, domainRule.Scope.Provider, "") + return err + } + if rule.Scope.Provider != filter.ProviderFirewalld { + sequence, err := s.sequenceForCreatedFirewallRule(ctx, snapshot, rule) if err != nil { return err } - if err := identities.CheckDuplicate(domainRule); err != nil { - if errors.Is(err, filter.ErrRuleOperation) { - return filter.ErrDuplicateAdoption - } - return err - } + record.Sequence = &sequence } - if domainRule.Scope.Provider != filter.ProviderFirewalld { - sequence, sequenceErr := s.sequenceForCreatedFirewallRule(ctx, snapshot, domainRule) - if sequenceErr != nil { - return sequenceErr - } - ruleRecord.Sequence = &sequence - } - if err := s.rules.Create(ctx, &ruleRecord); err != nil { - return err - } - domainRule.UUID = ruleRecord.UUID - change.After = &domainRule - backendPlan, verification, err := runtime.Execute(ctx, snapshot, []filter.DesiredChange{change}) + 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 s.cleanupFailedCreate(ctx, ruleRecord, err) + return err } if !verification.Matched { - return s.cleanupFailedCreate(ctx, ruleRecord, filter.ErrVerificationFailed) + return filter.ErrVerificationFailed } - _, err = filter.FindCommittedObserved(verification.Snapshot, domainRule, backendPlan) - if err != nil { - return s.cleanupAppliedCreate(ctx, runtime, backendPlan, ruleRecord, err) + if _, err := filter.FindCommittedObserved(verification.Snapshot, rule, plan); err != nil { + return rollbackFirewallPlan(ctx, runtime, plan, err) } - return nil + return s.saveFirewallRule(ctx, &record) } func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string, allowProtected bool) error { @@ -2234,32 +2057,10 @@ func (s *FirewallService) resolveRuntime(ctx context.Context, provider filter.Pr return s.adapters.Resolve(provider) } -func (s *FirewallService) cleanupFailedCreate(ctx context.Context, rule model.FirewallRule, cause error) error { - if err := s.rules.DeleteWithRevision(ctx, rule.UUID, rule.Revision); err != nil { - return errors.Join(cause, fmt.Errorf("cleanup failed firewall rule %q: %w", rule.UUID, err)) - } - return cause -} - -func (s *FirewallService) cleanupAppliedCreate( - ctx context.Context, - runtime *filterruntime.Engine, - plan filter.BackendPlan, - rule model.FirewallRule, - cause error, -) error { - cause = rollbackFirewallPlan(ctx, runtime, plan, cause) - return s.cleanupFailedCreate(ctx, rule, cause) -} - func (s *FirewallService) ensureSystemPort(ctx context.Context, port dto.FirewallSystemPort) error { - create, err := s.prepareSystemPortCreate(ctx, port) - if err == nil && create == nil { - return nil - } - if err == nil { - err = s.createFirewallRuleItem(ctx, *create) - } + firewallRuleMutationMu.Lock() + err := s.ensureSystemPortLocked(ctx, port) + firewallRuleMutationMu.Unlock() if err == nil { return nil } @@ -2319,66 +2120,84 @@ func (s *FirewallService) appendUFWSystemPortUnverified( return nil } -func (s *FirewallService) createFirewallRuleItem(ctx context.Context, item dto.FirewallRuleCreateItem) error { - result, err := s.create(ctx, dto.FirewallRuleCreate{Items: []dto.FirewallRuleCreateItem{item}}) +func (s *FirewallService) ensureSystemPortLocked(ctx context.Context, port dto.FirewallSystemPort) error { + provider, err := s.selectedProvider(ctx) if err != nil { return err } - if result.Failed == 0 && result.Skipped == 0 { - return nil + source := dto.FirewallRuleCreateItem{ + Rule: systemPortRule(provider, port), SourceKind: constant.FirewallRuleSourceSecurity, + SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port), } - if len(result.Errors) > 0 && result.Errors[0].Error != "" { - return errors.New(result.Errors[0].Error) + prepared, err := s.prepareCreate(ctx, provider, source) + if err != nil { + return err } - return errors.New("create firewall rule failed") -} - -func (s *FirewallService) prepareSystemPortCreate(ctx context.Context, port dto.FirewallSystemPort) (*dto.FirewallRuleCreateItem, error) { - provider, err := s.selectedProvider(ctx) + rule, runtime := prepared.request.Rule, prepared.runtime + snapshot, err := runtime.ObserveMutation(ctx, rule.Scope) if err != nil { - return nil, err + return err } - rule := systemPortRule(provider, port) - check, err := s.checkRule(ctx, "", dto.FirewallRuleCheckItem{Rule: rule}) + stored, err := s.rules.List(ctx) if err != nil { - return nil, err + return err } - - if check.Decision == filter.CheckDecisionNoChange { - return nil, nil - } - create := dto.FirewallRuleCreateItem{ - Rule: check.RequestedRule, - CheckFlag: check.CheckFlag, - SourceKind: constant.FirewallRuleSourceSecurity, - SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port), - } - switch check.Classification { - case filter.CheckClassificationNone: - create.Action = filter.CheckActionCreate - case filter.CheckClassificationExactExternal: - if len(check.Candidates) == 0 { - return nil, fmt.Errorf("%w: external port rule has no candidate", filter.ErrRuleStale) - } - if len(check.Candidates) == 1 { - create.Action = filter.CheckActionAdopt - } else { - create.Action = filter.CheckActionSelectAdopt - } - create.AdoptInstanceKey = check.Candidates[0].InstanceKey - case filter.CheckClassificationConflict: - return nil, fmt.Errorf("cannot manage accepted port %s/%s: %s", port.Port, port.Protocol, check.Reason) - case filter.CheckClassificationExactManaged: - return nil, nil - case filter.CheckClassificationProtected: - if len(check.Candidates) > 0 { - return nil, nil - } - return nil, fmt.Errorf("%w: protected accepted port %s", filter.ErrProtectedRule, port.Port) - default: - return nil, fmt.Errorf("cannot manage accepted port %s/%s: %s", port.Port, port.Protocol, check.Reason) + matchKey, err := filter.RuleMatchKey(rule) + if err != nil { + return err } - return &create, nil + 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 (s *FirewallService) deleteSystemPort(ctx context.Context, port dto.FirewallSystemPort) error { @@ -2722,67 +2541,6 @@ func selectedRuleProvider() (filter.Provider, error) { return filter.Provider(provider), nil } -type firewallRuleCreateAuthorization = filter.CreateAuthorization - -func refreshCreateAuthorization( - snapshot filter.Snapshot, - prepared preparedFirewallRuleCreate, -) (firewallRuleCreateAuthorization, error) { - authorization := prepared.authorization - if authorization.Operation != filter.ChangeAdopt { - return authorization, nil - } - if err := filter.CheckAdoptDuplicates(snapshot, prepared.request.Rule); err != nil { - return firewallRuleCreateAuthorization{}, err - } - if candidate, err := filter.FindCandidate(snapshot.Rules, prepared.request.AdoptInstanceKey); err == nil { - locator := candidate.Locator - authorization.Locator = &locator - return authorization, nil - } - - ruleKey, err := filter.RuleKey(prepared.request.Rule) - if err != nil { - return firewallRuleCreateAuthorization{}, err - } - candidates := make([]filter.ObservedRule, 0) - for _, observed := range snapshot.Rules { - if observed.Marker != "" || observed.Protected || observed.ParseStatus != filter.ParseStatusSupported { - continue - } - observedKey, keyErr := filter.RuleKey(observed.Rule) - if keyErr == nil && observedKey == ruleKey { - candidates = append(candidates, observed) - } - } - if len(candidates) == 1 { - locator := candidates[0].Locator - authorization.Locator = &locator - return authorization, nil - } - return firewallRuleCreateAuthorization{}, filter.ErrRuleStale -} - -func signFirewallRuleCheck(result filter.RuleCheckResult, snapshot filter.Snapshot, managedRevision string) (string, error) { - return firewallCheckFlagCodec().Sign(result, snapshot, managedRevision) -} - -func authorizeFirewallRuleCreate( - checkFlag string, - action filter.CheckAction, - adoptInstanceKey string, - rule filter.FirewallRule, - snapshot filter.Snapshot, - managedRevision string, -) (firewallRuleCreateAuthorization, error) { - return firewallCheckFlagCodec().Authorize(checkFlag, action, adoptInstanceKey, rule, snapshot, managedRevision) -} - -func firewallCheckFlagCodec() *filter.CheckFlagCodec { - secret := []byte(global.CONF.Base.EncryptKey + "\x00firewall-rule-check-v1") - return filter.NewCheckFlagCodec(secret, constant.FirewallRuleCheckVersion) -} - func OperateFirewallPort(oldPorts, newPorts []int) error { client, err := selectedSystemFirewallClient() if err != nil { @@ -2848,86 +2606,6 @@ func LoadPanelPort() string { return portSetting.Value } -func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error { - previousSet, err := normalizeSystemPorts(previous) - if err != nil { - return err - } - currentSet, err := normalizeSystemPorts(current) - if err != nil { - return err - } - - provider, err := s.selectedProvider(ctx) - if err != nil { - return err - } - if !supportsNativeRuleBatch(provider) { - var syncErrors []error - for _, key := range sortedSystemPortKeys(currentSet) { - if _, exists := previousSet[key]; exists { - continue - } - if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil { - wrapped := fmt.Errorf("restore accepted firewall port %s: %w", key, err) - syncErrors = append(syncErrors, wrapped) - if global.LOG != nil { - global.LOG.Errorf("%v", wrapped) - } - } - } - for _, key := range sortedSystemPortKeys(previousSet) { - if _, exists := currentSet[key]; exists { - continue - } - if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil { - wrapped := fmt.Errorf("release accepted firewall port %s: %w", key, err) - syncErrors = append(syncErrors, wrapped) - if global.LOG != nil { - global.LOG.Errorf("%v", wrapped) - } - } - } - return errors.Join(syncErrors...) - } - - creates := make([]dto.FirewallRuleCreateItem, 0) - for _, key := range sortedSystemPortKeys(currentSet) { - if _, exists := previousSet[key]; exists { - continue - } - create, prepareErr := s.prepareSystemPortCreate(ctx, currentSet[key]) - if prepareErr != nil { - return prepareErr - } - if create != nil { - creates = append(creates, *create) - } - } - if len(creates) > 0 { - result, batchErr := s.Create(ctx, dto.FirewallRuleCreate{Items: creates}) - if batchErr != nil { - return batchErr - } - if result.Failed > 0 || result.Skipped > 0 { - if len(result.Errors) > 0 { - return fmt.Errorf("batch create accepted firewall ports: %s", result.Errors[0].Error) - } - return fmt.Errorf("batch create accepted firewall ports failed") - } - } - - for _, key := range sortedSystemPortKeys(previousSet) { - if _, exists := currentSet[key]; exists { - continue - } - if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil { - return err - } - } - return nil -} - func loadConfiguredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) { value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList) if err != nil { @@ -2994,18 +2672,8 @@ func (s *FirewallService) updatePortWhitelist(ctx context.Context, value string) } ctx = context.WithValue(ctx, panelPortWhitelistKey{}, firewall.NormalizePortWhitelist(append(oldPorts, required...))) added := excludeFirewallPorts(excludeFirewallPorts(ports, oldPorts), required) - items := make([]dto.FirewallRuleCreateItem, 0, len(added)) for _, port := range systemPorts(added) { - item, err := s.prepareSystemPortCreate(ctx, port) - if err != nil { - return err - } - if item != nil { - items = append(items, *item) - } - } - if len(items) > 0 { - if _, err := s.createLocked(ctx, dto.FirewallRuleCreate{Items: items}); err != nil { + if err := s.ensureSystemPortLocked(ctx, port); err != nil { return err } } @@ -3151,11 +2819,6 @@ func (s *FirewallService) addPortsBeforeStart(client lifecycle.Client) error { return errors.Join(recoveryErrors...) } -func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error { - return newFirewallService(). - SyncSystemPorts(context.Background(), systemPorts(previous), systemPorts(current)) -} - func systemPorts(ports []firewall.PortWhitelist) []dto.FirewallSystemPort { result := make([]dto.FirewallSystemPort, 0, len(ports)) for _, port := range ports { @@ -3173,3 +2836,89 @@ func systemPorts(ports []firewall.PortWhitelist) []dto.FirewallSystemPort { func excludeFirewallPorts(ports, excluded []firewall.PortWhitelist) []firewall.PortWhitelist { return firewall.ExcludePorts(ports, excluded) } + +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 +} diff --git a/agent/app/service/firewall_docker.go b/agent/app/service/firewall_docker.go index d8643da24f9b..3e13494d17ac 100644 --- a/agent/app/service/firewall_docker.go +++ b/agent/app/service/firewall_docker.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/netip" + "os" "path/filepath" "sort" "strconv" @@ -17,13 +18,13 @@ import ( "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" agenti18n "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/system" @@ -88,8 +89,8 @@ type IDockerPortGuardService interface { LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error) Operate(context.Context, dto.DockerPortGuardOperation) error QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) - DeletePolicies(context.Context, dto.DockerPortGuardPolicyBatchDelete) error - UpsertPolicies(context.Context, dto.DockerPortGuardPolicyBatch) error + DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) + UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) Reconcile(context.Context) error } @@ -125,6 +126,12 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto. } defer cli.Close() + if socketPath, local := strings.CutPrefix(cli.DaemonHost(), "unix://"); local { + if _, statErr := os.Stat(socketPath); errors.Is(statErr, os.ErrNotExist) { + return []dto.DockerPortGuardContainer{}, nil + } + } + endpoints, err := discoverDockerEndpoints(ctx, cli, false) if err != nil { return nil, err @@ -213,6 +220,19 @@ func matchDockerGuardPolicies( return endpoints, orphanPolicies } +func initializeDockerGuardRuntime(runtime dockerGuardRuntime, policies []docker_guard.Policy) error { + err := runtime.Initialize(policies) + if errors.Is(err, docker_guard.ErrDockerForwardPolicyDrop) { + family := "IPv4" + var familyErr *docker_guard.FamilyError + if errors.As(err, &familyErr) && familyErr.Family == docker_guard.FamilyIPv6 { + family = "IPv6" + } + return buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": family}, err) + } + return err +} + func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.DockerPortGuardOperation) error { dockerPortGuardServiceMu.Lock() defer dockerPortGuardServiceMu.Unlock() @@ -226,7 +246,7 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker if err != nil { return err } - if err := runtime.Initialize(policies); err != nil { + if err := initializeDockerGuardRuntime(runtime, policies); err != nil { recordDockerPortGuardReconcileError(err) return err } @@ -296,7 +316,7 @@ func (s *DockerPortGuardService) QueueInitialization( dockerPortGuardServiceMu.Lock() defer dockerPortGuardServiceMu.Unlock() t.Logf("backend=%s", backend) - err := runtime.Initialize(policies) + err := initializeDockerGuardRuntime(runtime, policies) recordDockerPortGuardReconcileError(err) return err }, nil) @@ -317,51 +337,68 @@ func (s *DockerPortGuardService) QueueInitialization( return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil } -func (s *DockerPortGuardService) DeletePolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatchDelete) error { - dockerPortGuardServiceMu.Lock() - defer dockerPortGuardServiceMu.Unlock() +func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) { uuids, err := docker_guard.NormalizePolicyUUIDs(request.UUIDs) if err != nil { - return err + return dto.FilterChainOperationResponse{}, err } - if err := s.policies.DeleteBatch(ctx, uuids); err != nil { - return err + labels := make([]string, len(uuids)) + for i, id := range uuids { + labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id) } - return s.reconcileLocked(ctx) + return queueFirewallRuleTask("Docker", task.TaskDelete, labels, func(ctx context.Context) error { + dockerPortGuardServiceMu.Lock() + defer dockerPortGuardServiceMu.Unlock() + if err := ctx.Err(); err != nil { + return err + } + if err := s.policies.DeleteBatch(ctx, uuids); err != nil { + return err + } + return s.reconcileLocked(ctx) + }) } -func (s *DockerPortGuardService) UpsertPolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatch) error { - dockerPortGuardServiceMu.Lock() - defer dockerPortGuardServiceMu.Unlock() - if err := s.rejectHostInputDockerGuardEndpoints(ctx, request.Endpoints); err != nil { - return err +func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) { + labels := make([]string, len(request.Policies)) + for i, policy := range request.Policies { + labels[i] = fmt.Sprintf("[%d/%d] %s %s %s:%d %s", i+1, len(request.Policies), policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, policy.Mode) } - policies := make([]model.DockerPortGuardPolicy, 0, len(request.Endpoints)) - seen := make(map[string]struct{}, len(request.Endpoints)) - for _, endpoint := range request.Endpoints { - normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{ - Family: endpoint.Family, HostIP: endpoint.HostIP, HostPort: endpoint.HostPort, - Protocol: endpoint.Protocol, Mode: request.Mode, Sources: request.Sources, - }) - if err != nil { + return queueFirewallRuleTask("Docker", task.TaskUpdate, labels, func(ctx context.Context) error { + dockerPortGuardServiceMu.Lock() + defer dockerPortGuardServiceMu.Unlock() + policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies)) + endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies)) + for i, policy := range request.Policies { + if err := ctx.Err(); err != nil { + return err + } + normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{ + Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, + Protocol: policy.Protocol, Mode: policy.Mode, Sources: policy.Sources, + }) + if err != nil { + return fmt.Errorf("%s: %w", labels[i], err) + } + encoded, err := json.Marshal(normalized.Sources) + if err != nil { + return fmt.Errorf("%s: %w", labels[i], err) + } + policies = append(policies, model.DockerPortGuardPolicy{ + UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP, + HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode, + Sources: string(encoded), Description: strings.TrimSpace(policy.Description), + }) + endpoints = append(endpoints, policy.DockerPortGuardEndpointIdentity) + } + if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil { return err } - key := guardEndpointKey(normalized.Family, normalized.HostIP, normalized.HostPort, normalized.Protocol) - if _, exists := seen[key]; exists { - continue + if err := s.policies.UpsertBatch(ctx, policies); err != nil { + return err } - seen[key] = struct{}{} - encoded, _ := json.Marshal(normalized.Sources) - policies = append(policies, model.DockerPortGuardPolicy{ - UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP, - HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode, - Sources: string(encoded), Description: strings.TrimSpace(request.Description), - }) - } - if err := s.policies.UpsertBatch(ctx, policies); err != nil { - return err - } - return s.reconcileLocked(ctx) + return s.reconcileLocked(ctx) + }) } func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints( @@ -407,63 +444,6 @@ func (s *DockerPortGuardService) Reconcile(ctx context.Context) error { return s.reconcileLocked(ctx) } -func (s *DockerPortGuardService) loadRuleSyncCandidates( - ctx context.Context, - request dto.FirewallRuleSyncRequest, -) (string, []model.DockerPortGuardPolicy, dockerGuardRuntime, error) { - targetProvider, err := databaseRuleSyncTarget(request, "Docker") - if err != nil { - return "", nil, nil, err - } - target := string(targetProvider) - selected, err := s.selectedRuleSyncBackend(ctx) - if err != nil { - return "", nil, nil, err - } - if target != selected { - return "", nil, nil, fmt.Errorf( - "%w: selected Docker firewall backend is %s, requested target is %s", - filter.ErrProviderUnavailable, selected, target, - ) - } - policies, err := s.policies.ListManaged(ctx) - if err != nil { - return "", nil, nil, err - } - return target, policies, s.guardRuntime(target), nil -} - -func (s *DockerPortGuardService) selectedRuleSyncBackend(ctx context.Context) (string, error) { - if global.DB != nil { - selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey) - selected = strings.ToLower(strings.TrimSpace(selected)) - if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables { - return selected, nil - } - } - if s.client == nil { - return "", fmt.Errorf("%w: Docker firewall backend is unavailable", ErrDockerUnavailable) - } - cli, err := s.client() - if err != nil { - return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err) - } - defer cli.Close() - info, err := cli.Info(ctx) - if err != nil { - return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err) - } - return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil -} - -func dockerGuardPoliciesFromModels(policies []model.DockerPortGuardPolicy) []docker_guard.Policy { - result := make([]docker_guard.Policy, 0, len(policies)) - for _, policy := range policies { - result = append(result, dockerGuardPolicyFromModel(policy)) - } - return result -} - func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard.Policy { return docker_guard.Policy{ UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, @@ -471,33 +451,6 @@ func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard } } -func dockerGuardRuleSyncDTO(policy model.DockerPortGuardPolicy) *dto.DockerPortGuardEndpoint { - return &dto.DockerPortGuardEndpoint{ - Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, - PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description, - TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, - ManagementReason: dockerReasonNoMatchingPath, - } -} - -func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGuardEndpoint { - return &dto.DockerPortGuardEndpoint{ - Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, - PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...), - TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, - ManagementReason: dockerReasonNoMatchingPath, - } -} - -func dockerGuardReadOnlyRuleSyncDTO(policy docker_guard.ReadOnlyPolicy) *dto.DockerPortGuardEndpoint { - return &dto.DockerPortGuardEndpoint{ - Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort, - Protocol: policy.Policy.Protocol, PolicyUUID: dockerGuardReadOnlyPolicyUUID(policy), Sources: append([]string(nil), policy.Policy.Sources...), - NativeAction: policy.Action, ReadOnly: true, TrafficPath: dockerTrafficPathUnknown, - ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath, - } -} - func dockerGuardReadOnlyPolicyUUID(policy docker_guard.ReadOnlyPolicy) string { nativeRules, _ := json.Marshal(policy.NativeRules) fingerprint := strings.Join([]string{ @@ -576,7 +529,7 @@ func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error return err } if !initialized { - err = runtime.Initialize(policies) + err = initializeDockerGuardRuntime(runtime, policies) } else { err = runtime.Reconcile(policies) } diff --git a/agent/app/service/firewall_panel_port.go b/agent/app/service/firewall_panel_port.go index ef2cf8c5ee54..a026c96cb6b2 100644 --- a/agent/app/service/firewall_panel_port.go +++ b/agent/app/service/firewall_panel_port.go @@ -2,7 +2,6 @@ package service import ( "context" - "errors" "fmt" "strconv" @@ -12,7 +11,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" ) type panelPortWhitelistKey struct{} @@ -97,32 +95,6 @@ func warnPanelPortCleanupFailure(port uint, err error) { } } -func syncPanelRequiredPorts(provider string, ports []firewall.PortWhitelist) error { - loadPorts := func() ([]firewall.PortWhitelist, error) { return ports, nil } - if provider == constant.FirewallProviderIptables { - manager := newIptablesHelperManager() - manager.LoadRequiredPorts = loadPorts - return manager.SyncRequiredPorts(true) - } - initialized := false - for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { - familyInitialized, _, err := nftables_helper.LoadFamilyInitStatus(family, "base") - if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { - continue - } - if err != nil { - return err - } - initialized = initialized || familyInitialized - } - if !initialized { - return nil - } - manager := newNftablesHelperManager() - manager.LoadRequiredPorts = loadPorts - return manager.SyncRequiredPorts() -} - func panelPortStillRequired(port dto.FirewallSystemPort, protected []firewall.PortWhitelist) bool { rule := systemPortRule(filter.ProviderUFW, port) for _, required := range protected { diff --git a/agent/app/service/firewall_rule_task.go b/agent/app/service/firewall_rule_task.go new file mode 100644 index 000000000000..70f9dfae40e0 --- /dev/null +++ b/agent/app/service/firewall_rule_task.go @@ -0,0 +1,58 @@ +package service + +import ( + "context" + "fmt" + "io" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/task" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" +) + +func queueFirewallRuleTask(name, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) { + taskItem, err := task.NewTaskWithOps(name, operation, task.TaskScopeFirewall, "", 0) + if err != nil { + return dto.FilterChainOperationResponse{}, err + } + taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { + t.Logf("rules=%d", len(labels)) + err := t.TaskCtx.Err() + if err == nil { + err = apply(t.TaskCtx) + } + succeeded, failed := 0, 0 + for _, label := range labels { + if err != nil { + failed++ + t.LogFailedWithErr(label, err) + } else { + succeeded++ + t.LogSuccess(label) + } + } + t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{ + "succeeded": succeeded, "failed": failed, + })) + return err + }, nil, 0, 0) + if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { + taskItem.LogFailedWithErr(taskItem.Name, err) + closeUnstartedFirewallTask(taskItem) + return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err) + } + go func() { _ = taskItem.Execute() }() + return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil +} + +func closeUnstartedFirewallTask(t *task.Task) { + if cancel, ok := global.LoadTaskCancel(t.TaskID); ok { + cancel() + } + global.RemoveTaskCancel(t.TaskID) + if closer, ok := t.Logger.Out.(io.Closer); ok { + _ = closer.Close() + } +} diff --git a/agent/app/service/firewall_sync.go b/agent/app/service/firewall_sync.go index 46cdbfa7f4a0..078f4b9505f2 100644 --- a/agent/app/service/firewall_sync.go +++ b/agent/app/service/firewall_sync.go @@ -15,11 +15,12 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" "gorm.io/gorm" ) @@ -29,360 +30,343 @@ var ( firewallRuleSyncTaskID string ) -const ( - firewallRuleSyncReady = firewallsync.StatusReady - firewallRuleSyncExisting = firewallsync.StatusExisting - firewallRuleSyncBlocked = firewallsync.StatusBlocked -) +type firewallDatabaseSyncAdapter interface { + previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) + syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) +} + +type firewallSyncRule struct { + dto.FirewallRuleSyncItem + desired filter.DesiredRule + observed *filter.ObservedRule + done bool +} func firewallSyncSubsystem(value string) string { - value = strings.TrimSpace(value) - if value == "" { + if value = strings.TrimSpace(value); value == "" { return "system" } return value } -type firewallRuleSyncOutcome = firewallsync.Outcome - -const ( - firewallRuleSyncApplied = firewallsync.OutcomeApplied - firewallRuleSyncSkipped = firewallsync.OutcomeSkipped - firewallRuleSyncRemoved = firewallsync.OutcomeRemoved - firewallRuleSyncFailed = firewallsync.OutcomeFailed -) - -type firewallRuleSyncEntry struct { - source model.FirewallRule - rule filter.FirewallRule - desired filter.DesiredRule - remove *filter.ObservedRule - match filter.InventoryMatch - reorder bool - item dto.FirewallRuleSyncItem - err error - outcome firewallRuleSyncOutcome - failure error -} - -type firewallRuleSyncScopePlan struct { - runtime *filterruntime.Engine - snapshot filter.Snapshot - entries []*firewallRuleSyncEntry -} - -type firewallSystemSyncPlan struct { - target filter.Provider - entries []*firewallRuleSyncEntry - database databaseSyncPlan - scopes []firewallRuleSyncScopePlan -} - -func (s *FirewallService) PreviewRuleSync( - ctx context.Context, - clientIP string, - request dto.FirewallRuleSyncRequest, -) (dto.FirewallRuleSyncPreview, error) { +func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) { switch firewallSyncSubsystem(request.Subsystem) { case "forwarding": return s.forwardingRuleSyncService().previewRuleSync(ctx, request) case "docker": return s.dockerRuleSyncService().previewRuleSync(ctx, request) - default: - return s.previewSystemRuleSync(ctx, clientIP, request) } -} - -func (s *FirewallService) previewSystemRuleSync( - ctx context.Context, - clientIP string, - request dto.FirewallRuleSyncRequest, -) (dto.FirewallRuleSyncPreview, error) { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() - plan, err := s.loadFirewallRuleSyncPlan(ctx, clientIP, request) - if err != nil { - return dto.FirewallRuleSyncPreview{}, err + _, rules, _, err := s.loadFirewallSyncRules(ctx, request) + preview := dto.FirewallRuleSyncPreview{Subsystem: "system", TargetProvider: request.TargetProvider, Items: make([]dto.FirewallRuleSyncItem, 0, len(rules))} + for _, rule := range rules { + preview.Add(rule.FirewallRuleSyncItem) } - return plan.database.preview(), nil + return preview, err } -func (s *FirewallService) syncRules( - ctx context.Context, - clientIP string, - request dto.FirewallRuleSyncRequest, -) (dto.FirewallRuleSyncResult, error) { - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - plan, err := s.loadFirewallRuleSyncPlan(ctx, clientIP, request) - if err != nil { - return dto.FirewallRuleSyncResult{}, err - } - if plan.database.preview().Blocked > 0 { - return plan.database.validationResult(), nil - } - return s.executeFirewallSystemSyncPlan(ctx, clientIP, plan), nil -} - -func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider) error { - result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{ - Subsystem: "system", - TargetProvider: provider, - }) - if err != nil { - return fmt.Errorf("restore database firewall rules: %w", err) - } - if result.Failed == 0 { - return nil - } - messages := firewallRuleSyncFailureMessages(result.Errors) - if len(messages) == 0 { - return fmt.Errorf("restore database firewall rules: %d rules failed", result.Failed) - } - return fmt.Errorf("restore database firewall rules: %s", strings.Join(messages, "; ")) -} - -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 +func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (*filterruntime.Engine, []*firewallSyncRule, []filter.Snapshot, error) { + if request.SourceProvider != "" || request.ResetSource { + return nil, nil, nil, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule) } - if selected != filter.ProviderIptables && selected != filter.ProviderUFW { - return fmt.Errorf("%w: selected provider %s does not require legacy ownership transfer", filter.ErrProviderUnavailable, selected) + if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil { + return nil, nil, nil, err } - runtime, err := s.adapters.Resolve(selected) + runtime, err := s.adapters.Resolve(request.TargetProvider) if err != nil { - return err + return nil, nil, nil, 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) + return nil, nil, nil, err } - for _, scope := range filter.ManagedInputScopes(selected) { - desired := desiredByScope[scope.Key()] - if len(desired) == 0 { + model.SortFirewallRules(stored, request.TargetProvider) + rules := make([]*firewallSyncRule, 0, len(stored)) + compileFailed := false + for _, record := range stored { + desired, err := s.compileStoredFirewallRules(ctx, record, request.TargetProvider) + if err != nil { + compileFailed = true + rules = append(rules, &firewallSyncRule{FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Status: firewallsync.StatusBlocked, Reason: err.Error()}}) + continue + } + for _, native := range desired { + rule := native.Rule + rules = append(rules, &firewallSyncRule{desired: native, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Rule: &rule}}) + } + } + snapshots := make([]filter.Snapshot, 0) + for _, scope := range filter.ManagedInputScopes(request.TargetProvider) { + desired := make([]filter.DesiredRule, 0) + scoped := make([]*firewallSyncRule, 0) + byUUID := make(map[string]*firewallSyncRule) + for _, rule := range rules { + if rule.Rule != nil && rule.Rule.Scope.Key() == scope.Key() { + scoped = append(scoped, rule) + desired = append(desired, rule.desired) + byUUID[rule.desired.Rule.UUID] = rule + } + } + if compileFailed && len(desired) == 0 { continue } snapshot, err := runtime.ObserveMutation(ctx, scope) + if errors.Is(err, filter.ErrFamilyUnavailable) { + for _, rule := range scoped { + rule.Status, rule.Reason = firewallsync.StatusBlocked, err.Error() + } + continue + } if err != nil { - return err + return nil, nil, nil, 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) { + snapshots = append(snapshots, snapshot) + inventory, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) + if err != nil { + return nil, nil, nil, err + } + matched := make(map[string]filter.InventoryItem) + for _, item := range inventory { + if item.Desired == nil { + if compileFailed || item.Observed == nil || !strings.HasPrefix(item.Observed.Marker, "1panel-rule:") { continue } - candidate = item - break + observed := item.Observed + rule := &firewallSyncRule{observed: observed, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{ + SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &observed.Rule, Status: firewallsync.StatusRemove, + ReasonCode: firewallsync.ReasonManagedOnlyInTarget, Reason: firewallsync.ReasonMessage(firewallsync.ReasonManagedOnlyInTarget), + }} + if observed.Protected || observed.ParseStatus == filter.ParseStatusOpaque { + rule.Status, rule.ReasonCode = firewallsync.StatusBlocked, firewallsync.ReasonUnsafeRemoval + rule.Reason = firewallsync.ReasonMessage(rule.ReasonCode) + } + rules = append(rules, rule) + continue } - if candidate == nil { - break + rule := byUUID[item.Desired.Rule.UUID] + matched[item.Desired.Rule.UUID] = item + rule.observed = item.Observed + divergent := item.Observed != nil && item.Observed.Persistence != "" && item.Observed.Persistence != filter.PersistenceStatusConverged + switch { + case item.Match == filter.InventoryMatchExact && !divergent: + rule.Status, rule.Reason = firewallsync.StatusExisting, "rule already matches database policy" + case item.Match == filter.InventoryMatchMissing || item.Match == filter.InventoryMatchChanged || item.Match == filter.InventoryMatchExact: + rule.Status, rule.Reason = firewallsync.StatusReady, "target rule differs from database policy" + if item.Observed != nil && item.Observed.Protected { + rule.Status, rule.Reason = firewallsync.StatusBlocked, filter.ErrProtectedRule.Error() + } + default: + rule.Status, rule.Reason = firewallsync.StatusBlocked, fmt.Sprintf("target rule cannot be synchronized: %s", item.Match) } - 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 + } + ordered := make([]filter.InventoryItem, 0, len(scoped)) + for _, rule := range scoped { + ordered = append(ordered, matched[rule.desired.Rule.UUID]) + } + drifted := firewallsync.RuleOrder(snapshot, ordered) + for _, rule := range scoped { + if !drifted[rule.desired.Marker] { + continue } - if !verification.Matched { - return filter.ErrVerificationFailed + if rule.Status == firewallsync.StatusExisting { + rule.Status, rule.Reason = firewallsync.StatusReady, "managed rule order differs from database sequence" } - snapshot = verification.Snapshot } } - if selected == filter.ProviderIptables { - if err := iptables_helper.CleanupLegacyAdvancedChains(ctx); err != nil { - return err - } - } - return nil + return runtime, rules, snapshots, nil } -func (s *FirewallService) loadFirewallRuleSyncPlan( - ctx context.Context, - clientIP string, - request dto.FirewallRuleSyncRequest, -) (firewallSystemSyncPlan, error) { - target, entries, hasCompileErrors, err := s.loadStoredFirewallRuleSyncCandidates(ctx, request) - if err != nil { - return firewallSystemSyncPlan{}, err +func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.FirewallRuleSyncRequest, t *task.Task) (result dto.FirewallRuleSyncResult, err error) { + firewallRuleMutationMu.Lock() + defer firewallRuleMutationMu.Unlock() + result = dto.FirewallRuleSyncResult{Subsystem: "system", TargetProvider: request.TargetProvider} + if err := ctx.Err(); err != nil { + return result, err } - runtime, err := s.adapters.Resolve(target) + runtime, rules, snapshots, err := s.loadFirewallSyncRules(ctx, request) if err != nil { - return firewallSystemSyncPlan{}, err - } - - entriesByScope := make(map[string][]*firewallRuleSyncEntry) - expectedMarkers := make(map[string]struct{}) - for _, entry := range entries { - entry.item = dto.FirewallRuleSyncItem{SourceUUID: entry.source.UUID, Rule: &entry.rule} - if entry.err != nil { - entry.item.Status, entry.item.Reason = firewallRuleSyncBlocked, entry.err.Error() - continue + return result, err + } + created, removed, unexecuted := 0, 0, 0 + stopped := make(map[string]error) + failedRemovals := make(map[string]error) + record := func(operation string, rule *firewallSyncRule, cause error, skipped bool) { + item := rule.FirewallRuleSyncItem + if operation == "TaskDelete" && rule.observed != nil { + item.Rule = &rule.observed.Rule + } + switch { + case skipped: + result.Skipped++ + unexecuted++ + rule.done = true + case cause != nil: + appendDatabaseSyncFailure(&result, item, cause) + rule.done = true + if operation == "TaskDelete" { + failedRemovals[rule.SourceUUID] = cause + } + case operation == "TaskDelete": + removed++ + if rule.Status == firewallsync.StatusRemove { + result.Removed++ + rule.done = true + } + case operation == "TaskCreate": + created++ + result.Succeeded++ + rule.done = true + default: + result.Skipped++ + rule.done = true + } + if t == nil { + return + } + label := fmt.Sprintf("%s %s", i18n.GetMsgByKey(operation), item.SourceUUID) + if r := item.Rule; r != nil { + label += fmt.Sprintf(" [%s] %s %s:%s -> %s:%s %s", r.Scope.Key(), r.Protocol, r.SourceAddress, r.SourcePort, r.DestinationAddress, r.DestinationPort, r.Action) + } + if skipped { + t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause) + } else { + t.LogWithStatus(label, cause) } - scopeKey := entry.rule.Scope.Key() - entriesByScope[scopeKey] = append(entriesByScope[scopeKey], entry) - expectedMarkers[scopeKey+"\x00"+entry.desired.Marker] = struct{}{} } - - scopes := filter.ManagedInputScopes(target) - if hasCompileErrors { - scopes = scopesWithFirewallSyncCandidates(scopes, entriesByScope) - } - scopePlans := make([]firewallRuleSyncScopePlan, 0, len(scopes)) - seenSnapshots := make(map[string]struct{}, len(scopes)) - for _, scope := range scopes { - snapshot, observeErr := runtime.ObserveMutation(ctx, scope) - if errors.Is(observeErr, filter.ErrFamilyUnavailable) { - for _, entry := range entriesByScope[scope.Key()] { - entry.err = observeErr - entry.item.Status, entry.item.Reason = firewallRuleSyncBlocked, observeErr.Error() - } - continue + defer func() { + if t != nil { + t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted})) } - if observeErr != nil { - return firewallSystemSyncPlan{}, observeErr + }() + blocked := false + for _, rule := range rules { + if rule.Status != firewallsync.StatusRemove { + result.Total++ } - scopeKey := snapshot.Scope.Key() - if _, seen := seenSnapshots[scopeKey]; seen { - continue + if rule.Status == firewallsync.StatusBlocked { + blocked = true + record("TaskSync", rule, errors.New(rule.Reason), false) } - seenSnapshots[scopeKey] = struct{}{} - scopeEntries := append([]*firewallRuleSyncEntry(nil), entriesByScope[scopeKey]...) - desired := make([]filter.DesiredRule, 0, len(scopeEntries)) - byRuleUUID := make(map[string]*firewallRuleSyncEntry, len(scopeEntries)) - for _, entry := range scopeEntries { - desired = append(desired, entry.desired) - ruleUUID := strings.TrimSpace(entry.desired.Rule.UUID) - if ruleUUID == "" { - return firewallSystemSyncPlan{}, fmt.Errorf("%w: compiled database rule has no runtime UUID", filter.ErrInvalidRule) - } - if _, exists := byRuleUUID[ruleUUID]; exists { - return firewallSystemSyncPlan{}, fmt.Errorf("%w: duplicate compiled runtime rule UUID %q", filter.ErrInvalidRule, ruleUUID) + } + if blocked { + for _, rule := range rules { + if !rule.done { + record("TaskSync", rule, filter.ErrRuleOperation, true) } - byRuleUUID[ruleUUID] = entry } - inventory, mergeErr := filter.MergeInventory(filter.InventoryMergeInput{ - Observed: snapshot.Rules, Desired: desired, - }) - if mergeErr != nil { - return firewallSystemSyncPlan{}, mergeErr + return result, nil + } + for _, rule := range rules { + if rule.Status == firewallsync.StatusExisting { + record("TaskSync", rule, nil, false) } - for itemIndex := range inventory { - inventoryItem := inventory[itemIndex] - if inventoryItem.Desired == nil { - continue - } - entry, exists := byRuleUUID[strings.TrimSpace(inventoryItem.Desired.Rule.UUID)] - if !exists { - continue - } - entry.match = inventoryItem.Match - entry.item.Status, entry.item.Reason = s.classifyFirewallRuleSyncCandidate( - clientIP, entry, inventoryItem, - ) + } + for _, operation := range []filter.ChangeOperation{filter.ChangeDelete, filter.ChangeCreate} { + name := "TaskCreate" + if operation == filter.ChangeDelete { + name = "TaskDelete" } - if !hasCompileErrors { - for observedIndex := range snapshot.Rules { - observed := snapshot.Rules[observedIndex] - if !strings.HasPrefix(observed.Marker, "1panel-rule:") { + for _, initial := range snapshots { + scope := initial.Scope + queue := make([]*firewallSyncRule, 0) + for _, rule := range rules { + if rule.done || rule.Rule.Scope.Key() != scope.Key() { continue } - if _, exists := expectedMarkers[scopeKey+"\x00"+observed.Marker]; exists { + if operation == filter.ChangeDelete && rule.observed != nil || operation == filter.ChangeCreate && rule.Status == firewallsync.StatusReady { + queue = append(queue, rule) + } + } + if operation == filter.ChangeDelete { + sort.SliceStable(queue, func(i, j int) bool { + return syncObservedPosition(queue[i].observed) > syncObservedPosition(queue[j].observed) + }) + } + for start := 0; start < len(queue); { + rule := queue[start] + cause := stopped[scope.Key()] + if operation == filter.ChangeCreate && cause == nil { + for id, err := range failedRemovals { + if id == rule.SourceUUID || strings.HasPrefix(id, rule.SourceUUID+"-") { + cause = err + break + } + } + } + if cause != nil { + record(name, rule, cause, true) + start++ continue } - copy := observed - entry := &firewallRuleSyncEntry{ - source: model.FirewallRule{UUID: strings.TrimPrefix(observed.Marker, "1panel-rule:")}, - rule: observed.Rule, remove: ©, + snapshot := initial + err := ctx.Err() + if err == nil { + snapshot, err = runtime.ObserveMutation(ctx, scope) } - status := firewallsync.StatusRemove - reasonCode := firewallsync.ReasonManagedOnlyInTarget - reason := firewallsync.ReasonMessage(reasonCode) - if observed.Protected || observed.ParseStatus == filter.ParseStatusOpaque { - status = firewallRuleSyncBlocked - reasonCode = firewallsync.ReasonUnsafeRemoval - reason = firewallsync.ReasonMessage(reasonCode) - entry.err = filter.ErrProtectedRule + unreadable := err != nil + end := start + 1 + if supportsNativeRuleBatch(scope.Provider) && (operation == filter.ChangeDelete || len(snapshot.Rules) == 0) && len(failedRemovals) == 0 { + end = min(start+filter.MaxAtomicExpansion, len(queue)) + } + batch := queue[start:end] + changes := make([]filter.DesiredChange, 0, len(batch)) + for _, entry := range batch { + if err != nil { + break + } + if operation == filter.ChangeDelete { + var change filter.DesiredChange + change, err = firewallsync.DeleteChange(snapshot, *entry.observed, entry.desired) + changes = append(changes, change) + } else { + after := *entry.Rule + 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 { + after.OrderIndex = firewallsync.InsertionPosition(snapshot, markers, entry.desired.Marker) + } + changes = append(changes, filter.DesiredChange{Operation: operation, After: &after, Append: scope.Provider == filter.ProviderUFW && after.OrderIndex == nil}) + } + } + if err == nil { + err = runtime.ExecuteSync(ctx, snapshot, changes) + } + for _, entry := range batch { + record(name, entry, err, false) } - entry.item = dto.FirewallRuleSyncItem{ - SourceUUID: entry.source.UUID, Rule: &entry.rule, - Status: status, ReasonCode: reasonCode, Reason: reason, + if err != nil && (operation == filter.ChangeDelete || scope.Provider != filter.ProviderFirewalld || unreadable || firewallCreateUnavailable(err)) { + stopped[scope.Key()] = err } - entries = append(entries, entry) - scopeEntries = append(scopeEntries, entry) + start = end } } - planFirewallManagedOrder(snapshot, scopeEntries) - scopePlans = append(scopePlans, firewallRuleSyncScopePlan{ - runtime: runtime, snapshot: snapshot, entries: scopeEntries, - }) } - items := make([]dto.FirewallRuleSyncItem, 0, len(entries)) - for _, entry := range entries { - items = append(items, entry.item) + return result, nil +} + +func syncObservedPosition(rule *filter.ObservedRule) int { + if rule.Locator.Position != nil { + return *rule.Locator.Position } - return firewallSystemSyncPlan{ - target: target, entries: entries, - database: databaseSyncPlan{subsystem: firewallSyncSubsystem(request.Subsystem), target: target, items: items}, - scopes: scopePlans, - }, nil + return 0 } -func planFirewallManagedOrder( - snapshot filter.Snapshot, - entries []*firewallRuleSyncEntry, -) { - byMarker := make(map[string]*firewallRuleSyncEntry, len(entries)) - desiredMarkers := make([]string, 0, len(entries)) - for _, entry := range entries { - if entry.remove == nil && entry.err == nil && entry.desired.Marker != "" { - byMarker[entry.desired.Marker] = entry - desiredMarkers = append(desiredMarkers, entry.desired.Marker) - } +func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider) error { + result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, nil) + if err != nil { + return fmt.Errorf("restore database firewall rules: %w", err) } - drifted := firewallsync.ManagedOrderDrift(snapshot, desiredMarkers) - for _, marker := range desiredMarkers { - if _, exists := drifted[marker]; !exists { - continue - } - entry := byMarker[marker] - entry.reorder = true - if entry.item.Status == firewallRuleSyncExisting { - entry.item.Status = firewallRuleSyncReady - entry.item.Reason = "managed rule order differs from database sequence" - } + failures := make([]error, 0, len(result.Errors)) + for _, failure := range result.Errors { + failures = append(failures, fmt.Errorf("rule %s: %s", failure.SourceUUID, failure.Error)) } + return errors.Join(failures...) } func (s *FirewallService) SyncRules( @@ -418,61 +402,26 @@ func (s *FirewallService) syncSystemRules( if firewallSyncSubsystem(request.Subsystem) != "system" { return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule) } - preview, err := s.previewSystemRuleSync(ctx, clientIP, request) - if err != nil { - return dto.FirewallRuleSyncResult{}, err - } - resourceName := fmt.Sprintf("database -> %s", request.TargetProvider) - taskItem, err := task.NewTaskWithOps(resourceName, task.TaskSync, task.TaskScopeFirewall, request.TaskID, 0) + taskItem, err := task.NewTaskWithOps(resourceName, task.TaskSync, task.TaskScopeFirewall, "", 0) if err != nil { - return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall migration task: %w", err) - } - taskRequest := request - var syncResult dto.FirewallRuleSyncResult - taskItem.AddSubTask(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error { - var syncErr error - syncResult, syncErr = s.syncRules(t.TaskCtx, clientIP, taskRequest) - if syncErr != nil { - return syncErr - } - t.Log(i18n.GetMsgWithMap("FirewallSyncResult", map[string]interface{}{ - "succeeded": syncResult.Succeeded, - "existing": syncResult.Skipped, - "failed": syncResult.Failed, - })) - if syncResult.Failed > 0 { - for _, message := range firewallRuleSyncFailureMessages(syncResult.Errors) { - t.Log(message) - } - return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": syncResult.Failed})) - } - return nil - }, nil) - taskItem.AddSubTask(i18n.GetMsgByKey("FirewallVerifyTargetStep"), func(t *task.Task) error { - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - verified, verifyErr := s.loadFirewallRuleSyncPlan(t.TaskCtx, clientIP, taskRequest) - if verifyErr != nil { - return verifyErr + return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall sync task: %w", err) + } + taskItem.AddSubTaskWithOps(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error { + result, err := s.syncRules(t.TaskCtx, clientIP, request, t) + if err != nil { + return err } - for _, item := range verified.database.items { - if item.Status == firewallRuleSyncExisting { - continue - } - reason := item.Reason - if reason == "" { - reason = i18n.GetMsgByKey("FirewallTargetRuleIneffective") - } - return errors.New(i18n.GetMsgWithMap("FirewallVerifyRuleFailed", map[string]interface{}{ - "name": item.SourceUUID, "detail": reason, - })) + if result.Failed > 0 { + return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": result.Failed})) } return nil - }, nil) + }, nil, 0, 0) if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { - return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall migration task: %w", err) + taskItem.LogFailedWithErr(taskItem.Name, err) + closeUnstartedFirewallTask(taskItem) + return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall sync task: %w", err) } firewallRuleSyncTaskID = taskItem.TaskID go func() { @@ -483,62 +432,18 @@ func (s *FirewallService) syncSystemRules( } firewallRuleSyncTaskMu.Unlock() }() - _ = taskItem.Execute() + if err := taskItem.Execute(); err != nil && global.LOG != nil { + global.LOG.Errorf("firewall sync task %s failed: %v", taskItem.TaskID, err) + } }() return dto.FirewallRuleSyncResult{ Subsystem: "system", TargetProvider: request.TargetProvider, - Total: preview.Total, TaskID: taskItem.TaskID, Queued: true, }, nil } -func firewallRuleSyncFailureMessages(failures []dto.FirewallRuleSyncFailure) []string { - type failureGroup struct { - detail string - uuids []string - } - groups := make([]failureGroup, 0, len(failures)) - groupByDetail := make(map[string]int, len(failures)) - for _, failure := range failures { - detail := strings.TrimSpace(failure.Error) - if detail == "" { - detail = "database synchronization failed" - } - uuid := strings.TrimSpace(failure.SourceUUID) - if uuid == "" { - uuid = "-" - } - if index, exists := groupByDetail[detail]; exists { - groups[index].uuids = append(groups[index].uuids, uuid) - continue - } - groupByDetail[detail] = len(groups) - groups = append(groups, failureGroup{detail: detail, uuids: []string{uuid}}) - } - - messages := make([]string, 0, len(groups)) - for _, group := range groups { - messages = append(messages, fmt.Sprintf("UUID [%s]: %s", strings.Join(group.uuids, ", "), group.detail)) - } - return messages -} - -func (s *FirewallService) forwardingRuleSyncService() firewallDatabaseSyncAdapter { - if s.forwardingSync == nil { - return newForwardingService() - } - return s.forwardingSync -} - -func (s *FirewallService) dockerRuleSyncService() firewallDatabaseSyncAdapter { - if s.dockerSync == nil { - return newDockerPortGuardService() - } - return s.dockerSync -} - func (s *FirewallService) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) { firewallRuleSyncTaskMu.Lock() defer firewallRuleSyncTaskMu.Unlock() @@ -576,319 +481,195 @@ func runningFirewallRuleSyncResult(request dto.FirewallRuleSyncRequest, taskID s } } -func (s *FirewallService) loadStoredFirewallRuleSyncCandidates( - ctx context.Context, - request dto.FirewallRuleSyncRequest, -) (filter.Provider, []*firewallRuleSyncEntry, bool, error) { - if request.SourceProvider != "" || request.ResetSource { - return "", nil, false, fmt.Errorf("%w: system firewall synchronization reads desired rules from the database", filter.ErrInvalidRule) +func (s *FirewallService) syncConfiguredFirewallPorts(ctx context.Context) error { + configured, err := loadConfiguredFirewallPortWhiteList() + if err != nil { + return err } - selected, err := s.selectedProvider(ctx) + required, err := LoadRequiredFirewallPortWhiteList() if err != nil { - return "", nil, false, err + return err } - if request.TargetProvider != selected { - return "", nil, false, fmt.Errorf( - "%w: selected provider is %s, requested target is %s", - filter.ErrProviderUnavailable, selected, request.TargetProvider, - ) + ports := excludeFirewallPorts(configured, required) + return s.SyncSystemPorts(ctx, nil, systemPorts(ports)) +} + +func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error { + previousSet, err := normalizeSystemPorts(previous) + if err != nil { + return err } - stored, err := s.rules.List(ctx) + currentSet, err := normalizeSystemPorts(current) if err != nil { - return "", nil, false, err + return err } - model.SortFirewallRules(stored, selected) - entries := make([]*firewallRuleSyncEntry, 0, len(stored)) - hasCompileErrors := false - for _, record := range stored { - desired, convertErr := s.compileStoredFirewallRules(ctx, record, selected) - if convertErr != nil { - hasCompileErrors = true - entries = append(entries, &firewallRuleSyncEntry{source: record, err: convertErr}) - continue + + provider, err := s.selectedProvider(ctx) + if err != nil { + return err + } + if !supportsNativeRuleBatch(provider) { + var syncErrors []error + for _, key := range sortedSystemPortKeys(currentSet) { + if _, exists := previousSet[key]; exists { + continue + } + if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil { + wrapped := fmt.Errorf("restore accepted firewall port %s: %w", key, err) + syncErrors = append(syncErrors, wrapped) + if global.LOG != nil { + global.LOG.Errorf("%v", wrapped) + } + } } - for _, compiled := range desired { - entries = append(entries, &firewallRuleSyncEntry{ - source: record, rule: compiled.Rule, desired: compiled, - }) + for _, key := range sortedSystemPortKeys(previousSet) { + if _, exists := currentSet[key]; exists { + continue + } + if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil { + wrapped := fmt.Errorf("release accepted firewall port %s: %w", key, err) + syncErrors = append(syncErrors, wrapped) + if global.LOG != nil { + global.LOG.Errorf("%v", wrapped) + } + } } + return errors.Join(syncErrors...) } - return selected, entries, hasCompileErrors, nil -} -func scopesWithFirewallSyncCandidates(scopes []filter.Scope, candidates map[string][]*firewallRuleSyncEntry) []filter.Scope { - result := make([]filter.Scope, 0, len(candidates)) - for _, scope := range scopes { - if len(candidates[scope.Key()]) > 0 { - result = append(result, scope) + for _, key := range sortedSystemPortKeys(currentSet) { + if _, exists := previousSet[key]; exists { + continue + } + if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil { + return err } } - return result -} -func (s *FirewallService) classifyFirewallRuleSyncCandidate( - clientIP string, - entry *firewallRuleSyncEntry, - item filter.InventoryItem, -) (firewallsync.Status, string) { - switch item.Match { - case filter.InventoryMatchExact: - return firewallRuleSyncExisting, "rule already matches database policy" - case filter.InventoryMatchMissing: - return firewallRuleSyncReady, "rule is missing from target backend" - case filter.InventoryMatchChanged: - if item.Observed == nil { - return firewallRuleSyncBlocked, filter.ErrRuleStale.Error() + for _, key := range sortedSystemPortKeys(previousSet) { + if _, exists := currentSet[key]; exists { + continue } - if err := filter.GuardMutation(*item.Observed); err != nil { - return firewallRuleSyncBlocked, err.Error() + if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil { + return err } - return firewallRuleSyncReady, "target rule differs from database policy" - default: - return firewallRuleSyncBlocked, fmt.Sprintf("target rule cannot be reconciled: %s", item.Match) } + return nil } -func firewallRuleSyncInventoryFromSnapshot( - snapshot filter.Snapshot, - entry *firewallRuleSyncEntry, -) (filter.InventoryItem, error) { - items, err := filter.MergeInventory(filter.InventoryMergeInput{ - Observed: snapshot.Rules, - Desired: []filter.DesiredRule{entry.desired}, - }) - if err != nil { - return filter.InventoryItem{}, err - } - for _, item := range items { - if item.Desired != nil && item.Desired.Marker == entry.desired.Marker { - return item, nil - } - } - return filter.InventoryItem{}, fmt.Errorf( - "%w: compiled database rule %q was not found in target inventory", - filter.ErrVerificationFailed, entry.source.UUID, - ) +func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error { + return newFirewallService(). + SyncSystemPorts(context.Background(), systemPorts(previous), systemPorts(current)) } -func (s *FirewallService) executeFirewallSystemSyncPlan( - ctx context.Context, - clientIP string, - plan firewallSystemSyncPlan, -) dto.FirewallRuleSyncResult { - for _, scopePlan := range plan.scopes { - reconciler := firewallScopeReconciler{ - ctx: ctx, - runtime: scopePlan.runtime, snapshot: scopePlan.snapshot, entries: scopePlan.entries, - } - reconciler.reconcile() +func syncPanelRequiredPorts(provider string, ports []firewall.PortWhitelist) error { + loadPorts := func() ([]firewall.PortWhitelist, error) { return ports, nil } + if provider == constant.FirewallProviderIptables { + manager := newIptablesHelperManager() + manager.LoadRequiredPorts = loadPorts + return manager.SyncRequiredPorts(true) } - return plan.result() -} - -func (p firewallSystemSyncPlan) result() dto.FirewallRuleSyncResult { - result := p.database.baseResult() - for _, entry := range p.entries { - switch entry.outcome { - case firewallRuleSyncApplied: - result.Succeeded++ - case firewallRuleSyncSkipped: - result.Skipped++ - case firewallRuleSyncRemoved: - result.Removed++ - case firewallRuleSyncFailed: - appendDatabaseSyncFailure(&result, entry.item, entry.failure) - default: - if entry.item.Status == firewallRuleSyncExisting { - result.Skipped++ - } + initialized := false + for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { + familyInitialized, _, err := nftables_helper.LoadFamilyInitStatus(family, "base") + if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { + continue + } + if err != nil { + return err } + initialized = initialized || familyInitialized } - return result -} - -func (e *firewallRuleSyncEntry) fail(err error) { - if e.outcome == firewallRuleSyncFailed { - return + if !initialized { + return nil } - e.outcome = firewallRuleSyncFailed - e.failure = err -} - -type databaseSyncDesired[T any] struct { - value T - item dto.FirewallRuleSyncItem - err error -} - -type databaseSyncPlan struct { - subsystem string - target filter.Provider - items []dto.FirewallRuleSyncItem + manager := newNftablesHelperManager() + manager.LoadRequiredPorts = loadPorts + return manager.SyncRequiredPorts() } -type firewallDatabaseSyncAdapter interface { - previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) - syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) +type forwardingRuleSyncCandidate struct { + rule forwarding.Rule + err error } -func buildDatabaseSyncPlan[T any]( - subsystem string, - target filter.Provider, - desired []databaseSyncDesired[T], - actual []T, - key func(T) string, - actualItem func(T) dto.FirewallRuleSyncItem, -) databaseSyncPlan { - candidates := make([]firewallsync.Desired[T, dto.FirewallRuleSyncItem], 0, len(desired)) - for _, candidate := range desired { - candidates = append(candidates, firewallsync.Desired[T, dto.FirewallRuleSyncItem]{ - Value: candidate.value, Payload: candidate.item, Err: candidate.err, - }) +func (s *ForwardingService) loadRuleSyncCandidates( + ctx context.Context, + targetProvider filter.Provider, +) (*forwarding.Manager, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) { + target, err := s.manager() + if err != nil { + return nil, nil, nil, false, err } - diff := firewallsync.Diff(candidates, actual, key, actualItem) - items := make([]dto.FirewallRuleSyncItem, 0, len(diff)) - for _, diffItem := range diff { - item := diffItem.Payload - item.Status, item.ReasonCode, item.Reason = diffItem.Status, diffItem.ReasonCode, diffItem.Reason - items = append(items, item) + if target.Name() != string(targetProvider) { + return nil, nil, nil, false, fmt.Errorf( + "%w: selected forwarding backend is %s, requested target is %s", + filter.ErrProviderUnavailable, target.Name(), targetProvider, + ) } - return databaseSyncPlan{subsystem: subsystem, target: target, items: items} -} - -func databaseSyncStatesEqual[T any](left, right []T, key func(T) string) bool { - return firewallsync.StatesEqual(left, right, key) -} - -func (p databaseSyncPlan) preview() dto.FirewallRuleSyncPreview { - result := dto.FirewallRuleSyncPreview{ - Subsystem: p.subsystem, TargetProvider: p.target, - Items: append(make([]dto.FirewallRuleSyncItem, 0, len(p.items)), p.items...), + stored, err := s.rules.List(ctx) + if err != nil { + return nil, nil, nil, false, err } - for _, item := range p.items { - switch item.Status { - case firewallRuleSyncReady: - result.Ready++ - result.Total++ - case firewallRuleSyncExisting: - result.Existing++ - result.Total++ - case firewallRuleSyncBlocked: - result.Blocked++ - if item.ReasonCode != firewallsync.ReasonReadOnlyRule { - result.Total++ - } - case firewallsync.StatusRemove: - result.Removed++ + candidates := make([]forwardingRuleSyncCandidate, 0, len(stored)) + for _, record := range stored { + rule := forwarding.Rule{ + Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP, + TargetPort: record.TargetPort, Interface: record.Interface, } + normalized, normalizeErr := forwarding.NormalizeRule(rule) + candidates = append(candidates, forwardingRuleSyncCandidate{rule: normalized, err: normalizeErr}) } - return result -} - -func (p databaseSyncPlan) baseResult() dto.FirewallRuleSyncResult { - result := dto.FirewallRuleSyncResult{Subsystem: p.subsystem, TargetProvider: p.target} - for _, item := range p.items { - if item.Status != firewallsync.StatusRemove { - result.Total++ - } + targetStatus, err := target.Status() + if err != nil { + return nil, nil, nil, false, err } - return result -} - -func (p databaseSyncPlan) completedResult() dto.FirewallRuleSyncResult { - result := p.baseResult() - for _, item := range p.items { - switch item.Status { - case firewallRuleSyncReady: - result.Succeeded++ - case firewallRuleSyncExisting: - result.Skipped++ - case firewallRuleSyncBlocked: - appendDatabaseSyncFailure(&result, item, errors.New(item.Reason)) - case firewallsync.StatusRemove: - result.Removed++ + targetRules := make([]forwarding.Rule, 0) + if targetStatus.IsInit { + targetRules, err = target.List("", "") + if err != nil { + return nil, nil, nil, false, err } - } - return result -} - -func (p databaseSyncPlan) validationResult() dto.FirewallRuleSyncResult { - result := p.baseResult() - for _, item := range p.items { - switch item.Status { - case firewallRuleSyncExisting: - result.Skipped++ - case firewallRuleSyncBlocked: - appendDatabaseSyncFailure(&result, item, errors.New(item.Reason)) + targetRules, err = normalizeForwardingRuntimeRules(targetRules) + if err != nil { + return nil, nil, nil, false, err } } - return result + return target, candidates, targetRules, targetStatus.IsInit, nil } -func (p databaseSyncPlan) failedResult(cause error) dto.FirewallRuleSyncResult { - result := p.baseResult() - for _, item := range p.items { - switch item.Status { - case firewallRuleSyncExisting: - result.Skipped++ - case firewallRuleSyncReady: - appendDatabaseSyncFailure(&result, item, cause) - case firewallRuleSyncBlocked: - appendDatabaseSyncFailure(&result, item, errors.New(item.Reason)) - } +func verifyForwardingRuleSync(target *forwarding.Manager, desired []forwarding.Rule) error { + actual, err := target.List("", "") + if err != nil { + return fmt.Errorf("verify synchronized forwarding rules: %w", err) } - return result -} - -func (p databaseSyncPlan) reconcile(run func() error) (dto.FirewallRuleSyncResult, error) { - if p.preview().Blocked > 0 { - return p.validationResult(), nil + actual, err = normalizeForwardingRuntimeRules(actual) + if err != nil { + return fmt.Errorf("verify synchronized forwarding rules: %w", err) } - if err := run(); err != nil { - return p.failedResult(err), err + if !firewallsync.StatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) { + return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database") } - return p.completedResult(), nil + return nil } -func (p databaseSyncPlan) reconcileReadOnlyPartial(run func() error) (dto.FirewallRuleSyncResult, error) { - preview := p.preview() - for _, item := range p.items { - if item.Status == firewallRuleSyncBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule { - return p.validationResult(), nil +func normalizeForwardingRuntimeRules(rules []forwarding.Rule) ([]forwarding.Rule, error) { + normalized := make([]forwarding.Rule, 0, len(rules)) + for _, rule := range rules { + item, err := forwarding.NormalizeRule(rule) + if err != nil { + return nil, fmt.Errorf("normalize target forwarding rule %s: %w", rule.Identity(), err) } + normalized = append(normalized, item) } - if preview.Ready == 0 && preview.Removed == 0 { - return p.validationResult(), nil - } - if err := run(); err != nil { - return p.failedResult(err), err - } - return p.completedResult(), nil + return normalized, nil } -func appendDatabaseSyncFailure(result *dto.FirewallRuleSyncResult, item dto.FirewallRuleSyncItem, err error) { - if err == nil { - err = errors.New("database synchronization failed") +func forwardingRuleSyncDTO(rule forwarding.Rule) *dto.ForwardRule { + return &dto.ForwardRule{ + Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, + TargetPort: rule.TargetPort, Interface: rule.Interface, } - result.Failed++ - result.Errors = append(result.Errors, dto.FirewallRuleSyncFailure{ - SourceUUID: item.SourceUUID, - Rule: item.Rule, ForwardRule: item.ForwardRule, DockerRule: item.DockerRule, - Error: err.Error(), - }) -} - -func databaseRuleSyncTarget(request dto.FirewallRuleSyncRequest, subsystem string) (filter.Provider, error) { - if request.SourceProvider != "" { - return "", fmt.Errorf("%w: %s synchronization reads rules from the database and does not accept a source provider", filter.ErrInvalidRule, subsystem) - } - if request.ResetSource { - return "", fmt.Errorf("%w: %s synchronization does not have a source firewall to reset", filter.ErrInvalidRule, subsystem) - } - if request.TargetProvider != filter.ProviderIptables && request.TargetProvider != filter.ProviderNftables { - return "", fmt.Errorf("%w: %s synchronization only supports iptables and nftables targets", filter.ErrInvalidRule, subsystem) - } - return request.TargetProvider, nil } func (s *ForwardingService) previewRuleSync( @@ -903,7 +684,7 @@ func (s *ForwardingService) previewRuleSync( if err != nil { return dto.FirewallRuleSyncPreview{}, err } - return buildForwardingDatabaseSyncPlan(filter.Provider(target.Name()), candidates, targetRules).preview(), nil + return forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules), nil } func (s *ForwardingService) syncRules( @@ -921,21 +702,20 @@ func (s *ForwardingService) syncRules( if err != nil { return dto.FirewallRuleSyncResult{}, err } - plan := buildForwardingDatabaseSyncPlan(filter.Provider(target.Name()), candidates, targetRules) + preview := forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules) desired := make([]forwarding.Rule, 0, len(candidates)) for _, candidate := range candidates { if candidate.err == nil { desired = append(desired, candidate.rule) } } - preview := plan.preview() if preview.Blocked > 0 { - return plan.validationResult(), nil + return firewallSyncResult(preview, nil, false), nil } if len(desired) == 0 && !targetInitialized { - return plan.completedResult(), nil + return firewallSyncResult(preview, nil, true), nil } - result, reconcileErr := plan.reconcile(func() error { + reconcileErr := func() error { if len(desired) > 0 { if err := s.persistForwardingEnabled(); err != nil { return err @@ -948,7 +728,8 @@ func (s *ForwardingService) syncRules( return err } return verifyForwardingRuleSync(target, desired) - }) + }() + result := firewallSyncResult(preview, reconcileErr, true) recordForwardingSyncError(reconcileErr) if reconcileErr != nil { if preview.Ready == 0 { @@ -959,22 +740,22 @@ func (s *ForwardingService) syncRules( return result, nil } -func buildForwardingDatabaseSyncPlan( +func forwardingSyncPreview( target filter.Provider, candidates []forwardingRuleSyncCandidate, actual []forwarding.Rule, -) databaseSyncPlan { - desired := make([]databaseSyncDesired[forwarding.Rule], 0, len(candidates)) +) dto.FirewallRuleSyncPreview { + desired := make([]firewallsync.Desired[forwarding.Rule, dto.FirewallRuleSyncItem], 0, len(candidates)) for _, candidate := range candidates { - desired = append(desired, databaseSyncDesired[forwarding.Rule]{ - value: candidate.rule, - item: dto.FirewallRuleSyncItem{ + desired = append(desired, firewallsync.Desired[forwarding.Rule, dto.FirewallRuleSyncItem]{ + Value: candidate.rule, + Payload: dto.FirewallRuleSyncItem{ SourceUUID: candidate.rule.Identity(), ForwardRule: forwardingRuleSyncDTO(candidate.rule), }, - err: candidate.err, + Err: candidate.err, }) } - return buildDatabaseSyncPlan( + return firewallDiffPreview( "forwarding", target, desired, actual, func(rule forwarding.Rule) string { return rule.Identity() }, func(rule forwarding.Rule) dto.FirewallRuleSyncItem { @@ -983,6 +764,97 @@ func buildForwardingDatabaseSyncPlan( ) } +func (s *FirewallService) forwardingRuleSyncService() firewallDatabaseSyncAdapter { + if s.forwardingSync == nil { + return newForwardingService() + } + return s.forwardingSync +} + +func (s *DockerPortGuardService) loadRuleSyncCandidates( + ctx context.Context, + request dto.FirewallRuleSyncRequest, +) (string, []model.DockerPortGuardPolicy, dockerGuardRuntime, error) { + targetProvider, err := databaseRuleSyncTarget(request, "Docker") + if err != nil { + return "", nil, nil, err + } + target := string(targetProvider) + selected, err := s.selectedRuleSyncBackend(ctx) + if err != nil { + return "", nil, nil, err + } + if target != selected { + return "", nil, nil, fmt.Errorf( + "%w: selected Docker firewall backend is %s, requested target is %s", + filter.ErrProviderUnavailable, selected, target, + ) + } + policies, err := s.policies.ListManaged(ctx) + if err != nil { + return "", nil, nil, err + } + return target, policies, s.guardRuntime(target), nil +} + +func (s *DockerPortGuardService) selectedRuleSyncBackend(ctx context.Context) (string, error) { + if global.DB != nil { + selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey) + selected = strings.ToLower(strings.TrimSpace(selected)) + if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables { + return selected, nil + } + } + if s.client == nil { + return "", fmt.Errorf("%w: Docker firewall backend is unavailable", ErrDockerUnavailable) + } + cli, err := s.client() + if err != nil { + return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err) + } + defer cli.Close() + info, err := cli.Info(ctx) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err) + } + return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil +} + +func dockerGuardPoliciesFromModels(policies []model.DockerPortGuardPolicy) []docker_guard.Policy { + result := make([]docker_guard.Policy, 0, len(policies)) + for _, policy := range policies { + result = append(result, dockerGuardPolicyFromModel(policy)) + } + return result +} + +func dockerGuardRuleSyncDTO(policy model.DockerPortGuardPolicy) *dto.DockerPortGuardEndpoint { + return &dto.DockerPortGuardEndpoint{ + Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, + PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description, + TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, + ManagementReason: dockerReasonNoMatchingPath, + } +} + +func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGuardEndpoint { + return &dto.DockerPortGuardEndpoint{ + Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, + PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...), + TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, + ManagementReason: dockerReasonNoMatchingPath, + } +} + +func dockerGuardReadOnlyRuleSyncDTO(policy docker_guard.ReadOnlyPolicy) *dto.DockerPortGuardEndpoint { + return &dto.DockerPortGuardEndpoint{ + Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort, + Protocol: policy.Policy.Protocol, PolicyUUID: dockerGuardReadOnlyPolicyUUID(policy), Sources: append([]string(nil), policy.Policy.Sources...), + NativeAction: policy.Action, ReadOnly: true, TrafficPath: dockerTrafficPathUnknown, + ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath, + } +} + func (s *DockerPortGuardService) previewRuleSync( ctx context.Context, request dto.FirewallRuleSyncRequest, @@ -995,7 +867,7 @@ func (s *DockerPortGuardService) previewRuleSync( if err != nil { return dto.FirewallRuleSyncPreview{}, err } - return buildDockerDatabaseSyncPlan(filter.Provider(target), policies, targetInventory).preview(), nil + return dockerSyncPreview(filter.Provider(target), policies, targetInventory), nil } func (s *DockerPortGuardService) syncRules( @@ -1014,8 +886,16 @@ func (s *DockerPortGuardService) syncRules( if err != nil { return dto.FirewallRuleSyncResult{}, err } - plan := buildDockerDatabaseSyncPlan(filter.Provider(target), policies, targetInventory) - result, reconcileErr := plan.reconcileReadOnlyPartial(func() error { + preview := dockerSyncPreview(filter.Provider(target), policies, targetInventory) + for _, item := range preview.Items { + if item.Status == firewallsync.StatusBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule { + return firewallSyncResult(preview, nil, false), nil + } + } + if preview.Ready == 0 && preview.Removed == 0 { + return firewallSyncResult(preview, nil, false), nil + } + reconcileErr := func() error { if err := s.replaceRuntimeReadOnlyPolicies(ctx, targetInventory.ReadOnly); err != nil { return err } @@ -1032,7 +912,8 @@ func (s *DockerPortGuardService) syncRules( return err } return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable) - }) + }() + result := firewallSyncResult(preview, reconcileErr, true) recordDockerPortGuardReconcileError(reconcileErr) if reconcileErr != nil { return result, reconcileErr @@ -1040,26 +921,26 @@ func (s *DockerPortGuardService) syncRules( return result, nil } -func buildDockerDatabaseSyncPlan( +func dockerSyncPreview( target filter.Provider, policies []model.DockerPortGuardPolicy, inventory docker_guard.PolicyInventory, -) databaseSyncPlan { - desired := make([]databaseSyncDesired[docker_guard.Policy], 0, len(policies)) +) dto.FirewallRuleSyncPreview { + desired := make([]firewallsync.Desired[docker_guard.Policy, dto.FirewallRuleSyncItem], 0, len(policies)) for _, policy := range policies { - desired = append(desired, databaseSyncDesired[docker_guard.Policy]{ - value: dockerGuardPolicyFromModel(policy), - item: dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: dockerGuardRuleSyncDTO(policy)}, + desired = append(desired, firewallsync.Desired[docker_guard.Policy, dto.FirewallRuleSyncItem]{ + Value: dockerGuardPolicyFromModel(policy), + Payload: dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: dockerGuardRuleSyncDTO(policy)}, }) } - plan := buildDatabaseSyncPlan( + preview := firewallDiffPreview( "docker", target, desired, inventory.Policies, docker_guard.PolicySyncKey, func(policy docker_guard.Policy) dto.FirewallRuleSyncItem { return dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: dockerGuardRuntimeRuleSyncDTO(policy)} }, ) for _, policy := range inventory.ReadOnly { - plan.items = append(plan.items, dto.FirewallRuleSyncItem{ + preview.Add(dto.FirewallRuleSyncItem{ SourceUUID: dockerGuardReadOnlyPolicyUUID(policy), DockerRule: dockerGuardReadOnlyRuleSyncDTO(policy), Status: firewallsync.StatusBlocked, @@ -1067,296 +948,75 @@ func buildDockerDatabaseSyncPlan( Reason: firewallsync.ReasonMessage(firewallsync.ReasonReadOnlyRule), }) } - return plan + return preview } -type firewallScopeReconciler struct { - ctx context.Context - runtime *filterruntime.Engine - snapshot filter.Snapshot - entries []*firewallRuleSyncEntry -} - -func (r *firewallScopeReconciler) reconcile() { - if r.runtime.Provider() == filter.ProviderUFW { - snapshot, err := r.runtime.ObserveMutation(r.ctx, r.snapshot.Scope) - if err != nil { - for _, entry := range r.entries { - entry.fail(err) - } - return - } - r.snapshot = snapshot - } - removes := make([]*firewallRuleSyncEntry, 0) - updates := make([]*firewallRuleSyncEntry, 0) - creates := make([]*firewallRuleSyncEntry, 0) - for _, entry := range r.entries { - if entry.outcome == firewallRuleSyncFailed { - continue - } - switch entry.item.Status { - case firewallsync.StatusRemove: - removes = append(removes, entry) - case firewallRuleSyncReady: - switch entry.match { - case filter.InventoryMatchChanged: - updates = append(updates, entry) - case filter.InventoryMatchMissing: - creates = append(creates, entry) - } - } - } - sort.SliceStable(removes, func(i, j int) bool { - return firewallSyncRemovalPosition(removes[i]) > firewallSyncRemovalPosition(removes[j]) - }) - r.applyGroups(removes) - for _, entry := range updates { - r.applyGroups([]*firewallRuleSyncEntry{entry}) - } - if len(r.snapshot.Rules) == 0 { - r.applyGroups(creates) - } else { - for _, entry := range creates { - r.applyGroups([]*firewallRuleSyncEntry{entry}) - } - } - r.restoreOrder() -} - -func firewallSyncRemovalPosition(entry *firewallRuleSyncEntry) int { - if entry.remove == nil || entry.remove.Locator.Position == nil { - return 0 +func (s *FirewallService) dockerRuleSyncService() firewallDatabaseSyncAdapter { + if s.dockerSync == nil { + return newDockerPortGuardService() } - return *entry.remove.Locator.Position + return s.dockerSync } -func (r *firewallScopeReconciler) applyGroups(entries []*firewallRuleSyncEntry) { - if len(entries) == 0 { - return +func databaseRuleSyncTarget(request dto.FirewallRuleSyncRequest, subsystem string) (filter.Provider, error) { + if request.SourceProvider != "" { + return "", fmt.Errorf("%w: %s synchronization reads rules from the database and does not accept a source provider", filter.ErrInvalidRule, subsystem) } - batch := r.runtime.Provider() == filter.ProviderIptables || r.runtime.Provider() == filter.ProviderNftables - if batch { - r.apply(entries) - return + if request.ResetSource { + return "", fmt.Errorf("%w: %s synchronization does not have a source firewall to reset", filter.ErrInvalidRule, subsystem) } - for _, entry := range entries { - r.apply([]*firewallRuleSyncEntry{entry}) + if request.TargetProvider != filter.ProviderIptables && request.TargetProvider != filter.ProviderNftables { + return "", fmt.Errorf("%w: %s synchronization only supports iptables and nftables targets", filter.ErrInvalidRule, subsystem) } + return request.TargetProvider, nil } -func (r *firewallScopeReconciler) apply(entries []*firewallRuleSyncEntry) { - changes := make([]filter.DesiredChange, 0, len(entries)) - active := make([]*firewallRuleSyncEntry, 0, len(entries)) - for _, entry := range entries { - change, changed, err := firewallRuleSyncChange(r.snapshot, r.entries, entry) - if err != nil { - entry.fail(err) - continue - } - if !changed { - if entry.item.Status == firewallsync.StatusRemove { - entry.outcome = firewallRuleSyncRemoved - } else { - entry.outcome = firewallRuleSyncSkipped - } - continue - } - changes = append(changes, change) - active = append(active, entry) - } - if len(changes) == 0 { - return - } - _, verification, err := r.runtime.Execute(r.ctx, r.snapshot, changes) - if err == nil && !verification.Matched { - err = filter.ErrVerificationFailed - } - if err != nil { - for _, entry := range active { - entry.fail(err) - } - return - } - for _, entry := range active { - if entry.item.Status == firewallsync.StatusRemove { - entry.outcome = firewallRuleSyncRemoved - } else { - entry.outcome = firewallRuleSyncApplied - } +func appendDatabaseSyncFailure(result *dto.FirewallRuleSyncResult, item dto.FirewallRuleSyncItem, err error) { + if err == nil { + err = errors.New("database synchronization failed") } - r.snapshot = verification.Snapshot + result.Failed++ + result.Errors = append(result.Errors, dto.FirewallRuleSyncFailure{ + SourceUUID: item.SourceUUID, + Rule: item.Rule, ForwardRule: item.ForwardRule, DockerRule: item.DockerRule, + Error: err.Error(), + }) } -func (r *firewallScopeReconciler) restoreOrder() { - reorderEntries := make([]*firewallRuleSyncEntry, 0) - desiredMarkers := make([]string, 0, len(r.entries)) - for _, entry := range r.entries { - if entry.remove != nil || entry.err != nil || entry.desired.Marker == "" { - continue - } - desiredMarkers = append(desiredMarkers, entry.desired.Marker) - if entry.reorder { - reorderEntries = append(reorderEntries, entry) - } - } - if len(reorderEntries) == 0 { - return - } - for _, entry := range reorderEntries { - if entry.outcome == firewallRuleSyncFailed { - r.failOrder(reorderEntries, errors.New("managed rule order was not synchronized because a preceding rule change failed")) - return - } +func firewallDiffPreview[T any](subsystem string, target filter.Provider, desired []firewallsync.Desired[T, dto.FirewallRuleSyncItem], actual []T, key func(T) string, actualItem func(T) dto.FirewallRuleSyncItem) dto.FirewallRuleSyncPreview { + preview := dto.FirewallRuleSyncPreview{Subsystem: subsystem, TargetProvider: target, Items: make([]dto.FirewallRuleSyncItem, 0)} + for _, item := range firewallsync.Diff(desired, actual, key, actualItem) { + row := item.Payload + row.Status, row.ReasonCode, row.Reason = item.Status, item.ReasonCode, item.Reason + preview.Add(row) } - - changed := false - for step := 0; step < len(desiredMarkers); step++ { - marker, position, converged, err := firewallsync.NextManagedOrderChange(r.snapshot, desiredMarkers) - if err != nil { - r.failOrder(reorderEntries, err) - return - } - if converged { - for _, entry := range reorderEntries { - if changed { - entry.outcome = firewallRuleSyncApplied - } else if entry.outcome == "" { - entry.outcome = firewallRuleSyncSkipped - } - } - return - } - observed, _, exists := firewallsync.ObservedByMarker(r.snapshot, marker) - if !exists { - r.failOrder(reorderEntries, filter.ErrRuleStale) - return - } - after := firewallsync.ObservedRule(observed) - target := int64(position) - after.OrderIndex = &target - before := firewallsync.ObservedRule(observed) - locator := observed.Locator - operation := filter.ChangeReorder - if r.runtime.Provider() == filter.ProviderUFW { - operation = filter.ChangeUpdate - } - _, verification, executeErr := r.runtime.Execute(r.ctx, r.snapshot, []filter.DesiredChange{{ - Operation: operation, Before: &before, After: &after, Locator: &locator, - }}) - if executeErr == nil && !verification.Matched { - executeErr = filter.ErrVerificationFailed - } - if executeErr != nil { - r.failOrder(reorderEntries, executeErr) - return - } - r.snapshot = verification.Snapshot - changed = true - } - r.failOrder(reorderEntries, fmt.Errorf("%w: managed rule order did not converge", filter.ErrVerificationFailed)) + return preview } -func (r *firewallScopeReconciler) failOrder(entries []*firewallRuleSyncEntry, err error) { - for _, entry := range entries { - if entry.outcome != firewallRuleSyncFailed { - entry.fail(err) +func firewallSyncResult(preview dto.FirewallRuleSyncPreview, cause error, executed bool) dto.FirewallRuleSyncResult { + result := dto.FirewallRuleSyncResult{Subsystem: preview.Subsystem, TargetProvider: preview.TargetProvider} + for _, item := range preview.Items { + if item.Status != firewallsync.StatusRemove { + result.Total++ } - } -} - -func firewallRuleSyncChange( - snapshot filter.Snapshot, - entries []*firewallRuleSyncEntry, - entry *firewallRuleSyncEntry, -) (filter.DesiredChange, bool, error) { - if entry.remove != nil { - for index := range snapshot.Rules { - observed := snapshot.Rules[index] - if observed.Marker != entry.remove.Marker { - continue - } - if observed.Protected { - return filter.DesiredChange{}, false, filter.ErrProtectedRule + switch item.Status { + case firewallsync.StatusExisting: + result.Skipped++ + case firewallsync.StatusBlocked: + appendDatabaseSyncFailure(&result, item, errors.New(item.Reason)) + case firewallsync.StatusReady: + if executed { + if cause != nil { + appendDatabaseSyncFailure(&result, item, cause) + } else { + result.Succeeded++ + } } - before := firewallsync.ObservedRule(observed) - locator := observed.Locator - return filter.DesiredChange{ - Operation: filter.ChangeDelete, Before: &before, Locator: &locator, - }, true, nil - } - return filter.DesiredChange{}, false, nil - } - item, err := firewallRuleSyncInventoryFromSnapshot(snapshot, entry) - if err != nil { - return filter.DesiredChange{}, false, err - } - after := entry.rule - after.OrderIndex = nil - change := filter.DesiredChange{After: &after} - switch item.Match { - case filter.InventoryMatchExact: - return filter.DesiredChange{}, false, nil - case filter.InventoryMatchMissing: - change.Operation = filter.ChangeCreate - if position := firewallRuleSyncInsertionPosition(snapshot, entries, entry); position != nil { - if entry.rule.Scope.Provider == filter.ProviderUFW && *position > maxObservedFirewallPosition(snapshot) { - change.Append = true - } else { - after.OrderIndex = position - change.After = &after + case firewallsync.StatusRemove: + if executed && cause == nil { + result.Removed++ } - } else { - change.Append = entry.rule.Scope.Provider == filter.ProviderUFW - } - case filter.InventoryMatchChanged: - if item.Observed == nil { - return filter.DesiredChange{}, false, filter.ErrRuleStale } - if err := filter.GuardMutation(*item.Observed); err != nil { - return filter.DesiredChange{}, false, err - } - before := firewallsync.ObservedRule(*item.Observed) - locator := item.Observed.Locator - change.Operation = filter.ChangeUpdate - if requiresUFWMarkerAdoption(entry.desired, *item.Observed) { - change.Operation = filter.ChangeAdopt - change.PreviousMarker = item.Observed.Marker - } - change.Before = &before - change.Locator = &locator - default: - return filter.DesiredChange{}, false, fmt.Errorf("%w: target rule match is %s", filter.ErrRuleOperation, item.Match) - } - return change, true, nil -} - -func requiresUFWMarkerAdoption(desired filter.DesiredRule, observed filter.ObservedRule) bool { - if desired.Rule.Scope.Provider != filter.ProviderUFW || observed.Marker == desired.Marker { - return false } - marker := strings.TrimSpace(observed.Marker) - if marker == "" { - return desired.Origin == filter.RuleOriginAdopted - } - return marker == "1panel-rule:"+strings.TrimSpace(desired.UUID) -} - -func firewallRuleSyncInsertionPosition( - snapshot filter.Snapshot, - entries []*firewallRuleSyncEntry, - target *firewallRuleSyncEntry, -) *int64 { - desiredMarkers := make([]string, 0, len(entries)) - for _, entry := range entries { - if entry.remove != nil || entry.err != nil { - continue - } - desiredMarkers = append(desiredMarkers, entry.desired.Marker) - } - position, exists := firewallsync.InsertionPosition(snapshot, desiredMarkers, target.desired.Marker) - if !exists { - return nil - } - return &position + return result } diff --git a/agent/app/service/forward.go b/agent/app/service/forward.go index d8bdfb1677df..474cff304e8c 100644 --- a/agent/app/service/forward.go +++ b/agent/app/service/forward.go @@ -16,7 +16,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" forwardingproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding/providers" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" @@ -26,7 +25,7 @@ import ( type IForwardingService interface { LoadBaseInfo() (dto.FirewallSubsystemStatus, error) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) - OperateRules(request dto.ForwardRuleOperate) error + OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) Enable() error QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) Restore(context.Context) error @@ -72,11 +71,6 @@ func newForwardingService() *ForwardingService { } } -type forwardingRuleSyncCandidate struct { - rule forwarding.Rule - err error -} - func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) { selected := configuredForwardingBackend() baseInfo := dto.FirewallSubsystemStatus{ @@ -198,10 +192,29 @@ func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) return false } -func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) error { +func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) { + labels := make([]string, len(request.Rules)) + operation := task.TaskCreate + for i, rule := range request.Rules { + labels[i] = fmt.Sprintf("[%d/%d] %s %s %s %s -> %s:%s", i+1, len(request.Rules), rule.Operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort) + if rule.Operation != "add" { + operation = task.TaskUpdate + } + } + if forwardingOperationsOnlyRemove(request.Rules) { + operation = task.TaskDelete + } + return queueFirewallRuleTask("Forwarding", operation, labels, func(ctx context.Context) error { + return s.operateRules(ctx, request) + }) +} + +func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate) error { forwardingMutationMu.Lock() defer forwardingMutationMu.Unlock() - ctx := context.Background() + if err := ctx.Err(); err != nil { + return err + } stored, err := s.rules.List(ctx) if err != nil { return err @@ -336,85 +349,6 @@ func (s *ForwardingService) Restore(ctx context.Context) error { return err } -func (s *ForwardingService) loadRuleSyncCandidates( - ctx context.Context, - targetProvider filter.Provider, -) (*forwarding.Manager, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) { - target, err := s.manager() - if err != nil { - return nil, nil, nil, false, err - } - if target.Name() != string(targetProvider) { - return nil, nil, nil, false, fmt.Errorf( - "%w: selected forwarding backend is %s, requested target is %s", - filter.ErrProviderUnavailable, target.Name(), targetProvider, - ) - } - stored, err := s.rules.List(ctx) - if err != nil { - return nil, nil, nil, false, err - } - candidates := make([]forwardingRuleSyncCandidate, 0, len(stored)) - for _, record := range stored { - rule := forwarding.Rule{ - Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP, - TargetPort: record.TargetPort, Interface: record.Interface, - } - normalized, normalizeErr := forwarding.NormalizeRule(rule) - candidates = append(candidates, forwardingRuleSyncCandidate{rule: normalized, err: normalizeErr}) - } - targetStatus, err := target.Status() - if err != nil { - return nil, nil, nil, false, err - } - targetRules := make([]forwarding.Rule, 0) - if targetStatus.IsInit { - targetRules, err = target.List("", "") - if err != nil { - return nil, nil, nil, false, err - } - targetRules, err = normalizeForwardingRuntimeRules(targetRules) - if err != nil { - return nil, nil, nil, false, err - } - } - return target, candidates, targetRules, targetStatus.IsInit, nil -} - -func verifyForwardingRuleSync(target *forwarding.Manager, desired []forwarding.Rule) error { - actual, err := target.List("", "") - if err != nil { - return fmt.Errorf("verify synchronized forwarding rules: %w", err) - } - actual, err = normalizeForwardingRuntimeRules(actual) - if err != nil { - return fmt.Errorf("verify synchronized forwarding rules: %w", err) - } - if !databaseSyncStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) { - return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database") - } - return nil -} - -func normalizeForwardingRuntimeRules(rules []forwarding.Rule) ([]forwarding.Rule, error) { - normalized := make([]forwarding.Rule, 0, len(rules)) - for _, rule := range rules { - item, err := forwarding.NormalizeRule(rule) - if err != nil { - return nil, fmt.Errorf("normalize target forwarding rule %s: %w", rule.Identity(), err) - } - normalized = append(normalized, item) - } - return normalized, nil -} - -func forwardingRuleSyncDTO(rule forwarding.Rule) *dto.ForwardRule { - return &dto.ForwardRule{ - Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, - TargetPort: rule.TargetPort, Interface: rule.Interface, - } -} - func (s *ForwardingService) reconcile(rules []forwarding.Rule) error { manager, err := s.manager() if err != nil { diff --git a/agent/constant/firewall.go b/agent/constant/firewall.go index ebfad368768f..bee9da9e3512 100644 --- a/agent/constant/firewall.go +++ b/agent/constant/firewall.go @@ -32,7 +32,6 @@ const ( const ( FirewallSystemAcceptedPortSourcePrefix = "accepted-port:" - FirewallRuleCheckVersion = 1 FirewallRuleOriginCreated = "created" FirewallRuleOriginAdopted = "adopted" diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index f2f668982c2f..4af5b976bd6b 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: "The {{ .name }} application is not compatible with the ErrDockerFailed: 'Docker is abnormal; check service status' ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again' ErrDockerNftablesChainUnavailable: 'The Docker nftables IPv4 firewall chain was not found. Verify that the current Docker version supports the nftables firewall backend, restart Docker, and try again' +ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD defaults to DROP. Adjust the policy and retry' ErrUFWRuleAdopt: 'Failed to adopt the UFW rule: {{ .detail }}. If the installed UFW version is earlier than 0.35, upgrade UFW and try again' ErrDockerComposeCmdNotFound: 'Docker Compose not found on host' UseExistImage: 'Image exists; using existing image' @@ -510,13 +511,20 @@ AppLink: 'Linked Application' EnableSSL: 'Enable HTTPS' AppStore: 'App Store' Firewall: 'Firewall' +FirewallCreateRulesStep: 'Create firewall rules' +FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed' +FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed' +FirewallCreateRuleSkipped: 'Not executed' +FirewallCreateBatchStep: 'Submit {{ .count }} rules as a batch to {{ .backend }}' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; converted to {{ .count }} rules' +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' FirewallSyncStep: 'Synchronize rules to {{ .name }}' -FirewallSyncResult: 'Synchronization result: {{ .succeeded }} succeeded, {{ .existing }} existing, {{ .failed }} failed' FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize' -FirewallSyncFailedResetSkipped: '{{ .failed }} firewall rules failed to synchronize; the source firewall was not reset' FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}' FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted' -FirewallVerifyTargetStep: 'Verify target firewall rules' FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains' FirewallRestoreRulesStep: 'Restore database rules to {{ .name }}' FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules' FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies' FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains' FirewallPersistDockerGuardStep: 'Persist Docker port guard status' -FirewallTargetRuleIneffective: 'The target rule is no longer effective' -FirewallVerifyRuleFailed: 'Failed to verify synchronized rule {{ .name }}: {{ .detail }}' TaskSync: 'Sync' LocalApp: 'Local Application' SubTask: 'Subtask' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 6296a88c7127..01c446108bf1 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'La aplicación {{ .name }} no es compatible con la ver ErrDockerFailed: 'El estado de Docker es anómalo, revise el servicio' ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo' ErrDockerNftablesChainUnavailable: 'No se encontró la cadena de firewall IPv4 de nftables de Docker. Compruebe que la versión actual de Docker admita el backend de firewall nftables, reinicie Docker y vuelva a intentarlo' +ErrDockerForwardPolicyDrop: 'La política predeterminada de FORWARD para {{ .family }} es DROP. Ajústela y vuelva a intentarlo' ErrUFWRuleAdopt: 'No se pudo administrar la regla UFW: {{ .detail }}. Si la versión actual de UFW es anterior a 0.35, actualice UFW y vuelva a intentarlo' ErrDockerComposeCmdNotFound: 'El comando Docker Compose no existe instálelo primero en el host' UseExistImage: 'La imagen ya existe, usando imagen existente' @@ -510,13 +511,20 @@ AppLink: 'Aplicación enlazada' EnableSSL: 'Habilitar HTTPS' AppStore: 'Tienda de aplicaciones' Firewall: 'Firewall' +FirewallCreateRulesStep: 'Crear reglas de cortafuegos' +FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar' +FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas' +FirewallCreateRuleSkipped: 'Sin ejecutar' +FirewallCreateBatchStep: 'Enviar {{ .count }} reglas en un lote a {{ .backend }}' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido en {{ .count }} reglas' +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' FirewallSyncStep: 'Sincronizar reglas con {{ .name }}' -FirewallSyncResult: 'Resultado de sincronización: {{ .succeeded }} correctas, {{ .existing }} existentes, {{ .failed }} fallidas' FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall' -FirewallSyncFailedResetSkipped: 'No se pudieron sincronizar {{ .failed }} reglas; el firewall de origen no se restableció' FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}' FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos' -FirewallVerifyTargetStep: 'Verificar las reglas del firewall de destino' FirewallInitializeChainsStep: 'Inicializar y vincular las cadenas base de {{ .name }}' FirewallRestoreRulesStep: 'Restaurar las reglas de la base de datos en {{ .name }}' FirewallSyncWhitelistStep: 'Sincronizar la lista de puertos permitidos del firewall' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Restaurar las reglas de reenvío de puertos FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker y las políticas' FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}' FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker' -FirewallTargetRuleIneffective: 'La regla de destino ya no es efectiva' -FirewallVerifyRuleFailed: 'No se pudo verificar la regla sincronizada {{ .name }}: {{ .detail }}' TaskSync: 'Sincronizar' LocalApp: 'Aplicación local' SubTask: 'Subtarea' diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index a100519b287c..6dedd72cecdd 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: "برنامه {{ .name }} با نسخه فعلی 1Panel ErrDockerFailed: 'Docker غیرعادی است؛ وضعیت سرویس را بررسی کنید' ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید' ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بک‌اند فایروال nftables پشتیبانی کند، سپس داکر را راه‌اندازی مجدد کرده و دوباره تلاش کنید' +ErrDockerForwardPolicyDrop: 'سیاست پیش‌فرض FORWARD برای {{ .family }} برابر DROP است. آن را تغییر دهید و دوباره تلاش کنید' ErrUFWRuleAdopt: 'مدیریت قانون UFW ناموفق بود: {{ .detail }}. اگر نسخه فعلی UFW قدیمی‌تر از 0.35 است، ابتدا UFW را ارتقا دهید و دوباره تلاش کنید' ErrDockerComposeCmdNotFound: 'Docker Compose روی میزبان یافت نشد' UseExistImage: 'تصویر وجود دارد؛ استفاده از تصویر موجود' @@ -510,13 +511,20 @@ AppLink: 'برنامه متصل' EnableSSL: 'فعال‌سازی HTTPS' AppStore: 'فروشگاه برنامه' Firewall: 'فایروال' +FirewallCreateRulesStep: 'ایجاد قوانین دیوار آتش' +FirewallCreateRulesResult: 'نتیجه ایجاد: {{ .succeeded }} موفق، {{ .failed }} ناموفق، {{ .skipped }} اجرا نشده' +FirewallRuleOperationResult: 'نتیجه عملیات: {{ .succeeded }} موفق، {{ .failed }} ناموفق' +FirewallCreateRuleSkipped: 'اجرا نشده' +FirewallCreateBatchStep: 'ارسال {{ .count }} قانون به صورت دسته‌ای به {{ .backend }}' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}؛ به {{ .count }} قانون تبدیل شد' +FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند' +FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود' +FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد' +FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده' FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}' -FirewallSyncResult: 'نتیجه همگام‌سازی: {{ .succeeded }} موفق، {{ .existing }} موجود، {{ .failed }} ناموفق' FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود' -FirewallSyncFailedResetSkipped: 'همگام‌سازی {{ .failed }} قانون ناموفق بود؛ فایروال مبدأ بازنشانی نشد' FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}' FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد' -FirewallVerifyTargetStep: 'بررسی قوانین فایروال مقصد' FirewallInitializeChainsStep: 'راه‌اندازی و اتصال زنجیره‌های پایه {{ .name }}' FirewallRestoreRulesStep: 'بازیابی قوانین پایگاه داده در {{ .name }}' FirewallSyncWhitelistStep: 'همگام‌سازی فهرست مجاز پورت‌های فایروال' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پو FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاست‌ها' FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}' FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker' -FirewallTargetRuleIneffective: 'قانون مقصد دیگر مؤثر نیست' -FirewallVerifyRuleFailed: 'بررسی قانون همگام‌شده {{ .name }} ناموفق بود: {{ .detail }}' TaskSync: 'همگام‌سازی' LocalApp: 'برنامه محلی' SubTask: 'وظیفه فرعی' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index b41cd3746e6b..9d41d050307e 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} アプリケーションは現在の 1Pane ErrDockerFailed: 'Docker の状態が異常です。サービス状態を確認してください' ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください' ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください' +ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD の既定ポリシーが DROP です。変更してから再試行してください' ErrUFWRuleAdopt: 'UFW ルールの管理に失敗しました:{{ .detail }}。現在の UFW バージョンが 0.35 未満の場合は、UFW をアップグレードしてから再試行してください' ErrDockerComposeCmdNotFound: 'Docker Compose コマンドは存在しません。ホストマシンにこのコマンドを先にインストールしてください' UseExistImage: 'イメージは既に存在します。既存のイメージを使用します' @@ -510,13 +511,20 @@ AppLink: 'リンクされたアプリケーション' EnableSSL: 'HTTPS を有効にする' AppStore: 'Appストア' Firewall: 'ファイアウォール' +FirewallCreateRulesStep: 'ファイアウォールルールを作成' +FirewallCreateRulesResult: '作成結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件' +FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件' +FirewallCreateRuleSkipped: '未実行' +FirewallCreateBatchStep: '{{ .backend }} に {{ .count }} 件のルールを一括送信' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }};{{ .count }} 件のルールに変換' +FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません' +FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました' +FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました' +FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件' FirewallSyncStep: '{{ .name }} にルールを同期' -FirewallSyncResult: '同期結果:成功 {{ .succeeded }} 件、既存 {{ .existing }} 件、失敗 {{ .failed }} 件' FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした' -FirewallSyncFailedResetSkipped: '{{ .failed }} 件のルールを同期できなかったため、移行元ファイアウォールはリセットされませんでした' FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化' FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました' -FirewallVerifyTargetStep: '移行先ファイアウォールルールを検証' FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド' FirewallRestoreRulesStep: 'データベースルールを {{ .name }} に復元' FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルー FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認' FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド' FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存' -FirewallTargetRuleIneffective: '移行先ルールは有効ではありません' -FirewallVerifyRuleFailed: '同期ルール {{ .name }} の検証に失敗しました:{{ .detail }}' TaskSync: '同期' LocalApp: 'ローカルアプリケーション' SubTask: 'サブタスク' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index f39b1734ef3a..f5c57cf91251 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} 응용 프로그램은 현재 1Panel 버 ErrDockerFailed: 'Docker의 상태가 비정상입니다. 서비스 상태를 확인하세요' ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요' ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요' +ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD 기본 정책이 DROP입니다. 정책을 조정한 후 다시 시도하세요' ErrUFWRuleAdopt: 'UFW 규칙 관리에 실패했습니다: {{ .detail }}. 현재 UFW 버전이 0.35 미만이면 UFW를 업그레이드한 후 다시 시도하세요' ErrDockerComposeCmdNotFound: 'Docker Compose 명령이 없습니다. 호스트 머신에 먼저 이 명령을 설치하세요' UseExistImage: '이미지가 이미 존재하여 기존 이미지를 사용합니다' @@ -510,13 +511,20 @@ AppLink: '연결된 애플리케이션' EnableSSL: 'HTTPS 활성화' AppStore: '앱스토어' Firewall: '방화벽' +FirewallCreateRulesStep: '방화벽 규칙 생성' +FirewallCreateRulesResult: '생성 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개' +FirewallRuleOperationResult: '작업 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개' +FirewallCreateRuleSkipped: '미실행' +FirewallCreateBatchStep: '{{ .backend }}에 규칙 {{ .count }}개 일괄 제출' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; 규칙 {{ .count }}개로 변환' +FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다' +FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다' +FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다' +FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개' FirewallSyncStep: '{{ .name }}에 규칙 동기화' -FirewallSyncResult: '동기화 결과: 성공 {{ .succeeded }}개, 기존 {{ .existing }}개, 실패 {{ .failed }}개' FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다' -FirewallSyncFailedResetSkipped: '규칙 {{ .failed }}개를 동기화하지 못해 원본 방화벽을 초기화하지 않았습니다' FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화' FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다' -FirewallVerifyTargetStep: '대상 방화벽 규칙 확인' FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩' FirewallRestoreRulesStep: '데이터베이스 규칙을 {{ .name }}에 복원' FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복 FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인' FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩' FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장' -FirewallTargetRuleIneffective: '대상 규칙이 더 이상 적용되지 않습니다' -FirewallVerifyRuleFailed: '동기화된 규칙 {{ .name }} 확인 실패: {{ .detail }}' TaskSync: '동기화' LocalApp: '로컬 애플리케이션' SubTask: '하위 작업' diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index ea90c1d1f849..67a5c20ffd54 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -151,6 +151,7 @@ ErrAppVersionDeprecated: "ແອັບພລິເຄຊັນ {{ .name }} ບ ErrDockerFailed: 'Docker ຜິດປົກກະຕິ; ກະລຸນາກວດສອບສະຖານະການບໍລິການ' ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ' ErrDockerNftablesChainUnavailable: 'ບໍ່ພົບ chain ໄຟວໍ nftables IPv4 ຂອງ Docker. ກະລຸນາກວດສອບວ່າ Docker ລຸ້ນປັດຈຸບັນຮອງຮັບ backend ໄຟວໍ nftables, ເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ' +ErrDockerForwardPolicyDrop: 'ນະໂຍບາຍເລີ່ມຕົ້ນ FORWARD ຂອງ {{ .family }} ແມ່ນ DROP. ກະລຸນາປັບແລ້ວລອງອີກຄັ້ງ' ErrUFWRuleAdopt: 'ຈັດການກົດ UFW ບໍ່ສຳເລັດ: {{ .detail }}. ຖ້າ UFW ລຸ້ນປັດຈຸບັນຕ່ຳກວ່າ 0.35, ກະລຸນາອັບເກຣດ UFW ແລ້ວລອງໃໝ່' ErrDockerComposeCmdNotFound: 'ບໍ່ພົບຄຳສັ່ງ Docker Compose ໃນເຄື່ອງ' UseExistImage: 'ມີຮູບພາບຢູ່ແລ້ວ; ກຳລັງໃຊ້ຮູບພາບທີ່ມີຢູ່' @@ -500,13 +501,20 @@ AppLink: 'ແອັບພລິເຄຊັນທີ່ເຊື່ອມໂຍ EnableSSL: 'ເປີດໃຊ້ HTTPS' AppStore: 'ຮ້ານແອັບ' Firewall: 'ໄຟວອລ' +FirewallCreateRulesStep: 'ສ້າງກົດໄຟວໍ' +FirewallCreateRulesResult: 'ຜົນການສ້າງ: ສຳເລັດ {{ .succeeded }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}' +FirewallRuleOperationResult: 'ຜົນການດຳເນີນການ: ສຳເລັດ {{ .succeeded }} ລາຍການ, ລົ້ມເຫຼວ {{ .failed }} ລາຍການ' +FirewallCreateRuleSkipped: 'ບໍ່ໄດ້ດຳເນີນການ' +FirewallCreateBatchStep: 'ສົ່ງ {{ .count }} ກົດເປັນຊຸດໄປຫາ {{ .backend }}' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ແປງເປັນ {{ .count }} ກົດ' +FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ' +FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ' +FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້' +FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}' FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}' -FirewallSyncResult: 'ຜົນການຊິງຄ໌: ສຳເລັດ {{ .succeeded }}, ມີແລ້ວ {{ .existing }}, ລົ້ມເຫຼວ {{ .failed }}' FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ' -FirewallSyncFailedResetSkipped: 'ຊິງຄ໌ກົດ {{ .failed }} ລາຍການບໍ່ສຳເລັດ; ບໍ່ໄດ້ຣີເຊັດໄຟວອລຕົ້ນທາງ' FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}' FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ' -FirewallVerifyTargetStep: 'ກວດສອບກົດໄຟວອລປາຍທາງ' FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}' FirewallRestoreRulesStep: 'ກູ້ຄືນກົດຖານຂໍ້ມູນໄປຫາ {{ .name }}' FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ' @@ -515,8 +523,6 @@ FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕ FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ' FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}' FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker' -FirewallTargetRuleIneffective: 'ກົດປາຍທາງບໍ່ມີຜົນແລ້ວ' -FirewallVerifyRuleFailed: 'ກວດສອບກົດທີ່ຊິງຄ໌ {{ .name }} ບໍ່ສຳເລັດ: {{ .detail }}' TaskSync: 'ຊິງຄ໌' LocalApp: 'ແອັບພລິເຄຊັນພາຍໃນເຄື່ອງ' SubTask: 'ວຽກຍ່ອຍ' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index f504feccda4e..8b66fb31cd20 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'Aplikasi {{ .name }} tidak sesuai dengan versi 1Panel ErrDockerFailed: 'Keadaan Docker tidak normal, sila periksa status perkhidmatan' ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi' ErrDockerNftablesChainUnavailable: 'Rantaian firewall IPv4 nftables Docker tidak ditemui. Pastikan versi Docker semasa menyokong bahagian belakang firewall nftables, mulakan semula Docker dan cuba lagi' +ErrDockerForwardPolicyDrop: 'Dasar lalai FORWARD {{ .family }} ialah DROP. Laraskan dasar dan cuba lagi' ErrUFWRuleAdopt: 'Gagal mengurus peraturan UFW: {{ .detail }}. Jika versi UFW semasa lebih lama daripada 0.35, tingkatkan UFW dan cuba lagi' ErrDockerComposeCmdNotFound: 'Perintah Docker Compose tidak wujud, sila pasang perintah ini di mesin tuan terlebih dahulu' UseExistImage: 'Imej sudah wujud, menggunakan imej sedia ada' @@ -510,13 +511,20 @@ AppLink: 'Aplikasi Terpaut' EnableSSL: 'Dayakan HTTPS' AppStore: 'Kedai aplikasi' Firewall: 'Firewall' +FirewallCreateRulesStep: 'Cipta peraturan tembok api' +FirewallCreateRulesResult: 'Hasil penciptaan: {{ .succeeded }} berjaya, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan' +FirewallRuleOperationResult: 'Hasil operasi: {{ .succeeded }} berjaya, {{ .failed }} gagal' +FirewallCreateRuleSkipped: 'Tidak dilaksanakan' +FirewallCreateBatchStep: 'Hantar {{ .count }} peraturan secara kelompok ke {{ .backend }}' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ditukar kepada {{ .count }} peraturan' +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' FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}' -FirewallSyncResult: 'Hasil penyegerakan: {{ .succeeded }} berjaya, {{ .existing }} sedia ada, {{ .failed }} gagal' FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan' -FirewallSyncFailedResetSkipped: '{{ .failed }} peraturan gagal disegerakkan; firewall sumber tidak ditetapkan semula' FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}' FirewallResetSourceResult: 'Firewall sumber ditetapkan semula; {{ .removed }} peraturan pangkalan data dipadamkan' -FirewallVerifyTargetStep: 'Sahkan peraturan firewall sasaran' FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}' FirewallRestoreRulesStep: 'Pulihkan peraturan pangkalan data ke {{ .name }}' FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi' FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}' FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker' -FirewallTargetRuleIneffective: 'Peraturan sasaran tidak lagi berkesan' -FirewallVerifyRuleFailed: 'Gagal mengesahkan peraturan disegerakkan {{ .name }}: {{ .detail }}' TaskSync: 'Segerakkan' LocalApp: 'Aplikasi Tempatan' SubTask: 'Subtugas' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 0cf6425f865b..ff6a209ebda2 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'O aplicativo {{ .name }} não é compatível com a ver ErrDockerFailed: 'O estado do Docker está anormal, verifique o status do serviço' ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente' ErrDockerNftablesChainUnavailable: 'A cadeia de firewall IPv4 do nftables do Docker não foi encontrada. Confirme se a versão atual do Docker oferece suporte ao backend de firewall nftables, reinicie o Docker e tente novamente' +ErrDockerForwardPolicyDrop: 'A política padrão de FORWARD para {{ .family }} é DROP. Ajuste-a e tente novamente' ErrUFWRuleAdopt: 'Falha ao gerenciar a regra UFW: {{ .detail }}. Se a versão atual do UFW for anterior à 0.35, atualize o UFW e tente novamente' ErrDockerComposeCmdNotFound: 'O comando Docker Compose não existe, instale este comando na máquina host primeiro' UseExistImage: 'A imagem já existe, usando imagem existente' @@ -510,13 +511,20 @@ AppLink: 'Aplicativo vinculado' EnableSSL: 'Habilitar HTTPS' AppStore: 'Loja de aplicativos' Firewall: 'Firewall' +FirewallCreateRulesStep: 'Criar regras de firewall' +FirewallCreateRulesResult: 'Resultado da criação: {{ .succeeded }} com sucesso, {{ .failed }} com falha, {{ .skipped }} não executadas' +FirewallRuleOperationResult: 'Resultado da operação: {{ .succeeded }} com sucesso, {{ .failed }} com falha' +FirewallCreateRuleSkipped: 'Não executada' +FirewallCreateBatchStep: 'Enviar {{ .count }} regras em lote para {{ .backend }}' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido em {{ .count }} regras' +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' FirewallSyncStep: 'Sincronizar regras com {{ .name }}' -FirewallSyncResult: 'Resultado da sincronização: {{ .succeeded }} com sucesso, {{ .existing }} existentes, {{ .failed }} com falha' FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização' -FirewallSyncFailedResetSkipped: '{{ .failed }} regras falharam na sincronização; o firewall de origem não foi redefinido' FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}' FirewallResetSourceResult: 'Firewall de origem redefinido; {{ .removed }} regras do banco de dados foram excluídas' -FirewallVerifyTargetStep: 'Verificar regras do firewall de destino' FirewallInitializeChainsStep: 'Inicializar e vincular as cadeias base do {{ .name }}' FirewallRestoreRulesStep: 'Restaurar regras do banco de dados para {{ .name }}' FirewallSyncWhitelistStep: 'Sincronizar a lista de portas permitidas do firewall' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de porta FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as políticas' FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}' FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker' -FirewallTargetRuleIneffective: 'A regra de destino não está mais efetiva' -FirewallVerifyRuleFailed: 'Falha ao verificar a regra sincronizada {{ .name }}: {{ .detail }}' TaskSync: 'Sincronizar' LocalApp: 'Aplicativo local' SubTask: 'Subtarefa' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 3d2a1454b5be..6651ea52873b 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'Приложение {{ .name }} несовмести ErrDockerFailed: 'Состояние Docker аномально, проверьте состояние сервиса' ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку' ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку' +ErrDockerForwardPolicyDrop: 'Политика FORWARD по умолчанию для {{ .family }} — DROP. Измените её и повторите попытку' ErrUFWRuleAdopt: 'Не удалось принять правило UFW под управление: {{ .detail }}. Если текущая версия UFW ниже 0.35, обновите UFW и повторите попытку' ErrDockerComposeCmdNotFound: 'Команда Docker Compose отсутствует, пожалуйста, установите эту команду на хост-машине сначала' UseExistImage: 'Образ уже существует, используется существующий образ' @@ -510,13 +511,20 @@ AppLink: 'Связанное приложение' EnableSSL: 'Включить HTTPS' AppStore: 'Магазин приложений' Firewall: 'Межсетевой экран' +FirewallCreateRulesStep: 'Создание правил межсетевого экрана' +FirewallCreateRulesResult: 'Результат создания: успешно — {{ .succeeded }}, ошибок — {{ .failed }}, не выполнено — {{ .skipped }}' +FirewallRuleOperationResult: 'Результат операции: успешно — {{ .succeeded }}, ошибок — {{ .failed }}' +FirewallCreateRuleSkipped: 'Не выполнено' +FirewallCreateBatchStep: 'Отправка {{ .count }} правил одним пакетом в {{ .backend }}' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; преобразовано в {{ .count }} правил' +FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены' +FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления' +FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось' +FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}' FirewallSyncStep: 'Синхронизировать правила с {{ .name }}' -FirewallSyncResult: 'Результат синхронизации: успешно — {{ .succeeded }}, уже существуют — {{ .existing }}, ошибок — {{ .failed }}' FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}' -FirewallSyncFailedResetSkipped: 'Не удалось синхронизировать правил: {{ .failed }}; исходный межсетевой экран не был сброшен' FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}' FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}' -FirewallVerifyTargetStep: 'Проверить правила целевого межсетевого экрана' FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}' FirewallRestoreRulesStep: 'Восстановить правила базы данных в {{ .name }}' FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Восстановить правила п FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики' FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}' FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker' -FirewallTargetRuleIneffective: 'Целевое правило больше не действует' -FirewallVerifyRuleFailed: 'Не удалось проверить синхронизированное правило {{ .name }}: {{ .detail }}' TaskSync: 'Синхронизировать' LocalApp: 'Локальное приложение' SubTask: 'Подзадача' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 1e777702d0a7..7eb0c3f90576 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} uygulaması mevcut 1Panel sürümü ile uy ErrDockerFailed: 'Docker durumu anormal, lütfen servis durumunu kontrol edin' ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Docker’ı yeniden başlatıp tekrar deneyin' ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 güvenlik duvarı zinciri bulunamadı. Geçerli Docker sürümünün nftables güvenlik duvarı arka ucunu desteklediğini doğrulayın, Docker’ı yeniden başlatıp tekrar deneyin' +ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD varsayılan ilkesi DROP. İlkeyi düzenleyip tekrar deneyin' ErrUFWRuleAdopt: 'UFW kuralı yönetilemedi: {{ .detail }}. Geçerli UFW sürümü 0.35’ten eskiyse UFW’yi yükseltip tekrar deneyin' ErrDockerComposeCmdNotFound: 'Docker Compose komutu mevcut değil, lütfen önce bu komutu host makinesine yükleyin' UseExistImage: 'Görüntü zaten mevcut, mevcut görüntü kullanılıyor' @@ -510,13 +511,20 @@ AppLink: 'Bağlantılı Uygulama' EnableSSL: 'HTTPS Etkinleştir' AppStore: 'Uygulama Mağazası' Firewall: 'Güvenlik duvarı' +FirewallCreateRulesStep: 'Güvenlik duvarı kuralları oluştur' +FirewallCreateRulesResult: 'Oluşturma sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi' +FirewallRuleOperationResult: 'İşlem sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız' +FirewallCreateRuleSkipped: 'Yürütülmedi' +FirewallCreateBatchStep: '{{ .backend }} için {{ .count }} kuralı toplu gönder' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; {{ .count }} kurala dönüştürüldü' +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' FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle' -FirewallSyncResult: 'Eşitleme sonucu: {{ .succeeded }} başarılı, {{ .existing }} mevcut, {{ .failed }} başarısız' FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi' -FirewallSyncFailedResetSkipped: '{{ .failed }} kural eşitlenemedi; kaynak güvenlik duvarı sıfırlanmadı' FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak' FirewallResetSourceResult: 'Kaynak güvenlik duvarı sıfırlandı; {{ .removed }} veritabanı kuralı silindi' -FirewallVerifyTargetStep: 'Hedef güvenlik duvarı kurallarını doğrula' FirewallInitializeChainsStep: '{{ .name }} temel zincirlerini başlat ve bağla' FirewallRestoreRulesStep: 'Veritabanı kurallarını {{ .name }} üzerine geri yükle' FirewallSyncWhitelistStep: 'Güvenlik duvarı bağlantı noktası izin listesini eşitle' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirm FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkelerini denetle' FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla' FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet' -FirewallTargetRuleIneffective: 'Hedef kural artık etkili değil' -FirewallVerifyRuleFailed: 'Eşitlenen {{ .name }} kuralı doğrulanamadı: {{ .detail }}' TaskSync: 'Senkronize Et' LocalApp: 'Yerel Uygulama' SubTask: 'Alt görev' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 6e0a167a190a..0a05dcdca8b8 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} 應用不適配目前 1Panel 版本,跳 ErrDockerFailed: 'Docker 狀態異常,請檢查服務狀態' ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試' ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試' +ErrDockerForwardPolicyDrop: '偵測到 {{ .family }} FORWARD 預設策略為 DROP,請調整後重試' ErrUFWRuleAdopt: 'UFW 規則納管失敗:{{ .detail }}。如果目前 UFW 版本低於 0.35,請先升級 UFW 後再試' ErrDockerComposeCmdNotFound: 'Docker Compose 指令不存在,請先在宿主機安裝此指令' UseExistImage: '映像已存在,使用現有映像' @@ -510,13 +511,20 @@ AppLink: '關聯應用程式' EnableSSL: '開啟HTTPS' AppStore: '應用程式商店' Firewall: '防火牆' +FirewallCreateRulesStep: '建立防火牆規則' +FirewallCreateRulesResult: '建立結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條' +FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條' +FirewallCreateRuleSkipped: '未執行' +FirewallCreateBatchStep: '向 {{ .backend }} 批次提交 {{ .count }} 條規則' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};轉換為 {{ .count }} 條規則' +FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復' +FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗' +FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗' +FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條' FirewallSyncStep: '同步規則到 {{ .name }}' -FirewallSyncResult: '同步結果:成功 {{ .succeeded }} 條,已存在 {{ .existing }} 條,失敗 {{ .failed }} 條' FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗' -FirewallSyncFailedResetSkipped: '{{ .failed }} 條防火牆規則同步失敗,未重設來源防火牆' FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}' FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則' -FirewallVerifyTargetStep: '驗證目標防火牆規則' FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈' FirewallRestoreRulesStep: '恢復資料庫規則至 {{ .name }}' FirewallSyncWhitelistStep: '同步防火牆連接埠白名單' @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則' FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略' FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈' FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態' -FirewallTargetRuleIneffective: '目標規則已失效' -FirewallVerifyRuleFailed: '驗證同步規則 {{ .name }} 失敗:{{ .detail }}' TaskSync: '同步' LocalApp: '本機應用' SubTask: '子任務' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 5c1b1af62172..9e778895c81e 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -161,6 +161,7 @@ ErrAppVersionDeprecated: "{{ .name }} 应用不适配当前 1Panel 版本,跳 ErrDockerFailed: "Docker 状态异常,请检查服务状态" ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试" ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试" +ErrDockerForwardPolicyDrop: '检测到 {{ .family }} FORWARD 默认策略为 DROP,请调整后重试' ErrUFWRuleAdopt: "UFW 规则纳管失败:{{ .detail }}。如果当前 UFW 版本低于 0.35,请先升级 UFW 后重试" ErrDockerComposeCmdNotFound: "Docker Compose 命令不存在,请先在宿主机安装" UseExistImage: "镜像已存在,使用存量镜像" @@ -510,13 +511,20 @@ AppLink: "关联应用" EnableSSL: "开启 HTTPS" AppStore: "应用商店" Firewall: "防火墙" +FirewallCreateRulesStep: '创建防火墙规则' +FirewallCreateRulesResult: '创建结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条' +FirewallRuleOperationResult: '操作结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条' +FirewallCreateRuleSkipped: '未执行' +FirewallCreateBatchStep: '向 {{ .backend }} 批量提交 {{ .count }} 条规则' +FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};转换为 {{ .count }} 条规则' +FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚' +FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败' +FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败' +FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条' FirewallSyncStep: "同步规则到 {{ .name }}" -FirewallSyncResult: "同步结果:成功 {{ .succeeded }} 条,已存在 {{ .existing }} 条,失败 {{ .failed }} 条" FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败" -FirewallSyncFailedResetSkipped: "{{ .failed }} 条防火墙规则同步失败,未重置源防火墙" FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}" FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则" -FirewallVerifyTargetStep: "验证目标防火墙规则" FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链" FirewallRestoreRulesStep: "恢复数据库规则到 {{ .name }}" FirewallSyncWhitelistStep: "同步防火墙端口白名单" @@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则" FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略" FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链" FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态" -FirewallTargetRuleIneffective: "目标规则已失效" -FirewallVerifyRuleFailed: "验证同步规则 {{ .name }} 失败:{{ .detail }}" TaskSync: "同步" LocalApp: "本地应用" SubTask: "子任务" diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index f66ce4c83cbc..5b38adb8a47f 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -34,7 +34,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/rules/search", baseApi.SearchFirewallRules) hostRouter.POST("/firewall/rules/reset", baseApi.ResetFirewallRules) hostRouter.POST("/firewall/rules/native/detail", baseApi.LoadFirewallNativeDetail) - hostRouter.POST("/firewall/rules/check", baseApi.CheckFirewallRules) + hostRouter.POST("/firewall/rules/adopt", baseApi.AdoptFirewallRule) hostRouter.POST("/firewall/rules", baseApi.CreateFirewallRules) hostRouter.POST("/firewall/rules/sync/preview", baseApi.PreviewFirewallRuleSync) hostRouter.GET("/firewall/rules/sync/task", baseApi.LoadFirewallRuleSyncTask) diff --git a/agent/utils/firewall/docker_guard/forward_policy.go b/agent/utils/firewall/docker_guard/forward_policy.go new file mode 100644 index 000000000000..3a923b847eb9 --- /dev/null +++ b/agent/utils/firewall/docker_guard/forward_policy.go @@ -0,0 +1,42 @@ +package docker_guard + +import ( + "errors" + "fmt" + "strings" +) + +var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP") + +func (m *NftablesManager) checkForwardPolicy() error { + for _, family := range []struct{ command, name string }{ + {"iptables", FamilyIPv4}, + {"ip6tables", FamilyIPv6}, + } { + if !m.runner.Exists(family.command) { + continue + } + output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD") + if err != nil { + return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)} + } + found := false + for _, line := range strings.Split(output, "\n") { + fields := strings.Fields(line) + if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" { + continue + } + found = true + if fields[2] == "DROP" { + return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop} + } + if fields[2] != "ACCEPT" { + return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])} + } + } + if !found { + return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")} + } + } + return nil +} diff --git a/agent/utils/firewall/docker_guard/nftables.go b/agent/utils/firewall/docker_guard/nftables.go index 7a753fd4d159..d57e87c924db 100644 --- a/agent/utils/firewall/docker_guard/nftables.go +++ b/agent/utils/firewall/docker_guard/nftables.go @@ -6,6 +6,8 @@ import ( "sort" "strconv" "strings" + + "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" ) const ( @@ -25,13 +27,16 @@ func NewNftablesManager() *NftablesManager { return &NftablesManager{runner: com func (m *NftablesManager) Initialize(policies []Policy) error { mutationMu.Lock() defer mutationMu.Unlock() + if !m.runner.Exists("nft") { + return errors.New("nft is not installed") + } + if err := m.checkForwardPolicy(); err != nil { + return err + } inventory, err := m.ListPolicies() if err != nil { return err } - if !m.runner.Exists("nft") { - return errors.New("nft is not installed") - } if err := m.ensureFamily(FamilyIPv4, true); err != nil { return err } @@ -70,10 +75,10 @@ func (m *NftablesManager) ListPolicies() (PolicyInventory, error) { inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)} for _, family := range []string{FamilyIPv4, FamilyIPv6} { tableFamily := nftTableFamily(family) - if !m.objectExists("chain", tableFamily, NftTable, NftChain) { + output, err := nftables_helper.ReadChain(m.run, tableFamily, NftTable, NftChain) + if errors.Is(err, nftables_helper.ErrChainNotFound) { continue } - output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftChain) if err != nil { return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("list %s chain: %w", NftChain, err)} } diff --git a/agent/utils/firewall/filter/adapter.go b/agent/utils/firewall/filter/adapter.go index 0f92d2172151..9cf7b149dc9f 100644 --- a/agent/utils/firewall/filter/adapter.go +++ b/agent/utils/firewall/filter/adapter.go @@ -22,13 +22,15 @@ const ( ) type DesiredChange struct { - Operation ChangeOperation `json:"operation"` - Before *FirewallRule `json:"before,omitempty"` - After *FirewallRule `json:"after,omitempty"` - Locator *Locator `json:"locator,omitempty"` - PreviousMarker string `json:"previousMarker,omitempty"` - Append bool `json:"append,omitempty"` - RestoreAtEnd bool `json:"restoreAtEnd,omitempty"` + CommandOnly bool `json:"-"` + UnmarkedAdopted bool `json:"-"` + Operation ChangeOperation `json:"operation"` + Before *FirewallRule `json:"before,omitempty"` + After *FirewallRule `json:"after,omitempty"` + Locator *Locator `json:"locator,omitempty"` + PreviousMarker string `json:"previousMarker,omitempty"` + Append bool `json:"append,omitempty"` + RestoreAtEnd bool `json:"restoreAtEnd,omitempty"` } type NativeCommand struct { @@ -47,12 +49,25 @@ type NativeRulePlan struct { } type BackendPlan struct { + CommandOnly bool `json:"-"` Provider Provider `json:"provider"` Scope Scope `json:"scope"` SnapshotRevision string `json:"snapshotRevision"` Rules []NativeRulePlan `json:"rules"` } +func (p BackendPlan) CreatesOnly() bool { + if len(p.Rules) == 0 { + return false + } + for _, rule := range p.Rules { + if rule.Operation != ChangeCreate { + return false + } + } + return true +} + type ApplyResult struct { Applied []ObservedRule `json:"applied"` Verification *VerifyResult `json:"verification,omitempty"` diff --git a/agent/utils/firewall/filter/adopt.go b/agent/utils/firewall/filter/adopt.go index 8fe5946831ac..039b0eb73145 100644 --- a/agent/utils/firewall/filter/adopt.go +++ b/agent/utils/firewall/filter/adopt.go @@ -23,75 +23,3 @@ func CheckAdoptDuplicates(snapshot Snapshot, requested FirewallRule) error { } return nil } - -func CheckAdopt(snapshot Snapshot, requested FirewallRule, desired []DesiredRule, locator Locator) (RuleCheckResult, error) { - normalized, err := NormalizeRule(requested) - if err != nil { - return RuleCheckResult{}, err - } - if normalized.Scope.Key() != snapshot.Scope.Key() { - return RuleCheckResult{}, ErrInvalidScope - } - key, err := RuleKey(normalized) - if err != nil { - return RuleCheckResult{}, err - } - result := RuleCheckResult{RequestedRule: normalized, RequestedRuleKey: key} - var selected *ObservedRule - for index := range snapshot.Rules { - candidate := &snapshot.Rules[index] - if SameLocator(candidate.Locator, locator) { - if selected != nil { - return RuleCheckResult{}, ErrRuleStale - } - selected = candidate - } - } - if selected == nil { - return RuleCheckResult{}, ErrRuleStale - } - selectedKey, err := RuleKey(selected.Rule) - if err != nil || selected.ParseStatus != ParseStatusSupported || selectedKey != key { - return RuleCheckResult{}, fmt.Errorf("%w: selected rule changed or cannot be adopted", ErrRuleStale) - } - if err := CheckAdoptDuplicates(snapshot, normalized); err != nil { - if err != ErrDuplicateAdoption { - return RuleCheckResult{}, err - } - result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationExactExternal, "duplicate_rules" - return finishCheck(result) - } - items, err := MergeInventory(InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) - if err != nil { - return RuleCheckResult{}, err - } - for _, item := range items { - if item.Desired != nil && item.Observed != nil && SameLocator(item.Observed.Locator, locator) { - result.Decision, result.Classification, result.Reason = CheckDecisionNoChange, CheckClassificationExactManaged, "equivalent_managed_rule" - result.ExistingRuleUUID = item.Desired.UUID - return result, nil - } - } - for _, owned := range desired { - same, err := SameRuleContent(owned.Rule, normalized) - if err != nil { - return RuleCheckResult{}, err - } - if same { - result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationExactManaged, "duplicate_rules" - result.ExistingRuleUUID = owned.UUID - return result, nil - } - } - result.Candidates = []ObservedRule{*selected} - switch { - case selected.Protected: - result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationProtected, "protected_rule" - case containsPersistenceDrift(result.Candidates): - result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationConflict, "runtime_permanent_mismatch" - default: - result.Decision, result.Classification, result.Reason = CheckDecisionConfirmationRequired, CheckClassificationExactExternal, "equivalent_external_rule" - result.AllowedActions = []CheckAction{CheckActionAdopt, CheckActionCancel} - } - return finishCheck(result) -} diff --git a/agent/utils/firewall/filter/check.go b/agent/utils/firewall/filter/check.go deleted file mode 100644 index 75064630f4f9..000000000000 --- a/agent/utils/firewall/filter/check.go +++ /dev/null @@ -1,420 +0,0 @@ -package filter - -import ( - "errors" - "fmt" - "net/netip" - "strconv" - "strings" -) - -var ( - ErrRuleConflict = errors.New("firewall rule has identical conditions and an opposing action") - ErrRuleStale = errors.New("firewall rule state is stale") - ErrRuleOperation = errors.New("firewall rule operation is not allowed") - ErrRuleCheckRequired = errors.New("firewall rule must be checked again") -) - -type CheckDecision string - -const ( - CheckDecisionReady CheckDecision = "ready" - CheckDecisionConfirmationRequired CheckDecision = "confirmation_required" - CheckDecisionBlocked CheckDecision = "blocked" - CheckDecisionNoChange CheckDecision = "no_change" -) - -type CheckClassification string - -const ( - CheckClassificationNone CheckClassification = "none" - CheckClassificationExactManaged CheckClassification = "exact_managed" - CheckClassificationExactExternal CheckClassification = "exact_external" - CheckClassificationConflict CheckClassification = "conflict" - CheckClassificationUnsupported CheckClassification = "unsupported" - CheckClassificationProtected CheckClassification = "protected" -) - -type CheckAction string - -const ( - CheckActionCreate CheckAction = "create" - CheckActionAdopt CheckAction = "adopt" - CheckActionSelectAdopt CheckAction = "select_adopt" - CheckActionCancel CheckAction = "cancel" -) - -type RuleCheckResult struct { - Decision CheckDecision `json:"decision"` - Classification CheckClassification `json:"classification"` - Reason string `json:"reason"` - RequestedRule FirewallRule `json:"requestedRule"` - RequestedRuleKey string `json:"requestedRuleKey"` - ExistingRuleUUID string `json:"existingRuleUUID,omitempty"` - Candidates []ObservedRule `json:"candidates,omitempty"` - AllowedActions []CheckAction `json:"allowedActions,omitempty"` -} - -func CheckCreate( - snapshot Snapshot, - requested FirewallRule, - desired []DesiredRule, -) (RuleCheckResult, error) { - normalized, err := NormalizeRule(requested) - if err != nil { - return RuleCheckResult{}, err - } - if normalized.Scope.Key() != snapshot.Scope.Normalize().Key() { - return RuleCheckResult{}, fmt.Errorf("%w: requested scope %q does not match snapshot %q", ErrInvalidRule, normalized.Scope.Key(), snapshot.Scope.Key()) - } - ruleKey, err := RuleKey(normalized) - if err != nil { - return RuleCheckResult{}, err - } - result := RuleCheckResult{ - Decision: CheckDecisionReady, - Classification: CheckClassificationNone, - Reason: "new_rule", - RequestedRule: normalized, - RequestedRuleKey: ruleKey, - } - matchKey, err := RuleMatchKey(normalized) - if err != nil { - return RuleCheckResult{}, err - } - - for _, owned := range desired { - ownedKey, err := desiredRuleKey(owned) - if err != nil { - return RuleCheckResult{}, err - } - ownedMatchKey, err := RuleMatchKey(owned.Rule) - if err != nil { - return RuleCheckResult{}, err - } - if ownedMatchKey != matchKey || (owned.Origin != RuleOriginCreated && owned.Origin != RuleOriginAdopted) { - continue - } - if OppositeActions(normalized.Action, owned.Rule.Action) { - result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationConflict, "exact_rule_conflict" - return finishCheck(result) - } - if normalized.Action != owned.Rule.Action { - continue - } - owned.RuleKey = ownedKey - result.ExistingRuleUUID = owned.UUID - if snapshotHasOwnedRule(snapshot, owned) { - result.Decision = CheckDecisionNoChange - result.Classification = CheckClassificationExactManaged - result.Reason = "equivalent_managed_rule" - } else { - result.Decision = CheckDecisionBlocked - result.Classification = CheckClassificationConflict - result.Reason = "managed_rule_drifted" - } - return finishCheck(result) - } - - exact := make([]ObservedRule, 0) - equivalentExternal := false - for _, observed := range snapshot.Rules { - if observed.ParseStatus != ParseStatusSupported { - if hydrated, ok := hydrateOwnedPartialRule(observed, desired); ok { - observed = hydrated - } else { - continue - } - } - observedRule, err := NormalizeRule(observed.Rule) - if err != nil { - return RuleCheckResult{}, err - } - observed.Rule = observedRule - observedKey, err := RuleMatchKey(observedRule) - if err != nil { - return RuleCheckResult{}, err - } - if observedKey != matchKey { - continue - } - if OppositeActions(normalized.Action, observedRule.Action) { - result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationConflict, "exact_rule_conflict" - return finishCheck(result) - } - if normalized.Action == observedRule.Action { - equivalentExternal = true - key, err := RuleKey(observedRule) - if err != nil { - return RuleCheckResult{}, err - } - if key == ruleKey { - exact = append(exact, observed) - } - } - } - - if err := CheckAdoptDuplicates(snapshot, normalized); err != nil { - if err != ErrDuplicateAdoption { - return RuleCheckResult{}, err - } - result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationExactExternal, "duplicate_rules" - return finishCheck(result) - } - switch { - case containsPersistenceDrift(exact): - result.Decision = CheckDecisionBlocked - result.Classification = CheckClassificationConflict - result.Reason = "runtime_permanent_mismatch" - result.Candidates = exact - case containsProtectedRule(exact): - result.Decision = CheckDecisionBlocked - result.Classification = CheckClassificationProtected - result.Reason = "protected_rule" - result.Candidates = exact - case len(exact) == 1: - result.Decision = CheckDecisionConfirmationRequired - result.Classification = CheckClassificationExactExternal - result.Reason = "equivalent_external_rule" - result.Candidates = exact - result.AllowedActions = []CheckAction{CheckActionAdopt, CheckActionCancel} - case equivalentExternal: - result.Decision = CheckDecisionNoChange - result.Classification = CheckClassificationExactExternal - result.Reason = "equivalent_external_rule" - default: - result.AllowedActions = []CheckAction{CheckActionCreate} - } - return finishCheck(result) -} - -func hydrateOwnedPartialRule(observed ObservedRule, desired []DesiredRule) (ObservedRule, bool) { - if observed.ParseStatus != ParseStatusPartial || strings.TrimSpace(observed.Marker) == "" || - len(observed.UncertainFields) != 1 || observed.UncertainFields[0] != ObservedFieldProtocol { - return observed, false - } - var matched *DesiredRule - for index := range desired { - candidate := &desired[index] - if candidate.Origin != RuleOriginCreated && candidate.Origin != RuleOriginAdopted { - continue - } - if candidate.Rule.Scope.Normalize().Key() != observed.Rule.Scope.Normalize().Key() || - !desiredMarkerMatchesObserved(*candidate, observed.Marker) || - !ObservedRuleMatchesExpected(observed, candidate.Rule) { - continue - } - if matched != nil { - return observed, false - } - matched = candidate - } - if matched == nil { - return observed, false - } - orderIndex := observed.Rule.OrderIndex - observed.Rule = matched.Rule - observed.Rule.OrderIndex = orderIndex - observed.ParseStatus = ParseStatusSupported - observed.UncertainFields = nil - return observed, true -} - -func desiredMarkerMatchesObserved(desired DesiredRule, observedMarker string) bool { - observedMarker = strings.TrimSpace(observedMarker) - if observedMarker == strings.TrimSpace(desired.Marker) { - return observedMarker != "" - } - return desired.Origin == RuleOriginAdopted && strings.TrimSpace(desired.UUID) != "" && - observedMarker == "1panel-rule:"+strings.TrimSpace(desired.UUID) -} - -func finishCheck(result RuleCheckResult) (RuleCheckResult, error) { - for index, candidate := range result.Candidates { - identity, err := InstanceKey(candidate) - if err != nil { - identity, err = RuleKey(candidate.Rule) - if err != nil { - return RuleCheckResult{}, err - } - } - result.Candidates[index].InstanceKey = identity - } - return result, nil -} - -func containsPersistenceDrift(rules []ObservedRule) bool { - for _, rule := range rules { - if rule.Persistence != "" && rule.Persistence != PersistenceStatusConverged { - return true - } - } - return false -} - -func containsProtectedRule(rules []ObservedRule) bool { - for _, rule := range rules { - if rule.Protected { - return true - } - } - return false -} - -func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, error) { - matched := make([]ObservedRule, 0, 1) - for _, candidate := range candidates { - identity, err := InstanceKey(candidate) - if err != nil { - identity, err = RuleKey(candidate.Rule) - if err != nil { - continue - } - } - if selected != "" && identity == selected { - matched = append(matched, candidate) - } - } - if len(matched) != 1 { - return ObservedRule{}, ErrRuleOperation - } - return matched[0], nil -} - -func desiredRuleKey(desired DesiredRule) (string, error) { - key, err := RuleKey(desired.Rule) - if err != nil { - return "", err - } - if desired.RuleKey != "" && desired.RuleKey != key { - return "", fmt.Errorf("%w: desired rule %q key mismatch", ErrInvalidRule, desired.UUID) - } - return key, nil -} - -func snapshotHasOwnedRule(snapshot Snapshot, desired DesiredRule) bool { - items, err := MergeInventory(InventoryMergeInput{Observed: snapshot.Rules, Desired: []DesiredRule{desired}}) - if err != nil { - return false - } - for _, item := range items { - if item.Desired != nil { - return item.Match == InventoryMatchExact && item.State != InventoryStateDrifted - } - } - return false -} - -func RulesOverlap(left, right FirewallRule) bool { - left, leftErr := NormalizeRule(left) - right, rightErr := NormalizeRule(right) - if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() { - return false - } - return familiesOverlap(left.Scope.Family, right.Scope.Family) && - protocolsOverlap(left.Protocol, right.Protocol) && - addressesOverlap(left.SourceAddress, right.SourceAddress) && - addressesOverlap(left.DestinationAddress, right.DestinationAddress) && - portsOverlap(left.SourcePort, right.SourcePort) && - portsOverlap(left.DestinationPort, right.DestinationPort) && - (left.Interface == "" || right.Interface == "" || left.Interface == right.Interface) -} - -func familiesOverlap(left, right Family) bool { - return left == FamilyInet || right == FamilyInet || left == right -} - -func protocolsOverlap(left, right string) bool { - return left == "all" || right == "all" || left == right -} - -func addressesOverlap(left, right string) bool { - if left == "" || right == "" { - return true - } - leftPrefix, leftErr := netip.ParsePrefix(left) - rightPrefix, rightErr := netip.ParsePrefix(right) - if leftErr != nil || rightErr != nil { - return false - } - return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr()) -} - -func portCovers(existing, requested string) bool { - if existing == "" { - return true - } - if requested == "" { - return false - } - existingIntervals, err := portIntervals(existing) - if err != nil { - return false - } - requestedIntervals, err := portIntervals(requested) - if err != nil { - return false - } - for _, requestedInterval := range requestedIntervals { - covered := false - for _, existingInterval := range existingIntervals { - if existingInterval[0] <= requestedInterval[0] && existingInterval[1] >= requestedInterval[1] { - covered = true - break - } - } - if !covered { - return false - } - } - return true -} - -func portsOverlap(left, right string) bool { - if left == "" || right == "" { - return true - } - leftIntervals, leftErr := portIntervals(left) - rightIntervals, rightErr := portIntervals(right) - if leftErr != nil || rightErr != nil { - return false - } - for _, leftInterval := range leftIntervals { - for _, rightInterval := range rightIntervals { - if leftInterval[0] <= rightInterval[1] && rightInterval[0] <= leftInterval[1] { - return true - } - } - } - return false -} - -func portIntervals(value string) ([][2]int, error) { - parts := strings.Split(value, ",") - intervals := make([][2]int, 0, len(parts)) - for _, part := range parts { - start, end, err := portInterval(strings.TrimSpace(part)) - if err != nil { - return nil, err - } - intervals = append(intervals, [2]int{start, end}) - } - return intervals, nil -} - -func portInterval(value string) (int, int, error) { - parts := strings.Split(value, "-") - if len(parts) == 1 { - port, err := strconv.Atoi(parts[0]) - return port, port, err - } - if len(parts) != 2 { - return 0, 0, fmt.Errorf("invalid port interval %q", value) - } - start, err := strconv.Atoi(parts[0]) - if err != nil { - return 0, 0, err - } - end, err := strconv.Atoi(parts[1]) - return start, end, err -} diff --git a/agent/utils/firewall/filter/check_flag.go b/agent/utils/firewall/filter/check_flag.go deleted file mode 100644 index 4e90c6e9ea8e..000000000000 --- a/agent/utils/firewall/filter/check_flag.go +++ /dev/null @@ -1,170 +0,0 @@ -package filter - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "strings" -) - -type CheckFlagCodec struct { - secret []byte - version int -} - -type checkFlagClaims struct { - Version int `json:"version"` - Provider Provider `json:"provider"` - ScopeKey string `json:"scopeKey"` - RuleDigest string `json:"ruleDigest"` - SnapshotRevision string `json:"snapshotRevision"` - ManagedRevision string `json:"managedRevision"` - Decision CheckDecision `json:"decision"` - Classification CheckClassification `json:"classification"` - AllowedActions []CheckAction `json:"allowedActions"` - AdoptionCandidates []checkFlagAdoptionCandidate `json:"adoptionCandidates,omitempty"` -} - -type checkFlagAdoptionCandidate struct { - InstanceKey string `json:"instanceKey"` - Locator Locator `json:"locator"` -} - -type CreateAuthorization struct { - Operation ChangeOperation - Locator *Locator -} - -func NewCheckFlagCodec(secret []byte, version int) *CheckFlagCodec { - return &CheckFlagCodec{secret: append([]byte(nil), secret...), version: version} -} - -func (c *CheckFlagCodec) Sign(result RuleCheckResult, snapshot Snapshot, managedRevision string) (string, error) { - ruleDigest, err := ruleDigest(result.RequestedRule) - if err != nil { - return "", err - } - claims := checkFlagClaims{ - Version: c.version, - Provider: result.RequestedRule.Scope.Provider, - ScopeKey: result.RequestedRule.Scope.Key(), - RuleDigest: ruleDigest, - SnapshotRevision: snapshot.Revision, - ManagedRevision: managedRevision, - Decision: result.Decision, - Classification: result.Classification, - AllowedActions: result.AllowedActions, - } - if result.Classification == CheckClassificationExactExternal { - claims.AdoptionCandidates = make([]checkFlagAdoptionCandidate, 0, len(result.Candidates)) - for _, candidate := range result.Candidates { - claims.AdoptionCandidates = append(claims.AdoptionCandidates, checkFlagAdoptionCandidate{ - InstanceKey: candidate.InstanceKey, - Locator: candidate.Locator, - }) - } - } - payload, err := json.Marshal(claims) - if err != nil { - return "", err - } - signature := c.signature(payload) - return base64.RawURLEncoding.EncodeToString(payload) + "." + base64.RawURLEncoding.EncodeToString(signature), nil -} - -func (c *CheckFlagCodec) Authorize( - checkFlag string, - action CheckAction, - adoptInstanceKey string, - rule FirewallRule, - snapshot Snapshot, - managedRevision string, -) (CreateAuthorization, error) { - claims, err := c.parse(checkFlag) - if err != nil { - return CreateAuthorization{}, err - } - ruleDigest, err := ruleDigest(rule) - if err != nil { - return CreateAuthorization{}, err - } - if claims.Version != c.version || - claims.Provider != rule.Scope.Provider || - claims.ScopeKey != rule.Scope.Key() || - claims.RuleDigest != ruleDigest || - claims.SnapshotRevision != snapshot.Revision || - claims.ManagedRevision != managedRevision { - return CreateAuthorization{}, fmt.Errorf("%w: firewall or managed rules changed", ErrRuleCheckRequired) - } - if claims.Decision != CheckDecisionReady && claims.Decision != CheckDecisionConfirmationRequired { - return CreateAuthorization{}, ErrRuleOperation - } - if !containsCheckAction(claims.AllowedActions, action) { - return CreateAuthorization{}, ErrRuleOperation - } - - switch action { - case CheckActionCreate: - if strings.TrimSpace(adoptInstanceKey) != "" { - return CreateAuthorization{}, ErrRuleOperation - } - return CreateAuthorization{Operation: ChangeCreate}, nil - case CheckActionAdopt, CheckActionSelectAdopt: - for _, candidate := range claims.AdoptionCandidates { - if candidate.InstanceKey == adoptInstanceKey && adoptInstanceKey != "" { - locator := candidate.Locator - return CreateAuthorization{Operation: ChangeAdopt, Locator: &locator}, nil - } - } - return CreateAuthorization{}, ErrRuleOperation - default: - return CreateAuthorization{}, ErrRuleOperation - } -} - -func (c *CheckFlagCodec) parse(checkFlag string) (checkFlagClaims, error) { - parts := strings.Split(strings.TrimSpace(checkFlag), ".") - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return checkFlagClaims{}, ErrRuleCheckRequired - } - payload, err := base64.RawURLEncoding.DecodeString(parts[0]) - if err != nil { - return checkFlagClaims{}, ErrRuleCheckRequired - } - signature, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil || !hmac.Equal(signature, c.signature(payload)) { - return checkFlagClaims{}, ErrRuleCheckRequired - } - var claims checkFlagClaims - if err := json.Unmarshal(payload, &claims); err != nil { - return checkFlagClaims{}, ErrRuleCheckRequired - } - return claims, nil -} - -func (c *CheckFlagCodec) signature(payload []byte) []byte { - mac := hmac.New(sha256.New, c.secret) - _, _ = mac.Write(payload) - return mac.Sum(nil) -} - -func ruleDigest(rule FirewallRule) (string, error) { - payload, err := json.Marshal(rule) - if err != nil { - return "", err - } - sum := sha256.Sum256(payload) - return hex.EncodeToString(sum[:]), nil -} - -func containsCheckAction(actions []CheckAction, expected CheckAction) bool { - for _, action := range actions { - if action == expected { - return true - } - } - return false -} diff --git a/agent/utils/firewall/filter/identity.go b/agent/utils/firewall/filter/identity.go index 398dc8267ff5..7bdebecf9868 100644 --- a/agent/utils/firewall/filter/identity.go +++ b/agent/utils/firewall/filter/identity.go @@ -289,3 +289,20 @@ func hashJSON(value any) (string, error) { sum := sha256.Sum256(payload) return "sha256:" + hex.EncodeToString(sum[:]), nil } + +func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, error) { + matched := make([]ObservedRule, 0, 1) + for _, candidate := range candidates { + identity, err := InstanceKey(candidate) + if err != nil { + continue + } + if selected != "" && identity == selected { + matched = append(matched, candidate) + } + } + if len(matched) != 1 { + return ObservedRule{}, ErrRuleOperation + } + return matched[0], nil +} diff --git a/agent/utils/firewall/filter/inventory.go b/agent/utils/firewall/filter/inventory.go index eff013123c63..0ed27c3f06e5 100644 --- a/agent/utils/firewall/filter/inventory.go +++ b/agent/utils/firewall/filter/inventory.go @@ -103,6 +103,7 @@ func MergeInventory(input InventoryMergeInput) ([]InventoryItem, error) { byRuleKey[candidate.ruleKey] = append(byRuleKey[candidate.ruleKey], index) if instanceKey, err := instanceKeyWithRuleKey(candidate.rule, candidate.ruleKey); err == nil { candidate.instanceKey = instanceKey + candidate.rule.InstanceKey = instanceKey byInstanceKey[instanceKey] = append(byInstanceKey[instanceKey], index) } } diff --git a/agent/utils/firewall/filter/providers/firewalld/adapter.go b/agent/utils/firewall/filter/providers/firewalld/adapter.go index cd8dc34bd856..82bd0da47bd2 100644 --- a/agent/utils/firewall/filter/providers/firewalld/adapter.go +++ b/agent/utils/firewall/filter/providers/firewalld/adapter.go @@ -174,9 +174,6 @@ func (a *Adapter) PrepareRule(rule filter.FirewallRule) (filter.FirewallRule, er return filter.FirewallRule{}, err } } else if normalized.NativeKind == filter.NativeKindZonePort && !isNativeZonePort(normalized) { - // Edit requests inherit the current native kind. Once a native zone port - // gains an address, loses its port, or changes to a deny action, it must - // be represented as a rich rule instead. normalized.NativeKind = filter.NativeKindRichRule normalized, err = filter.NormalizeRule(normalized) if err != nil { @@ -231,6 +228,9 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap executed := 0 for index, command := range rulePlan.Commands { if err := a.writer.Run(ctx, command); err != nil { + if plan.CommandOnly { + return filter.ApplyResult{}, err + } return filter.ApplyResult{}, a.compensate(ctx, rulePlan, executed, err) } executed = index + 1 @@ -328,7 +328,7 @@ func (a *Adapter) compileChange(snapshot filter.Snapshot, change filter.DesiredC if target.Locator.Canonical == expected.Locator.Canonical { break } - removeCommands, restoreCommands := pairedCommands(target.Rule, "remove", "add") + removeCommands, restoreCommands := observedPairedCommands(target, "remove", "add") addCommands, removeNewCommands := missingRuleCommands(snapshot, normalized) plan.Commands = append(removeCommands, addCommands...) plan.RollbackCommands = append(restoreCommands, removeNewCommands...) @@ -340,7 +340,15 @@ func (a *Adapter) compileChange(snapshot filter.Snapshot, change filter.DesiredC plan.Previous = &target plan.Expected = target plan.Expected.Rule.UUID = normalized.UUID - plan.Commands, plan.RollbackCommands = pairedCommands(target.Rule, "remove", "add") + plan.Commands, plan.RollbackCommands = observedPairedCommands(target, "remove", "add") + if change.CommandOnly { + switch target.Persistence { + case filter.PersistenceStatusRuntimeOnly: + plan.Commands, plan.RollbackCommands = plan.Commands[:1], plan.RollbackCommands[:1] + case filter.PersistenceStatusPermanentOnly: + plan.Commands, plan.RollbackCommands = plan.Commands[1:], plan.RollbackCommands[1:] + } + } default: return filter.NativeRulePlan{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) } @@ -420,9 +428,20 @@ func missingRuleCommands(snapshot filter.Snapshot, rule filter.FirewallRule) ([] } func pairedCommands(rule filter.FirewallRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) { - option := nativeOption(rule, operation) - rollback := nativeOption(rule, inverse) - selector := scopeSelector(rule.Scope) + return pairedNativeCommands(rule.Scope, nativeOption(rule, operation), nativeOption(rule, inverse)) +} + +func observedPairedCommands(observed filter.ObservedRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) { + if observed.Rule.NativeKind != filter.NativeKindRichRule || observed.Raw == "" { + return pairedCommands(observed.Rule, operation, inverse) + } + return pairedNativeCommands(observed.Rule.Scope, + "--"+operation+"-rich-rule="+observed.Raw, + "--"+inverse+"-rich-rule="+observed.Raw) +} + +func pairedNativeCommands(scope filter.Scope, option, rollback string) ([]filter.NativeCommand, []filter.NativeCommand) { + selector := scopeSelector(scope) commands := []filter.NativeCommand{ {Executable: "firewall-cmd", Args: []string{selector, option}}, {Executable: "firewall-cmd", Args: []string{"--permanent", selector, option}}, @@ -464,7 +483,8 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang if target.Protected { return filter.ObservedRule{}, filter.ErrProtectedRule } - if target.ParseStatus != filter.ParseStatusSupported || target.Persistence != filter.PersistenceStatusConverged { + if target.ParseStatus != filter.ParseStatusSupported || + (target.Persistence != filter.PersistenceStatusConverged && !(change.CommandOnly && change.Operation == filter.ChangeDelete)) { return filter.ObservedRule{}, filter.ErrRuleStale } want := normalized @@ -511,6 +531,11 @@ func scopeSelector(scope filter.Scope) string { } func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, executed int, cause error) error { + if plan.Operation == filter.ChangeCreate { + return cause + } + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() rollbackErr := a.rollback(ctx, plan.Expected.Rule.Scope, plan, executed) if rollbackErr != nil { return fmt.Errorf("firewalld apply failed: %w; compensation failed: %v", cause, rollbackErr) diff --git a/agent/utils/firewall/filter/providers/iptables/adapter.go b/agent/utils/firewall/filter/providers/iptables/adapter.go index ea18ad68bfca..8a82ea4d662e 100644 --- a/agent/utils/firewall/filter/providers/iptables/adapter.go +++ b/agent/utils/firewall/filter/providers/iptables/adapter.go @@ -158,12 +158,18 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap executed := 0 for _, command := range rulePlan.Commands { if err := a.writer.Run(ctx, command); err != nil { + if plan.CommandOnly { + return filter.ApplyResult{}, err + } return filter.ApplyResult{}, a.compensate(ctx, plan, ruleIndex, executed, err) } executed++ } } if err := a.writer.Save(ctx, plan.Scope); err != nil { + if plan.CommandOnly { + return filter.ApplyResult{}, err + } return filter.ApplyResult{}, a.compensate(ctx, plan, len(plan.Rules)-1, -1, err) } applied := make([]filter.ObservedRule, 0, len(plan.Rules)) @@ -363,6 +369,11 @@ func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error { } func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, lastRule, lastCommandCount int, cause error) error { + if plan.CreatesOnly() { + return cause + } + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() rollbackErr := a.rollback(ctx, plan, lastRule, lastCommandCount) if rollbackErr != nil { return fmt.Errorf("iptables apply failed: %w; compensation failed: %v", cause, rollbackErr) @@ -641,7 +652,8 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang if wantErr != nil || observedErr != nil || wantKey != observedKey { return 0, filter.ObservedRule{}, filter.ErrRuleStale } - if change.Operation != filter.ChangeAdopt && observed.Marker != marker { + if change.Operation != filter.ChangeAdopt && observed.Marker != marker && + !(change.Operation == filter.ChangeDelete && change.UnmarkedAdopted && observed.Marker == "") { return 0, filter.ObservedRule{}, filter.ErrRuleStale } return position, observed, nil @@ -674,18 +686,7 @@ func (systemBackend) CheckMultiport(ctx context.Context, family filter.Family) e } func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) { - var output string - var err error - if scope.Family == filter.FamilyIPv6 { - output, err = native.RunIPv6WithStdContext(ctx, scope.Table, "-S", scope.Chain) - if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) { - if table, readErr := native.RunIPv6WithStdContext(ctx, scope.Table, "-S"); readErr == nil && !containsChainDeclaration(table, scope.Chain) { - return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain) - } - } - } else { - output, err = native.RunWithStdContext(ctx, scope.Table, "-S", scope.Chain) - } + output, err := native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6) if err != nil { return "", err } diff --git a/agent/utils/firewall/filter/providers/nftables/adapter.go b/agent/utils/firewall/filter/providers/nftables/adapter.go index f6cd9d91d1fc..7870c0563ef5 100644 --- a/agent/utils/firewall/filter/providers/nftables/adapter.go +++ b/agent/utils/firewall/filter/providers/nftables/adapter.go @@ -2,6 +2,7 @@ package nftables import ( "context" + "errors" "fmt" "strconv" "strings" @@ -42,6 +43,16 @@ func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snaps return filter.Snapshot{}, fmt.Errorf("nftables backend is required") } output, err := a.backend.ListChain(ctx, scope) + if errors.Is(err, nftables_helper.ErrChainNotFound) { + snapshot, snapshotErr := filter.NewSnapshot(scope, nil) + if snapshotErr != nil { + return filter.Snapshot{}, snapshotErr + } + snapshot.Notices = []filter.ScopeNotice{{ + Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(scope.Family), scope.Chain}, + }} + return snapshot, nil + } if err != nil { return filter.Snapshot{}, err } @@ -111,11 +122,17 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap for _, rulePlan := range plan.Rules { for _, command := range rulePlan.Commands { if err := a.backend.Run(ctx, command); err != nil { + if plan.CommandOnly { + return filter.ApplyResult{}, err + } return filter.ApplyResult{}, a.compensate(ctx, plan, err) } } } if err := a.backend.Save(ctx); err != nil { + if plan.CommandOnly { + return filter.ApplyResult{}, err + } return filter.ApplyResult{}, a.compensate(ctx, plan, err) } applied := make([]filter.ObservedRule, 0, len(plan.Rules)) @@ -178,6 +195,11 @@ func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error { } func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, cause error) error { + if plan.CreatesOnly() { + return cause + } + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() if err := a.Rollback(ctx, plan); err != nil { return fmt.Errorf("nftables apply failed: %w; compensation failed: %v", cause, err) } @@ -378,8 +400,6 @@ func parseChain(scope filter.Scope, output string) []filter.ObservedRule { continue } raw := strings.TrimSpace(line[:handleIndex]) - // `nft -a list chain` includes a handle on the chain declaration itself. - // It is metadata for the chain, not a rule in the chain. if strings.HasPrefix(raw, "chain ") { continue } @@ -393,7 +413,11 @@ func parseChain(scope filter.Scope, output string) []filter.ObservedRule { func parseRule(scope filter.Scope, raw, handle string, position int) filter.ObservedRule { locator := filter.Locator{Provider: filter.ProviderNftables, ScopeKey: scope.Key(), NativeID: handle, Canonical: raw, Position: &position} opaque := func() filter.ObservedRule { - return filter.ObservedRule{Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque}, Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw} + return filter.ObservedRule{ + Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque}, + Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw, + Protected: scope.Chain != filter.IptablesInputChain, + } } tokens, err := shellwords.Parse(raw) if err != nil { @@ -416,10 +440,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse return opaque() } } else if tokens[index+1] == "l4proto" { - rule.Protocol = tokens[index+2] - if rule.Protocol == "ipv6-icmp" { - rule.Protocol = "icmpv6" - } + rule.Protocol = parseProtocol(tokens[index+2]) } else { return opaque() } @@ -463,26 +484,26 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse return opaque() } index += 2 - if tokens[index] != "{" { - for _, state := range strings.Split(tokens[index], ",") { - if state = strings.TrimSpace(state); state != "" { - rule.ConnectionStates = append(rule.ConnectionStates, state) - } - } + values := tokens[index] + if values == "{" { index++ - continue - } - index++ - for index < len(tokens) && tokens[index] != "}" { - state := strings.Trim(tokens[index], ",") - if state != "" { - rule.ConnectionStates = append(rule.ConnectionStates, state) + start := index + for index < len(tokens) && tokens[index] != "}" { + index++ } - index++ + if index == len(tokens) { + return opaque() + } + values = strings.Join(tokens[start:index], " ") } - if index < len(tokens) && tokens[index] == "}" { - index++ + for _, state := range strings.Split(values, ",") { + state = strings.TrimSpace(state) + if state == "" { + return opaque() + } + rule.ConnectionStates = append(rule.ConnectionStates, parseConnectionState(state)) } + index++ case "accept", "drop", "reject": rule.Action = filter.Action(tokens[index]) index++ @@ -515,17 +536,59 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: scope.Chain != filter.IptablesInputChain} } +func parseProtocol(value string) string { + switch numericSymbol(value) { + case "1": + return "icmp" + case "6": + return "tcp" + case "17": + return "udp" + case "58", "ipv6-icmp": + return "icmpv6" + default: + return value + } +} + +func parseConnectionState(value string) string { + switch numericSymbol(value) { + case "1": + return "invalid" + case "2": + return "established" + case "4": + return "related" + case "8": + return "new" + case "64": + return "untracked" + default: + return value + } +} + +func numericSymbol(value string) string { + base := 10 + digits := value + if strings.HasPrefix(strings.ToLower(value), "0x") { + base, digits = 16, value[2:] + } + if number, err := strconv.ParseUint(digits, base, 32); err == nil { + return strconv.FormatUint(number, 10) + } + return value +} + type systemBackend struct{} func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) { - output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout( - "nft", "-n", "-n", "-a", "list", "chain", nftables_helper.TableFamily(scope.Family), nftables_helper.TableName, nativeChainName(scope), - ) - if err != nil && scope.Family == filter.FamilyIPv6 && - (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) { - return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err) - } - return output, err + run := func(args ...string) (string, error) { + return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout( + "nft", append([]string{"-n", "-n"}, args...)..., + ) + } + return nftables_helper.ReadChain(run, nftables_helper.TableFamily(scope.Family), nftables_helper.TableName, nativeChainName(scope)) } func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) error { diff --git a/agent/utils/firewall/filter/providers/ufw/adapter.go b/agent/utils/firewall/filter/providers/ufw/adapter.go index b5d499982776..397fe4ee808e 100644 --- a/agent/utils/firewall/filter/providers/ufw/adapter.go +++ b/agent/utils/firewall/filter/providers/ufw/adapter.go @@ -183,14 +183,24 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap executed := 0 for index, command := range plan.Rules[0].Commands { if err := a.writer.Run(ctx, command); err != nil { - if a.failedCommandApplied(ctx, plan.Rules[0], index) { - executed = index + 1 + if plan.CommandOnly { + return filter.ApplyResult{}, fmt.Errorf("execute UFW rule: %w", err) + } + if !plan.CreatesOnly() { + probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + if a.failedCommandApplied(probeCtx, plan.Rules[0], index) { + executed = index + 1 + } + cancel() } cause := ufwApplyError(plan.Rules[0], fmt.Errorf("execute UFW rule: %w", err)) return filter.ApplyResult{}, a.compensate(ctx, plan.Rules[0], executed, cause) } executed = index + 1 } + if plan.CommandOnly || plan.CreatesOnly() { + return filter.ApplyResult{Applied: []filter.ObservedRule{plan.Rules[0].Expected}}, nil + } verification, err := a.verify(ctx, plan) if err != nil { cause := ufwApplyError(plan.Rules[0], fmt.Errorf("verify UFW rule: %w", err)) @@ -392,7 +402,7 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte deleteRuleCommand(normalized, marker), } case filter.ChangeDelete: - target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, true) + target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, !change.UnmarkedAdopted) if targetErr != nil { return filter.NativeRulePlan{}, targetErr } @@ -628,10 +638,6 @@ func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.V positionMatches := rulePlan.Expected.Locator.Position == nil || (observed.Locator.Position != nil && *observed.Locator.Position == *rulePlan.Expected.Locator.Position) semanticMatches := filter.ObservedRuleMatchesExpected(observed, rulePlan.Expected.Rule) - // A unique generated marker proves that UFW committed this command. Some - // UFW versions render otherwise valid rules in a form the numbered-status - // parser cannot fully recover. Do not turn that read-side limitation into - // a failed write, but keep rejecting fully parsed semantic mismatches. if observed.ParseStatus == filter.ParseStatusOpaque { semanticMatches = true } @@ -644,6 +650,11 @@ func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.V } func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, executed int, cause error) error { + if plan.Operation == filter.ChangeCreate { + return cause + } + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() rollbackErr := a.rollback(ctx, plan, executed) if rollbackErr != nil { return fmt.Errorf("ufw apply failed: %w; compensation failed: %v", cause, rollbackErr) diff --git a/agent/utils/firewall/filter/runtime/runtime.go b/agent/utils/firewall/filter/runtime/runtime.go index dba7adec69d6..3913ec88fee7 100644 --- a/agent/utils/firewall/filter/runtime/runtime.go +++ b/agent/utils/firewall/filter/runtime/runtime.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld" @@ -236,6 +237,35 @@ func (e *Engine) Capabilities(ctx context.Context) (filter.Capabilities, error) return e.adapter.Capabilities(ctx) } +func (e *Engine) ExecuteCreate(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error { + plan, err := e.adapter.Compile(snapshot, changes) + if err != nil { + return err + } + if !plan.CreatesOnly() { + return fmt.Errorf("%w: expected a create-only plan", filter.ErrInvalidRule) + } + _, err = e.adapter.Apply(ctx, plan) + return err +} + +func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error { + if err := ctx.Err(); err != nil { + return err + } + changes = append([]filter.DesiredChange(nil), changes...) + for index := range changes { + changes[index].CommandOnly = true + } + plan, err := e.adapter.Compile(snapshot, changes) + if err != nil { + return err + } + plan.CommandOnly = true + _, err = e.adapter.Apply(ctx, plan) + return err +} + func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, filter.VerifyResult, error) { plan, err := e.adapter.Compile(snapshot, changes) if err != nil { @@ -246,7 +276,7 @@ func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes return plan, filter.VerifyResult{}, err } if result.Verification != nil { - if !result.Verification.Matched { + if !result.Verification.Matched && !plan.CreatesOnly() { if rollbackErr := e.Rollback(ctx, plan); rollbackErr != nil { return plan, *result.Verification, errors.Join(filter.ErrVerificationFailed, rollbackErr) } @@ -255,9 +285,12 @@ func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes } verification, err := e.adapter.Verify(ctx, plan) if err != nil { + if plan.CreatesOnly() { + return plan, verification, err + } return plan, verification, e.rollback(ctx, plan, err) } - if !verification.Matched { + if !verification.Matched && !plan.CreatesOnly() { if rollbackErr := e.Rollback(ctx, plan); rollbackErr != nil { return plan, verification, errors.Join(filter.ErrVerificationFailed, rollbackErr) } @@ -270,6 +303,8 @@ func (e *Engine) Rollback(ctx context.Context, plan filter.BackendPlan) error { if !ok { return fmt.Errorf("%w: provider %s does not support applied-plan rollback", filter.ErrAdapterUnavailable, e.adapter.Provider()) } + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() return rollbacker.Rollback(ctx, plan) } diff --git a/agent/utils/firewall/filter/safety.go b/agent/utils/firewall/filter/safety.go index e9677f7053f9..de562ea18733 100644 --- a/agent/utils/firewall/filter/safety.go +++ b/agent/utils/firewall/filter/safety.go @@ -3,9 +3,17 @@ package filter import ( "errors" "fmt" + "net/netip" + "strconv" "strings" ) +var ( + ErrRuleConflict = errors.New("firewall rule has identical conditions and an opposing action") + ErrRuleStale = errors.New("firewall rule state is stale") + ErrRuleOperation = errors.New("firewall rule operation is not allowed") +) + var ErrVerificationFailed = errors.New("firewall rule verification failed") func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error) { @@ -127,3 +135,117 @@ func FindCommittedObserved(snapshot Snapshot, requested FirewallRule, plan Backe } return matches[0], nil } + +func RulesOverlap(left, right FirewallRule) bool { + left, leftErr := NormalizeRule(left) + right, rightErr := NormalizeRule(right) + if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() { + return false + } + return familiesOverlap(left.Scope.Family, right.Scope.Family) && + protocolsOverlap(left.Protocol, right.Protocol) && + addressesOverlap(left.SourceAddress, right.SourceAddress) && + addressesOverlap(left.DestinationAddress, right.DestinationAddress) && + portsOverlap(left.SourcePort, right.SourcePort) && + portsOverlap(left.DestinationPort, right.DestinationPort) && + (left.Interface == "" || right.Interface == "" || left.Interface == right.Interface) +} + +func familiesOverlap(left, right Family) bool { + return left == FamilyInet || right == FamilyInet || left == right +} + +func protocolsOverlap(left, right string) bool { + return left == "all" || right == "all" || left == right +} + +func addressesOverlap(left, right string) bool { + if left == "" || right == "" { + return true + } + leftPrefix, leftErr := netip.ParsePrefix(left) + rightPrefix, rightErr := netip.ParsePrefix(right) + if leftErr != nil || rightErr != nil { + return false + } + return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr()) +} + +func portCovers(existing, requested string) bool { + if existing == "" { + return true + } + if requested == "" { + return false + } + existingIntervals, err := portIntervals(existing) + if err != nil { + return false + } + requestedIntervals, err := portIntervals(requested) + if err != nil { + return false + } + for _, requestedInterval := range requestedIntervals { + covered := false + for _, existingInterval := range existingIntervals { + if existingInterval[0] <= requestedInterval[0] && existingInterval[1] >= requestedInterval[1] { + covered = true + break + } + } + if !covered { + return false + } + } + return true +} + +func portsOverlap(left, right string) bool { + if left == "" || right == "" { + return true + } + leftIntervals, leftErr := portIntervals(left) + rightIntervals, rightErr := portIntervals(right) + if leftErr != nil || rightErr != nil { + return false + } + for _, leftInterval := range leftIntervals { + for _, rightInterval := range rightIntervals { + if leftInterval[0] <= rightInterval[1] && rightInterval[0] <= leftInterval[1] { + return true + } + } + } + return false +} + +func portIntervals(value string) ([][2]int, error) { + parts := strings.Split(value, ",") + intervals := make([][2]int, 0, len(parts)) + for _, part := range parts { + start, end, err := portInterval(strings.TrimSpace(part)) + if err != nil { + return nil, err + } + intervals = append(intervals, [2]int{start, end}) + } + return intervals, nil +} + +func portInterval(value string) (int, int, error) { + parts := strings.Split(value, "-") + if len(parts) == 1 { + port, err := strconv.Atoi(parts[0]) + return port, port, err + } + if len(parts) != 2 { + return 0, 0, fmt.Errorf("invalid port interval %q", value) + } + start, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, err + } + end, err := strconv.Atoi(parts[1]) + return start, end, err +} diff --git a/agent/utils/firewall/forwarding/providers/iptables.go b/agent/utils/firewall/forwarding/providers/iptables.go index f6004278e616..662cbb0dfec9 100644 --- a/agent/utils/firewall/forwarding/providers/iptables.go +++ b/agent/utils/firewall/forwarding/providers/iptables.go @@ -1,10 +1,12 @@ package providers import ( + "context" "errors" "fmt" "os" "path/filepath" + "strconv" "strings" "time" @@ -14,6 +16,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" + "github.com/mattn/go-shellwords" ) type iptablesBackend interface { @@ -39,6 +42,9 @@ func (systemIptablesBackend) Run(table string, args ...string) error { } func (systemIptablesBackend) RunWithStd(table string, args ...string) (string, error) { + if len(args) == 1 && args[0] == "-S" { + return iptables_helper.ReadTable(context.Background(), table, false) + } return iptables_helper.RunWithStd(table, args...) } @@ -47,6 +53,9 @@ func (systemIptablesBackend) RunIPv6(table string, args ...string) error { } func (systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (string, error) { + if len(args) == 1 && args[0] == "-S" { + return iptables_helper.ReadTable(context.Background(), table, true) + } return iptables_helper.RunIPv6WithStd(table, args...) } @@ -114,7 +123,7 @@ func (l *iptablesNATAdapter) Name() string { } func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) { - stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers") + stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-S") if err != nil { return nil, fmt.Errorf("failed to list NAT rules: %w", err) } @@ -122,7 +131,7 @@ func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) { if !l.backend.IPv6Available() { return rules, nil } - stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers") + stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-S") if err != nil { return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err) } @@ -549,40 +558,70 @@ func (l *iptablesNATAdapter) Replay() error { func parseIptablesRules(stdout, family string) []forwarding.Rule { var rules []forwarding.Rule + num := 0 +lines: for _, line := range strings.Split(stdout, "\n") { - fields := strings.Fields(line) - if len(fields) < 11 { + fields, err := shellwords.Parse(line) + if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != forwarding.ChainPreRouting { continue } - rule := forwarding.Rule{ - Num: fields[0], - Family: family, - Protocol: loadProtocol(fields[4]), - Interface: fields[6], - } - for index := 10; index < len(fields); index++ { - field := fields[index] - if strings.HasPrefix(field, "dpt:") || strings.HasPrefix(field, "dpts:") { - rule.Port = loadSourcePort(field) - } - if strings.HasPrefix(field, "to:") { - rule.TargetIP, rule.TargetPort = parseIptablesTarget(strings.TrimPrefix(field, "to:")) + num++ + rule := forwarding.Rule{Num: strconv.Itoa(num), Family: family} + target := "" + for index := 2; index < len(fields); index++ { + var value *string + switch fields[index] { + case "!": + continue lines + case "-p", "--protocol": + value = &rule.Protocol + case "-i", "--in-interface": + value = &rule.Interface + case "--dport", "--destination-port": + value = &rule.Port + case "-j", "--jump": + value = &target + case "--to-destination": + if index+1 >= len(fields) { + continue lines + } + index++ + rule.TargetIP, rule.TargetPort = parseIptablesTarget(fields[index]) + case "--to-ports", "--to-port": + value = &rule.TargetPort + case "-m", "--match", "--comment": + if index+1 >= len(fields) { + continue lines + } + index++ } - if field == "redir" && index+2 < len(fields) && fields[index+1] == "ports" { - rule.TargetPort = fields[index+2] + if value != nil { + if index+1 >= len(fields) { + continue lines + } + index++ + *value = fields[index] } } if rule.Protocol == "" || rule.Port == "" || rule.TargetPort == "" { continue } - if rule.TargetIP == "" { + switch target { + case "REDIRECT": + rule.TargetIP = "127.0.0.1" if family == forwarding.FamilyIPv6 { rule.TargetIP = "::1" - } else { - rule.TargetIP = "127.0.0.1" } + case "DNAT": + if rule.TargetIP == "" { + continue + } + default: + continue } - rule.TargetPort = strings.TrimPrefix(rule.TargetPort, ":") + rule.Protocol = loadProtocol(rule.Protocol) + rule.Port = strings.ReplaceAll(rule.Port, ":", "-") + rule.TargetPort = strings.ReplaceAll(rule.TargetPort, ":", "-") rules = append(rules, rule) } return rules @@ -617,14 +656,3 @@ func loadProtocol(protocol string) string { return protocol } } - -func loadSourcePort(value string) string { - port := "" - if strings.Contains(value, "dpt:") { - port = strings.ReplaceAll(value, "dpt:", "") - } - if strings.Contains(value, "dpts:") { - port = strings.ReplaceAll(value, "dpts:", "") - } - return strings.ReplaceAll(port, ":", "-") -} diff --git a/agent/utils/firewall/forwarding/providers/nftables.go b/agent/utils/firewall/forwarding/providers/nftables.go index 46ddfde9c0e2..677178569b90 100644 --- a/agent/utils/firewall/forwarding/providers/nftables.go +++ b/agent/utils/firewall/forwarding/providers/nftables.go @@ -34,7 +34,10 @@ func (n *nftablesAdapter) Name() string { return "nftables" } func (n *nftablesAdapter) List() ([]forwarding.Rule, error) { rules := make([]forwarding.Rule, 0) for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - stdout, err := nftRun("-a", "list", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)) + stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)) + if errors.Is(err, nftables_helper.ErrChainNotFound) { + continue + } if err != nil { return nil, fmt.Errorf("failed to list nftables %s forwarding rules: %w", family, err) } diff --git a/agent/utils/firewall/iptables_helper/read.go b/agent/utils/firewall/iptables_helper/read.go new file mode 100644 index 000000000000..2614fbdda9e7 --- /dev/null +++ b/agent/utils/firewall/iptables_helper/read.go @@ -0,0 +1,29 @@ +package iptables_helper + +import ( + "context" + "fmt" + "strings" + + "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" +) + +func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) { + commands, err := lifecycle.ResolveIptablesCommands() + if err != nil { + return "", err + } + executable := commands.IPv4 + if ipv6 { + if !commands.IPv6Available() { + return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable) + } + executable = commands.IPv6 + } + output, err := runTables(ctx, executable, table, false, true, "-S") + if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) { + return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err) + } + return output, err +} diff --git a/agent/utils/firewall/nftables_helper/read.go b/agent/utils/firewall/nftables_helper/read.go new file mode 100644 index 000000000000..7dc64142b3aa --- /dev/null +++ b/agent/utils/firewall/nftables_helper/read.go @@ -0,0 +1,16 @@ +package nftables_helper + +import "errors" + +var ErrChainNotFound = errors.New("nftables chain is not initialized") + +func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) { + output, exists, err := readNftObject(run, "-a", "list", "chain", family, table, chain) + if err != nil { + return "", err + } + if !exists { + return "", ErrChainNotFound + } + return output, nil +} diff --git a/agent/utils/firewall/sync/diff.go b/agent/utils/firewall/sync/diff.go index 0d3a27b15ca3..cef0ca0cdefd 100644 --- a/agent/utils/firewall/sync/diff.go +++ b/agent/utils/firewall/sync/diff.go @@ -9,15 +9,6 @@ const ( StatusBlocked Status = "blocked" ) -type Outcome string - -const ( - OutcomeApplied Outcome = "applied" - OutcomeSkipped Outcome = "skipped" - OutcomeRemoved Outcome = "removed" - OutcomeFailed Outcome = "failed" -) - type ReasonCode string const ( diff --git a/agent/utils/firewall/sync/order.go b/agent/utils/firewall/sync/order.go index e45b700e0601..18158c727b89 100644 --- a/agent/utils/firewall/sync/order.go +++ b/agent/utils/firewall/sync/order.go @@ -7,106 +7,107 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" ) -func SupportsManagedOrder(provider filter.Provider) bool { - return provider == filter.ProviderIptables || provider == filter.ProviderNftables || provider == filter.ProviderUFW -} - -func ManagedOrderDrift(snapshot filter.Snapshot, desiredMarkers []string) map[string]struct{} { - if !SupportsManagedOrder(snapshot.Scope.Provider) || len(desiredMarkers) < 2 { - return nil - } - expected := make(map[string]struct{}, len(desiredMarkers)) - for _, marker := range desiredMarkers { - expected[marker] = struct{}{} - } - actual := make([]string, 0, len(desiredMarkers)) - present := make(map[string]struct{}, len(desiredMarkers)) - for _, observed := range snapshot.Rules { - if _, wanted := expected[observed.Marker]; wanted { - actual = append(actual, observed.Marker) - present[observed.Marker] = struct{}{} +func RuleOrder(snapshot filter.Snapshot, ordered []filter.InventoryItem) map[string]bool { + drifted := make(map[string]bool) + if snapshot.Scope.Provider == filter.ProviderFirewalld { + return drifted + } + markers := make([]string, 0, len(ordered)) + projected := append([]filter.ObservedRule(nil), snapshot.Rules...) + for _, item := range ordered { + if item.Desired == nil || item.Desired.Marker == "" { + continue + } + markers = append(markers, item.Desired.Marker) + if item.Observed != nil { + for index := range projected { + if filter.SameLocator(projected[index].Locator, item.Observed.Locator) { + projected[index].Marker = item.Desired.Marker + break + } + } } } - desired := make([]string, 0, len(actual)) - for _, marker := range desiredMarkers { - if _, exists := present[marker]; exists { - desired = append(desired, marker) + actual := make([]string, 0, len(markers)) + presentMarkers := make(map[string]bool, len(markers)) + for _, rule := range projected { + if slices.Contains(markers, rule.Marker) { + actual = append(actual, rule.Marker) + presentMarkers[rule.Marker] = true } } - if slices.Equal(actual, desired) { - return nil + present := make([]string, 0, len(actual)) + for _, marker := range markers { + if presentMarkers[marker] { + present = append(present, marker) + } } - drifted := make(map[string]struct{}, len(desired)) - for index := range desired { - if actual[index] != desired[index] { - drifted[actual[index]] = struct{}{} - drifted[desired[index]] = struct{}{} + for index, marker := range present { + if actual[index] != marker { + drifted[marker] = true + drifted[actual[index]] = true } } return drifted } -func InsertionPosition(snapshot filter.Snapshot, desiredMarkers []string, targetMarker string) (int64, bool) { - if !SupportsManagedOrder(snapshot.Scope.Provider) { - return 0, false - } - targetIndex := slices.Index(desiredMarkers, targetMarker) - if targetIndex < 0 { - return 0, false +func InsertionPosition(snapshot filter.Snapshot, markers []string, target string) *int64 { + if snapshot.Scope.Provider == filter.ProviderFirewalld { + return nil } + targetIndex := slices.Index(markers, target) for index := targetIndex - 1; index >= 0; index-- { - if _, position, exists := ObservedByMarker(snapshot, desiredMarkers[index]); exists { - return int64(position + 1), true + for _, rule := range snapshot.Rules { + if rule.Marker == markers[index] && rule.Locator.Position != nil { + position := int64(*rule.Locator.Position + 1) + return &position + } } } - for index := targetIndex + 1; index < len(desiredMarkers); index++ { - if _, position, exists := ObservedByMarker(snapshot, desiredMarkers[index]); exists { - return int64(position), true + if targetIndex >= 0 { + for _, marker := range markers[targetIndex+1:] { + for _, rule := range snapshot.Rules { + if rule.Marker == marker && rule.Locator.Position != nil { + position := int64(*rule.Locator.Position) + return &position + } + } } } - return 0, false + return nil } -func NextManagedOrderChange(snapshot filter.Snapshot, desiredMarkers []string) (string, int, bool, error) { - expected := make(map[string]struct{}, len(desiredMarkers)) - for _, marker := range desiredMarkers { - expected[marker] = struct{}{} - } - actual := make([]string, 0, len(desiredMarkers)) - positions := make([]int, 0, len(desiredMarkers)) - for index, observed := range snapshot.Rules { - if _, exists := expected[observed.Marker]; !exists { +func DeleteChange(snapshot filter.Snapshot, previous filter.ObservedRule, desired filter.DesiredRule) (filter.DesiredChange, error) { + beforeKey, err := filter.RuleKey(previous.Rule) + if err != nil { + return filter.DesiredChange{}, err + } + matches := make([]filter.ObservedRule, 0, 1) + for _, current := range snapshot.Rules { + if previous.Marker != "" { + if current.Marker != previous.Marker { + continue + } + } else if current.Marker != "" || !filter.SameLocator(current.Locator, previous.Locator) { continue } - actual = append(actual, observed.Marker) - position := index + 1 - if observed.Locator.Position != nil { - position = *observed.Locator.Position + key, err := filter.RuleKey(current.Rule) + if err == nil && key == beforeKey { + matches = append(matches, current) } - positions = append(positions, position) } - if len(actual) != len(desiredMarkers) { - return "", 0, false, filter.ErrRuleStale + if len(matches) != 1 { + return filter.DesiredChange{}, filter.ErrRuleStale } - for index := range desiredMarkers { - if actual[index] != desiredMarkers[index] { - return desiredMarkers[index], positions[index], false, nil - } + current := matches[0] + if err := filter.GuardMutation(current); err != nil { + return filter.DesiredChange{}, err } - return "", 0, true, nil -} - -func ObservedByMarker(snapshot filter.Snapshot, marker string) (filter.ObservedRule, int, bool) { - for index, observed := range snapshot.Rules { - if observed.Marker == marker { - position := index + 1 - if observed.Locator.Position != nil { - position = *observed.Locator.Position - } - return observed, position, true - } + before := ObservedRule(current) + if before.UUID == "" { + before.UUID = desired.Rule.UUID } - return filter.ObservedRule{}, 0, false + return filter.DesiredChange{Operation: filter.ChangeDelete, Before: &before, Locator: ¤t.Locator, UnmarkedAdopted: current.Marker == "" && desired.Origin == filter.RuleOriginAdopted}, nil } func ObservedRule(observed filter.ObservedRule) filter.FirewallRule { diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index 4abbd187bec5..f6502b68124b 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -18626,7 +18626,10 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18643,11 +18646,9 @@ const docTemplate = `{ ], "x-panel-log": { "BeforeFunctions": [], - "bodyKeys": [ - "mode" - ], - "formatEN": "batch update Docker port guard policies [mode]", - "formatZH": "批量更新 Docker 端口防护策略 [mode]", + "bodyKeys": [], + "formatEN": "batch update Docker port guard policies", + "formatZH": "批量更新 Docker 端口防护策略", "paramKeys": [] } } @@ -18670,7 +18671,10 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18885,7 +18889,10 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18996,6 +19003,7 @@ const docTemplate = `{ "consumes": [ "application/json" ], + "description": "Creation and import return a taskID immediately; validation and execution results are written to the task log.", "parameters": [ { "description": "request", @@ -19035,7 +19043,7 @@ const docTemplate = `{ "Timestamp": [] } ], - "summary": "Create unified firewall v2 rules", + "summary": "Queue firewall rule creation", "tags": [ "Firewall" ], @@ -19048,7 +19056,7 @@ const docTemplate = `{ } } }, - "/hosts/firewall/rules/check": { + "/hosts/firewall/rules/adopt": { "post": { "consumes": [ "application/json" @@ -19060,16 +19068,13 @@ const docTemplate = `{ "name": "request", "required": true, "schema": { - "$ref": "#/definitions/dto.FirewallRuleCheck" + "$ref": "#/definitions/dto.FirewallRuleAdopt" } } ], "responses": { "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dto.FirewallRuleCheckResponse" - } + "description": "OK" }, "400": { "description": "Bad Request", @@ -19086,10 +19091,17 @@ const docTemplate = `{ "Timestamp": [] } ], - "summary": "Check unified firewall v2 rules for duplicates and conflicts", + "summary": "Adopt an external firewall rule", "tags": [ "Firewall" - ] + ], + "x-panel-log": { + "bodyKeys": [], + "paramKeys": [], + "BeforeFunctions": [], + "formatZH": "纳管防火墙规则", + "formatEN": "adopt firewall rule" + } } }, "/hosts/firewall/rules/delete": { @@ -35653,19 +35665,26 @@ const docTemplate = `{ ], "type": "object" }, - "dto.DockerPortGuardPolicyBatch": { + "dto.DockerPortGuardPolicy": { "properties": { "description": { "maxLength": 256, "type": "string" }, - "endpoints": { - "items": { - "$ref": "#/definitions/dto.DockerPortGuardEndpointIdentity" - }, - "maxItems": 256, - "minItems": 1, - "type": "array" + "family": { + "enum": [ + "ipv4", + "ipv6" + ], + "type": "string" + }, + "hostIP": { + "maxLength": 45, + "type": "string" + }, + "hostPort": { + "minimum": 1, + "type": "integer" }, "mode": { "enum": [ @@ -35675,18 +35694,41 @@ const docTemplate = `{ ], "type": "string" }, + "protocol": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + }, "sources": { "items": { "type": "string" }, - "maxItems": 256, "type": "array" } }, "required": [ - "endpoints", + "family", + "hostIP", + "hostPort", "mode", - "sources" + "protocol" + ], + "type": "object" + }, + "dto.DockerPortGuardPolicyBatch": { + "properties": { + "policies": { + "items": { + "$ref": "#/definitions/dto.DockerPortGuardPolicy" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "policies" ], "type": "object" }, @@ -35696,7 +35738,6 @@ const docTemplate = `{ "items": { "type": "string" }, - "maxItems": 256, "minItems": 1, "type": "array" } @@ -36094,84 +36135,21 @@ const docTemplate = `{ ], "type": "object" }, - "dto.FirewallRuleCheck": { - "properties": { - "items": { - "items": { - "$ref": "#/definitions/dto.FirewallRuleCheckItem" - }, - "maxItems": 256, - "minItems": 1, - "type": "array" - } - }, + "dto.FirewallRuleAdopt": { + "type": "object", "required": [ - "items" - ], - "type": "object" - }, - "dto.FirewallRuleCheckItem": { - "properties": { - "rule": { - "$ref": "#/definitions/filter.FirewallRule" - }, - "uuid": { - "type": "string" - } - }, - "required": [ - "rule" + "scope", + "instanceKey" ], - "type": "object" - }, - "dto.FirewallRuleCheckResponse": { - "properties": { - "items": { - "items": { - "$ref": "#/definitions/dto.FirewallRuleCheckResult" - }, - "type": "array" - } - }, - "type": "object" - }, - "dto.FirewallRuleCheckResult": { "properties": { - "allowedActions": { - "items": { - "$ref": "#/definitions/filter.CheckAction" - }, - "type": "array" - }, - "candidates": { - "items": { - "$ref": "#/definitions/filter.ObservedRule" - }, - "type": "array" - }, - "checkFlag": { - "type": "string" - }, - "classification": { - "$ref": "#/definitions/filter.CheckClassification" - }, - "decision": { - "$ref": "#/definitions/filter.CheckDecision" - }, - "existingRuleUUID": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "requestedRule": { - "$ref": "#/definitions/filter.FirewallRule" + "scope": { + "$ref": "#/definitions/filter.Scope" }, - "requestedRuleKey": { - "type": "string" + "instanceKey": { + "type": "string", + "maxLength": 128 } - }, - "type": "object" + } }, "dto.FirewallRuleCreate": { "properties": { @@ -36179,7 +36157,6 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/dto.FirewallRuleCreateItem" }, - "maxItems": 256, "minItems": 1, "type": "array" } @@ -36208,15 +36185,6 @@ const docTemplate = `{ }, "dto.FirewallRuleCreateItem": { "properties": { - "action": { - "$ref": "#/definitions/filter.CheckAction" - }, - "adoptInstanceKey": { - "type": "string" - }, - "checkFlag": { - "type": "string" - }, "rule": { "$ref": "#/definitions/filter.FirewallRule" }, @@ -36247,11 +36215,17 @@ const docTemplate = `{ "failed": { "type": "integer" }, + "queued": { + "type": "boolean" + }, "skipped": { "type": "integer" }, "succeeded": { "type": "integer" + }, + "taskID": { + "type": "string" } }, "type": "object" @@ -36262,7 +36236,6 @@ const docTemplate = `{ "items": { "type": "string" }, - "maxItems": 256, "minItems": 1, "type": "array" } @@ -36529,10 +36502,14 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/dto.ForwardRuleOperation" }, - "type": "array" + "type": "array", + "minItems": 1 } }, - "type": "object" + "type": "object", + "required": [ + "rules" + ] }, "dto.ForwardRuleOperation": { "properties": { @@ -41198,59 +41175,6 @@ const docTemplate = `{ "ActionReject" ] }, - "filter.CheckAction": { - "enum": [ - "create", - "create_anyway", - "adopt", - "select_adopt", - "cancel" - ], - "type": "string", - "x-enum-varnames": [ - "CheckActionCreate", - "CheckActionCreateAnyway", - "CheckActionAdopt", - "CheckActionSelectAdopt", - "CheckActionCancel" - ] - }, - "filter.CheckClassification": { - "enum": [ - "none", - "exact_managed", - "exact_external", - "covered", - "conflict", - "unsupported", - "protected" - ], - "type": "string", - "x-enum-varnames": [ - "CheckClassificationNone", - "CheckClassificationExactManaged", - "CheckClassificationExactExternal", - "CheckClassificationCovered", - "CheckClassificationConflict", - "CheckClassificationUnsupported", - "CheckClassificationProtected" - ] - }, - "filter.CheckDecision": { - "enum": [ - "ready", - "confirmation_required", - "blocked", - "no_change" - ], - "type": "string", - "x-enum-varnames": [ - "CheckDecisionReady", - "CheckDecisionConfirmationRequired", - "CheckDecisionBlocked", - "CheckDecisionNoChange" - ] - }, "filter.DesiredRule": { "properties": { "marker": { diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index 2932fa574460..8ec6681ce51d 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -18622,7 +18622,10 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18639,11 +18642,9 @@ ], "x-panel-log": { "BeforeFunctions": [], - "bodyKeys": [ - "mode" - ], - "formatEN": "batch update Docker port guard policies [mode]", - "formatZH": "批量更新 Docker 端口防护策略 [mode]", + "bodyKeys": [], + "formatEN": "batch update Docker port guard policies", + "formatZH": "批量更新 Docker 端口防护策略", "paramKeys": [] } } @@ -18666,7 +18667,10 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18881,7 +18885,10 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18992,6 +18999,7 @@ "consumes": [ "application/json" ], + "description": "Creation and import return a taskID immediately; validation and execution results are written to the task log.", "parameters": [ { "description": "request", @@ -19031,7 +19039,7 @@ "Timestamp": [] } ], - "summary": "Create unified firewall v2 rules", + "summary": "Queue firewall rule creation", "tags": [ "Firewall" ], @@ -19044,7 +19052,7 @@ } } }, - "/hosts/firewall/rules/check": { + "/hosts/firewall/rules/adopt": { "post": { "consumes": [ "application/json" @@ -19056,16 +19064,13 @@ "name": "request", "required": true, "schema": { - "$ref": "#/definitions/dto.FirewallRuleCheck" + "$ref": "#/definitions/dto.FirewallRuleAdopt" } } ], "responses": { "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dto.FirewallRuleCheckResponse" - } + "description": "OK" }, "400": { "description": "Bad Request", @@ -19082,10 +19087,17 @@ "Timestamp": [] } ], - "summary": "Check unified firewall v2 rules for duplicates and conflicts", + "summary": "Adopt an external firewall rule", "tags": [ "Firewall" - ] + ], + "x-panel-log": { + "bodyKeys": [], + "paramKeys": [], + "BeforeFunctions": [], + "formatZH": "纳管防火墙规则", + "formatEN": "adopt firewall rule" + } } }, "/hosts/firewall/rules/delete": { @@ -35649,19 +35661,26 @@ ], "type": "object" }, - "dto.DockerPortGuardPolicyBatch": { + "dto.DockerPortGuardPolicy": { "properties": { "description": { "maxLength": 256, "type": "string" }, - "endpoints": { - "items": { - "$ref": "#/definitions/dto.DockerPortGuardEndpointIdentity" - }, - "maxItems": 256, - "minItems": 1, - "type": "array" + "family": { + "enum": [ + "ipv4", + "ipv6" + ], + "type": "string" + }, + "hostIP": { + "maxLength": 45, + "type": "string" + }, + "hostPort": { + "minimum": 1, + "type": "integer" }, "mode": { "enum": [ @@ -35671,18 +35690,41 @@ ], "type": "string" }, + "protocol": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + }, "sources": { "items": { "type": "string" }, - "maxItems": 256, "type": "array" } }, "required": [ - "endpoints", + "family", + "hostIP", + "hostPort", "mode", - "sources" + "protocol" + ], + "type": "object" + }, + "dto.DockerPortGuardPolicyBatch": { + "properties": { + "policies": { + "items": { + "$ref": "#/definitions/dto.DockerPortGuardPolicy" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "policies" ], "type": "object" }, @@ -35692,7 +35734,6 @@ "items": { "type": "string" }, - "maxItems": 256, "minItems": 1, "type": "array" } @@ -36090,84 +36131,21 @@ ], "type": "object" }, - "dto.FirewallRuleCheck": { - "properties": { - "items": { - "items": { - "$ref": "#/definitions/dto.FirewallRuleCheckItem" - }, - "maxItems": 256, - "minItems": 1, - "type": "array" - } - }, + "dto.FirewallRuleAdopt": { + "type": "object", "required": [ - "items" - ], - "type": "object" - }, - "dto.FirewallRuleCheckItem": { - "properties": { - "rule": { - "$ref": "#/definitions/filter.FirewallRule" - }, - "uuid": { - "type": "string" - } - }, - "required": [ - "rule" + "scope", + "instanceKey" ], - "type": "object" - }, - "dto.FirewallRuleCheckResponse": { - "properties": { - "items": { - "items": { - "$ref": "#/definitions/dto.FirewallRuleCheckResult" - }, - "type": "array" - } - }, - "type": "object" - }, - "dto.FirewallRuleCheckResult": { "properties": { - "allowedActions": { - "items": { - "$ref": "#/definitions/filter.CheckAction" - }, - "type": "array" - }, - "candidates": { - "items": { - "$ref": "#/definitions/filter.ObservedRule" - }, - "type": "array" - }, - "checkFlag": { - "type": "string" - }, - "classification": { - "$ref": "#/definitions/filter.CheckClassification" - }, - "decision": { - "$ref": "#/definitions/filter.CheckDecision" - }, - "existingRuleUUID": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "requestedRule": { - "$ref": "#/definitions/filter.FirewallRule" + "scope": { + "$ref": "#/definitions/filter.Scope" }, - "requestedRuleKey": { - "type": "string" + "instanceKey": { + "type": "string", + "maxLength": 128 } - }, - "type": "object" + } }, "dto.FirewallRuleCreate": { "properties": { @@ -36175,7 +36153,6 @@ "items": { "$ref": "#/definitions/dto.FirewallRuleCreateItem" }, - "maxItems": 256, "minItems": 1, "type": "array" } @@ -36204,15 +36181,6 @@ }, "dto.FirewallRuleCreateItem": { "properties": { - "action": { - "$ref": "#/definitions/filter.CheckAction" - }, - "adoptInstanceKey": { - "type": "string" - }, - "checkFlag": { - "type": "string" - }, "rule": { "$ref": "#/definitions/filter.FirewallRule" }, @@ -36243,11 +36211,17 @@ "failed": { "type": "integer" }, + "queued": { + "type": "boolean" + }, "skipped": { "type": "integer" }, "succeeded": { "type": "integer" + }, + "taskID": { + "type": "string" } }, "type": "object" @@ -36258,7 +36232,6 @@ "items": { "type": "string" }, - "maxItems": 256, "minItems": 1, "type": "array" } @@ -36525,10 +36498,14 @@ "items": { "$ref": "#/definitions/dto.ForwardRuleOperation" }, - "type": "array" + "type": "array", + "minItems": 1 } }, - "type": "object" + "type": "object", + "required": [ + "rules" + ] }, "dto.ForwardRuleOperation": { "properties": { @@ -41194,59 +41171,6 @@ "ActionReject" ] }, - "filter.CheckAction": { - "enum": [ - "create", - "create_anyway", - "adopt", - "select_adopt", - "cancel" - ], - "type": "string", - "x-enum-varnames": [ - "CheckActionCreate", - "CheckActionCreateAnyway", - "CheckActionAdopt", - "CheckActionSelectAdopt", - "CheckActionCancel" - ] - }, - "filter.CheckClassification": { - "enum": [ - "none", - "exact_managed", - "exact_external", - "covered", - "conflict", - "unsupported", - "protected" - ], - "type": "string", - "x-enum-varnames": [ - "CheckClassificationNone", - "CheckClassificationExactManaged", - "CheckClassificationExactExternal", - "CheckClassificationCovered", - "CheckClassificationConflict", - "CheckClassificationUnsupported", - "CheckClassificationProtected" - ] - }, - "filter.CheckDecision": { - "enum": [ - "ready", - "confirmation_required", - "blocked", - "no_change" - ], - "type": "string", - "x-enum-varnames": [ - "CheckDecisionReady", - "CheckDecisionConfirmationRequired", - "CheckDecisionBlocked", - "CheckDecisionNoChange" - ] - }, "filter.DesiredRule": { "properties": { "marker": { diff --git a/frontend/src/api/interface/firewall.ts b/frontend/src/api/interface/firewall.ts index 563df0ced52c..9ba4750357ab 100644 --- a/frontend/src/api/interface/firewall.ts +++ b/frontend/src/api/interface/firewall.ts @@ -209,24 +209,6 @@ export namespace Firewall { withDockerRestart?: boolean; } - export type CheckDecision = 'ready' | 'confirmation_required' | 'blocked' | 'no_change'; - export type CheckClassification = - 'none' | 'exact_managed' | 'exact_external' | 'conflict' | 'unsupported' | 'protected'; - export type CheckAction = 'create' | 'adopt' | 'select_adopt' | 'cancel'; - export type ApplicableCheckAction = Exclude; - - export interface RuleCheckResult { - decision: CheckDecision; - classification: CheckClassification; - reason: string; - requestedRule: Rule; - requestedRuleKey: string; - existingRuleUUID?: string; - candidates?: ObservedRule[]; - allowedActions?: CheckAction[]; - checkFlag: string; - } - export interface InventoryRequest extends ReqPage { scopes: Scope[]; all?: boolean; @@ -244,25 +226,13 @@ export namespace Firewall { permanent: boolean; } - export interface CheckItem { - uuid?: string; - rule: Rule; - adoptLocator?: Locator; - } - - export interface CheckRequest { - items: CheckItem[]; - } - - export interface CheckResponse { - items: RuleCheckResult[]; + export interface AdoptRequest { + scope: Scope; + instanceKey: string; } export interface CreateItem { rule: Rule; - checkFlag: string; - action: ApplicableCheckAction; - adoptInstanceKey?: string; sourceKind?: 'user' | 'panel' | 'security' | 'imported'; sourceID?: string; } @@ -272,6 +242,8 @@ export namespace Firewall { } export interface CreateResponse { + taskID?: string; + queued?: boolean; succeeded: number; failed: number; skipped: number; @@ -444,10 +416,7 @@ export namespace Firewall { description: string; } export interface DockerGuardPolicyBatch { - endpoints: DockerGuardEndpointIdentity[]; - mode: 'deny_sources' | 'allow_sources' | 'deny_all'; - sources: string[]; - description: string; + policies: DockerGuardPolicy[]; } export interface DockerGuardPolicyBatchDelete { uuids: string[]; diff --git a/frontend/src/api/modules/firewall.ts b/frontend/src/api/modules/firewall.ts index 02e12711c84b..4d36225a46ae 100644 --- a/frontend/src/api/modules/firewall.ts +++ b/frontend/src/api/modules/firewall.ts @@ -16,7 +16,9 @@ export const operateFire = (operation: string, withDockerRestart: boolean) => http.post('/hosts/firewall/operate', { operation, withDockerRestart }, TimeoutEnum.T_10M); export const operateForwardRule = (request: { rules: Firewall.RuleForward[]; forceDelete?: boolean }) => - http.post('/hosts/firewall/forward/operate', request, TimeoutEnum.T_40S); + http.postWithConfig('/hosts/firewall/forward/operate', request, { + skipErrorMessage: true, + }); export const enableForwarding = (taskID?: string) => http.post( @@ -44,8 +46,8 @@ export const loadFirewallNativeDetail = (request: Firewall.NativeDetailRequest) return http.post('/hosts/firewall/rules/native/detail', request, TimeoutEnum.T_40S); }; -export const checkFirewallRules = (request: Firewall.CheckRequest) => { - return http.post('/hosts/firewall/rules/check', request, TimeoutEnum.T_3M); +export const adoptFirewallRule = (request: Firewall.AdoptRequest) => { + return http.post('/hosts/firewall/rules/adopt', request, TimeoutEnum.T_3M); }; export const createFirewallRules = (request: Firewall.CreateRequest) => { @@ -89,13 +91,12 @@ export const operateDockerPortGuard = (operation: 'initialize' | 'bind' | 'unbin ); export const upsertDockerPortGuardPolicies = (request: Firewall.DockerGuardPolicyBatch) => - http.postWithConfig('/hosts/firewall/docker/policies/batch', request, { - timeout: TimeoutEnum.T_60S, + http.postWithConfig('/hosts/firewall/docker/policies/batch', request, { skipErrorMessage: true, }); export const deleteDockerPortGuardPolicies = (request: Firewall.DockerGuardPolicyBatchDelete) => - http.postWithConfig('/hosts/firewall/docker/policies/delete/batch', request, { + http.postWithConfig('/hosts/firewall/docker/policies/delete/batch', request, { timeout: TimeoutEnum.T_60S, skipErrorMessage: true, }); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index e4467918e632..115dfec06ef9 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -4097,20 +4097,12 @@ const message = { drifted: 'Abnormal', }, ruleTargetRequired: 'Enter at least one IP address or port', - batchRuleLimit: 'A maximum of {0} rules can be created at a time', resolution_adopt: 'Take over management', plan_duplicate_rules: 'Rules with identical conditions and actions cannot be adopted. Manually delete duplicate rules and retry.', adoptRuleConfirm: 'After takeover, 1Panel can maintain and delete this existing rule. Continue?', plan_exact_rule_conflict: 'A rule with identical matching conditions has an opposing allow or deny action.', allRulesAlreadyExist: 'All {0} checked rules already exist. There are no new rules to create.', - ruleCheckResult: 'Rule check results', - ruleCheckStatus_creatable: 'Creatable', - ruleCheckStatus_existing: 'Exists', - ruleCheckStatus_error: 'Error', - ruleCheckExistingHelper: 'An identical rule already exists and will be skipped.', - ruleCheckReadyHelper: 'The check passed. This rule can be created.', - ruleCheckBlockedHelper: 'Rules with errors cannot be submitted. Go back, edit them, and check again.', plan_managed_rule_drifted: 'The 1Panel management record differs from the live firewall rule. Check the system rule.', plan_opaque_rule_in_target_scope: @@ -4172,7 +4164,6 @@ const message = { managedOrderDiffers: 'The managed rule order differs from the database sequence.', managedOnlyInTarget: 'The managed rule exists only in the target firewall.', managedRuntimeCannotRemove: 'The managed runtime rule cannot be removed safely.', - managedOrderBlocked: 'Managed rules cannot be reordered across external, unrecognized, or protected rules.', missingFromTarget: 'The rule is missing from the target firewall.', targetDiffers: 'The target rule differs from the database policy.', alreadyExistsInTarget: 'The rule already exists in the target firewall.', @@ -4283,8 +4274,6 @@ const message = { forwardPortHelper: 'Supports port ranges, e.g. 8080-8089', forwardInboundInterface: 'Forward Inbound Network Interface', exportHelper: 'About to export {0} firewall rules. Continue?', - importSuccess: 'Successfully imported {0} rules', - importPartialSuccess: 'Import completed: {0} succeeded, {1} failed', basicStatus: 'The current firewall is unbound. Bind it first.', initMsg: 'About to initialize {0}, continue?', initDirectBackendConflictMsg: @@ -4317,8 +4306,7 @@ const message = { deleteWildcardRuleConfirm: 'This rule allows {0} to access {1}. Deleting it may affect access to multiple services. Continue?', deleteRiskRulesConfirm: 'Will delete {0} rules. {1} allow rules may affect service access. Continue?', - editRuleConfirm: - 'The following fields will change: {0}. The rule will be applied and verified immediately. Continue?', + editRuleConfirm: 'Are you sure you want to modify this rule?', }, runtime: { importEnv: 'Import Environment Variables', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 9453795586c3..8d385592b548 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -4143,7 +4143,6 @@ const message = { drifted: 'Desviada', }, ruleTargetRequired: 'Introduce al menos una dirección IP o un puerto', - batchRuleLimit: 'Se pueden crear como máximo {0} reglas a la vez', resolution_adopt: 'Asumir la gestión', plan_duplicate_rules: 'No se pueden gestionar reglas duplicadas con las mismas condiciones y acciones. Elimine manualmente los duplicados y vuelva a intentarlo.', @@ -4151,14 +4150,6 @@ const message = { plan_exact_rule_conflict: 'Ya existe una regla con las mismas condiciones, pero con una acción opuesta de permitir o denegar.', allRulesAlreadyExist: 'Las {0} reglas comprobadas ya existen. No hay reglas nuevas que crear.', - ruleCheckResult: 'Resultados de la comprobación de reglas', - ruleCheckStatus_creatable: 'Se puede crear', - ruleCheckStatus_existing: 'Ya existe', - ruleCheckStatus_error: 'Error', - ruleCheckExistingHelper: 'Ya existe una regla idéntica y se omitirá.', - ruleCheckReadyHelper: 'La comprobación se superó. Esta regla se puede crear.', - ruleCheckBlockedHelper: - 'Las reglas con errores no se pueden enviar. Vuelve, modifícalas y compruébalas de nuevo.', plan_managed_rule_drifted: 'La regla gestionada no coincide con el firewall activo. Resuelve primero la diferencia.', plan_opaque_rule_in_target_scope: @@ -4222,8 +4213,6 @@ const message = { managedOrderDiffers: 'El orden de las reglas gestionadas difiere del orden de la base de datos.', managedOnlyInTarget: 'La regla gestionada solo existe en el firewall de destino.', managedRuntimeCannotRemove: 'La regla activa gestionada no se puede eliminar de forma segura.', - managedOrderBlocked: - 'Las reglas gestionadas no se pueden reordenar por encima de reglas externas, no reconocidas o protegidas.', missingFromTarget: 'La regla no existe en el firewall de destino.', targetDiffers: 'La regla de destino difiere de la política de la base de datos.', alreadyExistsInTarget: 'La regla ya existe en el firewall de destino.', @@ -4337,8 +4326,6 @@ const message = { forwardPortHelper: 'Admite rangos de puertos, ej.: 8080-8089', forwardInboundInterface: 'Interfaz de Red de Entrada para Reenvío', exportHelper: 'A punto de exportar {0} reglas de firewall. ¿Continuar?', - importSuccess: 'Se importaron correctamente {0} reglas', - importPartialSuccess: 'Importación completada: {0} correctas, {1} fallidas', basicStatus: 'El firewall actual no está vinculado. Vincúlelo primero.', initMsg: 'A punto de inicializar {0}, ¿continuar?', initDirectBackendConflictMsg: @@ -4373,8 +4360,7 @@ const message = { 'Esta regla permite que {0} acceda a {1}. Eliminarla puede afectar al acceso a varios servicios. ¿Continuar?', deleteRiskRulesConfirm: 'Se eliminarán {0} reglas. {1} reglas de acceso pueden afectar al acceso a servicios. ¿Continuar?', - editRuleConfirm: - 'Se modificarán los siguientes campos: {0}. La regla se aplicará y verificará inmediatamente. ¿Continuar?', + editRuleConfirm: '¿Confirma que desea modificar esta regla?', }, runtime: { importEnv: 'Importar variables de entorno', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 9bb49414b3d7..2720e4f852fa 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -4056,20 +4056,12 @@ const message = { protectedHelper: 'قانون حیاتی سیستم که برای جلوگیری از اختلال در دسترسی قابل ویرایش یا حذف نیست.', stateShort: { managed: 'پنل', adopted: 'مدیریت', external: 'خارجی', protected: 'محافظت', drifted: 'ناهمگام' }, ruleTargetRequired: 'حداقل یک نشانی IP یا درگاه وارد کنید', - batchRuleLimit: 'در هر بار حداکثر می‌توان {0} قانون ایجاد کرد', resolution_adopt: 'واگذاری مدیریت', plan_duplicate_rules: 'قوانین تکراری با شرایط و اقدامات یکسان قابل پذیرش برای مدیریت نیستند. قوانین تکراری را به‌صورت دستی حذف کرده و دوباره تلاش کنید.', adoptRuleConfirm: 'پس از واگذاری، 1Panel می‌تواند این قانون موجود را نگهداری و حذف کند. ادامه می‌دهید؟', plan_exact_rule_conflict: 'قانونی با شرایط تطبیق یکسان، اما با اقدام مخالفِ اجازه یا رد وجود دارد.', allRulesAlreadyExist: 'هر {0} قانون بررسی‌شده از قبل وجود دارند. قانون جدیدی برای ایجاد وجود ندارد.', - ruleCheckResult: 'نتایج بررسی قوانین', - ruleCheckStatus_creatable: 'قابل ایجاد', - ruleCheckStatus_existing: 'موجود', - ruleCheckStatus_error: 'خطا', - ruleCheckExistingHelper: 'یک قانون یکسان از قبل وجود دارد و نادیده گرفته خواهد شد.', - ruleCheckReadyHelper: 'بررسی با موفقیت انجام شد. این قانون قابل ایجاد است.', - ruleCheckBlockedHelper: 'قوانین دارای خطا قابل ارسال نیستند. بازگردید، آن‌ها را ویرایش و دوباره بررسی کنید.', plan_managed_rule_drifted: 'قانون مدیریت‌شده با دیوارهٔ آتش فعال یکسان نیست. ابتدا ناهمگامی را رفع کنید.', plan_opaque_rule_in_target_scope: 'در محدودهٔ هدف قانونی وجود دارد که تحلیل امن آن ممکن نیست. عملیات متوقف شد.', plan_runtime_permanent_mismatch: 'پیکربندی فعال و دائمی دیوارهٔ آتش متفاوت است. ابتدا آن‌ها را همگام کنید.', @@ -4128,8 +4120,6 @@ const message = { managedOrderDiffers: 'ترتیب قوانین مدیریت‌شده با ترتیب پایگاه داده متفاوت است.', managedOnlyInTarget: 'قانون مدیریت‌شده فقط در فایروال مقصد وجود دارد.', managedRuntimeCannotRemove: 'قانون فعال مدیریت‌شده را نمی‌توان با ایمنی حذف کرد.', - managedOrderBlocked: - 'قوانین مدیریت‌شده را نمی‌توان از میان قوانین خارجی، ناشناخته یا محافظت‌شده جابه‌جا کرد.', missingFromTarget: 'این قانون در فایروال مقصد وجود ندارد.', targetDiffers: 'قانون مقصد با سیاست پایگاه داده متفاوت است.', alreadyExistsInTarget: 'این قانون از قبل در فایروال مقصد وجود دارد.', @@ -4241,8 +4231,6 @@ const message = { forwardPortHelper: 'پشتیبانی از محدوده پورت، مثلاً 8080-8089', forwardInboundInterface: 'رابط شبکه ورودی انتقال', exportHelper: 'در حال خروجی {0} قوانین دیواره آتش. ادامه می‌دهید؟', - importSuccess: 'واردات {0} قانون با موفقیت انجام شد', - importPartialSuccess: 'واردات انجام شد: {0} موفق، {1} ناموفق', basicStatus: 'دیواره آتش فعلی متصل نیست. ابتدا آن را متصل کنید!', initMsg: 'در حال راه‌اندازی {0}، ادامه می‌دهید؟', initDirectBackendConflictMsg: @@ -4276,7 +4264,7 @@ const message = { 'این قانون به {0} اجازه دسترسی به {1} را می‌دهد. حذف آن ممکن است دسترسی به چندین سرویس را تحت تأثیر قرار دهد. ادامه می‌دهید؟', deleteRiskRulesConfirm: '{0} قانون حذف خواهند شد. {1} قانون مجاز ممکن است دسترسی به سرویس‌ها را تحت تأثیر قرار دهد. ادامه می‌دهید؟', - editRuleConfirm: 'فیلدهای زیر تغییر می‌کنند: {0}. قانون بلافاصله اعمال و بررسی می‌شود. ادامه می‌دهید؟', + editRuleConfirm: 'آیا از تغییر این قانون مطمئن هستید؟', }, runtime: { importEnv: 'وارد کردن متغیرهای محیطی', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 9de9dd2315d2..bb7b8675b4cf 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -4081,20 +4081,12 @@ const message = { protectedHelper: 'アクセスへの影響を防ぐため、編集や削除ができない重要なシステムルールです。', stateShort: { managed: 'パネル', adopted: '管理済', external: '外部', protected: '保護', drifted: '差異' }, ruleTargetRequired: 'IP アドレスまたはポートを1つ以上入力してください', - batchRuleLimit: '一度に作成できるルールは最大 {0} 件です', resolution_adopt: '管理対象にする', plan_duplicate_rules: '条件とアクションが同じルールが重複しているため、管理対象に追加できません。重複ルールを手動で削除してから再試行してください。', adoptRuleConfirm: '管理対象にすると、1Panel がこの既存ルールの保守と削除を行えるようになります。続行しますか?', plan_exact_rule_conflict: '一致条件が同じで、許可・拒否の動作が逆のルールが存在します。', allRulesAlreadyExist: '確認した {0} 件のルールはすべて既に存在します。新しく作成するルールはありません。', - ruleCheckResult: 'ルール確認結果', - ruleCheckStatus_creatable: '作成可能', - ruleCheckStatus_existing: '既存', - ruleCheckStatus_error: 'エラー', - ruleCheckExistingHelper: '同じルールが既に存在するため、今回はスキップします。', - ruleCheckReadyHelper: '確認に合格しました。このルールは作成できます。', - ruleCheckBlockedHelper: 'エラーのあるルールは送信できません。戻って修正し、もう一度確認してください。', plan_managed_rule_drifted: '管理対象ルールが実際のファイアウォールと一致しません。先に不整合を解消してください。', plan_opaque_rule_in_target_scope: '対象範囲に安全に解析できないルールがあります。操作を停止しました。', @@ -4157,8 +4149,6 @@ const message = { managedOrderDiffers: '管理対象ルールの順序がデータベースの順序と異なります。', managedOnlyInTarget: '管理対象ルールは対象ファイアウォールにのみ存在します。', managedRuntimeCannotRemove: '管理対象の実行中ルールを安全に削除できません。', - managedOrderBlocked: - '外部、認識不能、または保護されたルールを越えて管理対象ルールを並べ替えることはできません。', missingFromTarget: '対象ファイアウォールにこのルールがありません。', targetDiffers: '対象ルールがデータベースポリシーと異なります。', alreadyExistsInTarget: '対象ファイアウォールにこのルールは既に存在します。', @@ -4268,8 +4258,6 @@ const message = { forwardPortHelper: 'ポート範囲をサポートします。例: 8080-8089', forwardInboundInterface: '転送入站ネットワークインターフェース', exportHelper: '{0} 件のファイアウォールルールをエクスポートします。続行しますか?', - importSuccess: '{0} 件のルールを正常にインポートしました', - importPartialSuccess: 'インポート完了: {0} 件成功、{1} 件失敗', basicStatus: '現在のファイアウォールはバインドされていません。先にバインドしてください。', initMsg: '{0} を初期化します。続行しますか?', initDirectBackendConflictMsg: @@ -4303,7 +4291,7 @@ const message = { 'このルールは {0} に {1} へのアクセスを許可しています。削除すると複数のサービスへのアクセスに影響する可能性があります。続行しますか?', deleteRiskRulesConfirm: '{0} 個のルールを削除します。そのうち {1} 個の許可ルールがサービスへのアクセスに影響する可能性があります。続行しますか?', - editRuleConfirm: '次の項目を変更します:{0}。ルールは直ちに適用され、再検証されます。続行しますか?', + editRuleConfirm: 'このルールを変更してもよろしいですか?', }, runtime: { importEnv: '環境変数をインポート', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 7666789213ae..49391e9f5802 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -4007,7 +4007,6 @@ const message = { protectedHelper: '접속 장애를 방지하기 위해 편집하거나 삭제할 수 없는 중요한 시스템 규칙입니다.', stateShort: { managed: '패널', adopted: '관리', external: '외부', protected: '보호', drifted: '불일치' }, ruleTargetRequired: 'IP 주소 또는 포트를 하나 이상 입력하세요', - batchRuleLimit: '한 번에 최대 {0}개의 규칙을 생성할 수 있습니다', resolution_adopt: '관리 대상으로 전환', plan_duplicate_rules: '조건과 동작이 동일한 중복 규칙은 관리 대상으로 추가할 수 없습니다. 중복 규칙을 수동으로 삭제한 후 다시 시도하세요.', @@ -4015,13 +4014,6 @@ const message = { '관리 대상으로 전환하면 1Panel이 이 기존 규칙을 유지하고 삭제할 수 있습니다. 계속하시겠습니까?', plan_exact_rule_conflict: '일치 조건이 같지만 허용 또는 거부 동작이 반대인 규칙이 이미 있습니다.', allRulesAlreadyExist: '확인한 규칙 {0}개가 모두 이미 존재합니다. 새로 생성할 규칙이 없습니다.', - ruleCheckResult: '규칙 검사 결과', - ruleCheckStatus_creatable: '생성 가능', - ruleCheckStatus_existing: '이미 존재함', - ruleCheckStatus_error: '오류', - ruleCheckExistingHelper: '동일한 규칙이 이미 존재하므로 이번 작업에서는 건너뜁니다.', - ruleCheckReadyHelper: '검사를 통과했습니다. 이 규칙을 생성할 수 있습니다.', - ruleCheckBlockedHelper: '오류가 있는 규칙은 제출할 수 없습니다. 돌아가서 수정한 후 다시 검사하세요.', plan_managed_rule_drifted: '관리 규칙이 실제 방화벽과 일치하지 않습니다. 먼저 불일치를 해결하세요.', plan_opaque_rule_in_target_scope: '대상 범위에 안전하게 분석할 수 없는 규칙이 있어 작업을 중지했습니다.', plan_runtime_permanent_mismatch: '실행 중 구성과 영구 방화벽 구성이 다릅니다. 먼저 동기화하세요.', @@ -4080,7 +4072,6 @@ const message = { managedOrderDiffers: '관리 규칙 순서가 데이터베이스 순서와 다릅니다.', managedOnlyInTarget: '관리 규칙이 대상 방화벽에만 존재합니다.', managedRuntimeCannotRemove: '관리 중인 런타임 규칙을 안전하게 삭제할 수 없습니다.', - managedOrderBlocked: '외부, 인식할 수 없거나 보호된 규칙을 넘어 관리 규칙의 순서를 변경할 수 없습니다.', missingFromTarget: '대상 방화벽에 이 규칙이 없습니다.', targetDiffers: '대상 규칙이 데이터베이스 정책과 다릅니다.', alreadyExistsInTarget: '대상 방화벽에 이 규칙이 이미 존재합니다.', @@ -4189,8 +4180,6 @@ const message = { forwardPortHelper: '포트 범위를 지원합니다, 예: 8080-8089', forwardInboundInterface: '포워딩 인바운드 네트워크 인터페이스', exportHelper: '{0}개의 방화벽 규칙을 내보내려고 합니다. 계속하시겠습니까?', - importSuccess: '{0}개의 규칙을 성공적으로 가져왔습니다', - importPartialSuccess: '가져오기 완료: 성공 {0}건, 실패 {1}건', basicStatus: '현재 방화벽이 바인딩되지 않았습니다. 먼저 바인딩하세요!', initMsg: '{0}을(를) 초기화하려고 합니다. 계속하시겠습니까?', initDirectBackendConflictMsg: @@ -4223,7 +4212,7 @@ const message = { '이 규칙은 {0}에서 {1}에 접근하도록 허용합니다. 삭제하면 여러 서비스에 대한 접근에 영향을 줄 수 있습니다. 계속하시겠습니까?', deleteRiskRulesConfirm: '{0}개의 규칙을 삭제합니다. 그중 {1}개의 허용 규칙이 서비스 접근에 영향을 줄 수 있습니다. 계속하시겠습니까?', - editRuleConfirm: '다음 필드가 변경됩니다: {0}. 규칙이 즉시 적용되고 다시 검증됩니다. 계속하시겠습니까?', + editRuleConfirm: '이 규칙을 수정하시겠습니까?', }, runtime: { importEnv: '환경 변수 가져오기', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index cb8bef95290a..56fcac2358d8 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -3978,20 +3978,12 @@ const message = { protectedHelper: 'ກົດສຳຄັນຂອງລະບົບທີ່ບໍ່ສາມາດແກ້ໄຂ ຫຼື ລຶບໄດ້ ເພື່ອຫຼີກລ້ຽງການຂັດຂວາງການເຂົ້າເຖິງ.', stateShort: { managed: 'ແຜງ', adopted: 'ຈັດການ', external: 'ພາຍນອກ', protected: 'ປົກປ້ອງ', drifted: 'ບໍ່ກົງ' }, ruleTargetRequired: 'ກະລຸນາປ້ອນ IP ຫຼື ພອດຢ່າງໜ້ອຍໜຶ່ງລາຍການ', - batchRuleLimit: 'ສາມາດສ້າງກົດໄດ້ສູງສຸດ {0} ລາຍການຕໍ່ຄັ້ງ', resolution_adopt: 'ຮັບເຂົ້າຈັດການ', plan_duplicate_rules: 'ບໍ່ສາມາດນຳກົດທີ່ມີເງື່ອນໄຂ ແລະ ການກະທຳຄືກັນເຂົ້າມາຈັດການໄດ້. ກະລຸນາລຶບກົດທີ່ຊ້ຳກັນດ້ວຍຕົນເອງ ແລ້ວລອງອີກຄັ້ງ.', adoptRuleConfirm: 'ຫຼັງຈາກຮັບເຂົ້າແລ້ວ 1Panel ຈະສາມາດຮັກສາແລະລຶບກົດນີ້ໄດ້. ສືບຕໍ່ບໍ?', plan_exact_rule_conflict: 'ມີກົດທີ່ມີເງື່ອນໄຂຄືກັນ ແຕ່ການອະນຸຍາດຫຼືປະຕິເສດກົງກັນຂ້າມ.', allRulesAlreadyExist: 'ກົດທັງໝົດ {0} ລາຍການທີ່ກວດສອບມີຢູ່ແລ້ວ. ບໍ່ມີກົດໃໝ່ທີ່ຕ້ອງສ້າງ.', - ruleCheckResult: 'ຜົນການກວດສອບກົດ', - ruleCheckStatus_creatable: 'ສາມາດສ້າງໄດ້', - ruleCheckStatus_existing: 'ມີຢູ່ແລ້ວ', - ruleCheckStatus_error: 'ຂໍ້ຜິດພາດ', - ruleCheckExistingHelper: 'ມີກົດທີ່ຄືກັນຢູ່ແລ້ວ ແລະ ຈະຖືກຂ້າມ.', - ruleCheckReadyHelper: 'ການກວດສອບຜ່ານ. ສາມາດສ້າງກົດນີ້ໄດ້.', - ruleCheckBlockedHelper: 'ບໍ່ສາມາດສົ່ງກົດທີ່ມີຂໍ້ຜິດພາດໄດ້. ກັບໄປແກ້ໄຂ ແລະກວດສອບໃໝ່.', plan_managed_rule_drifted: 'ກົດທີ່ຈັດການບໍ່ກົງກັບໄຟວໍທີ່ໃຊ້ງານ. ກະລຸນາແກ້ໄຂກ່ອນ.', plan_opaque_rule_in_target_scope: 'ມີກົດໃນຂອບເຂດທີ່ບໍ່ສາມາດວິເຄາະໄດ້ຢ່າງປອດໄພ. ການດຳເນີນງານຖືກຢຸດ.', plan_runtime_permanent_mismatch: 'ຄ່າໄຟວໍທີ່ໃຊ້ງານແລະຄ່າຖາວອນບໍ່ກົງກັນ. ກະລຸນາຊິງກ່ອນ.', @@ -4049,7 +4041,6 @@ const message = { managedOrderDiffers: 'ລຳດັບກົດທີ່ຈັດການແຕກຕ່າງຈາກລຳດັບໃນຖານຂໍ້ມູນ.', managedOnlyInTarget: 'ກົດທີ່ຈັດການມີຢູ່ສະເພາະໃນໄຟວໍເປົ້າໝາຍ.', managedRuntimeCannotRemove: 'ບໍ່ສາມາດລຶບກົດທີ່ກຳລັງໃຊ້ງານໄດ້ຢ່າງປອດໄພ.', - managedOrderBlocked: 'ບໍ່ສາມາດຈັດລຳດັບກົດທີ່ຈັດການຂ້າມກົດພາຍນອກ, ກົດທີ່ບໍ່ຮູ້ຈັກ ຫຼືກົດທີ່ຖືກປົກປ້ອງ.', missingFromTarget: 'ບໍ່ມີກົດນີ້ໃນໄຟວໍເປົ້າໝາຍ.', targetDiffers: 'ກົດເປົ້າໝາຍແຕກຕ່າງຈາກນະໂຍບາຍໃນຖານຂໍ້ມູນ.', alreadyExistsInTarget: 'ກົດນີ້ມີຢູ່ແລ້ວໃນໄຟວໍເປົ້າໝາຍ.', @@ -4154,8 +4145,6 @@ const message = { forwardPortHelper: 'ຮອງຮັບຊ່ວງພອດ ຕົວຢ່າງ: 8080-8089', forwardInboundInterface: 'ອິນເຕີເຟດເຄືອຂ່າຍຂາເຂົ້າທີ່ສົ່ງຕໍ່', exportHelper: 'ກຳລັງຈະສົ່ງອອກກົດລະບຽບໄຟວໍ {0} ລາຍການ. ຕ້ອງການຕໍ່ຫຼືບໍ່?', - importSuccess: 'ນຳເຂົ້າ {0} ກົດລະບຽບສຳເລັດແລ້ວ', - importPartialSuccess: 'ນຳເຂົ້າສຳເລັດ: ສຳເລັດ {0}, ລົ້ມເຫຼວ {1}', basicStatus: 'ໄຟວໍປັດຈຸບັນຍັງບໍ່ໄດ້ຜູກມັດ. ກະລຸນາຜູກມັດກ່ອນ!', initMsg: 'ກຳລັງຈະເລີ່ມຕົ້ນ {0}, ຕ້ອງການຕໍ່ຫຼືບໍ່?', initDirectBackendConflictMsg: @@ -4187,7 +4176,7 @@ const message = { deleteWildcardRuleConfirm: 'ກົດນີ້ອະນຸຍາດໃຫ້ {0} ເຂົ້າເຖິງ {1}. ການລຶບອາດສົ່ງຜົນຕໍ່ການເຂົ້າເຖິງຫຼາຍບໍລິການ. ສືບຕໍ່ບໍ?', deleteRiskRulesConfirm: 'ຈະລຶບ {0} ກົດລະບຽບ. {1} ກົດອະນຸຍາດອາດສົ່ງຜົນຕໍ່ການເຂົ້າເຖິງບໍລິການ. ສືບຕໍ່ບໍ?', - editRuleConfirm: 'ຈະປ່ຽນຟິວຕໍ່ໄປນີ້: {0}. ກົດຈະຖືກນຳໃຊ້ແລະກວດສອບທັນທີ. ສືບຕໍ່ບໍ?', + editRuleConfirm: 'ທ່ານແນ່ໃຈບໍວ່າຕ້ອງການແກ້ໄຂກົດນີ້?', }, runtime: { importEnv: 'ນຳເຂົ້າຕົວແປສະພາບແວດລ້ອມ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 6ee8bc1cfdf1..b663bf8bba1d 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -4163,7 +4163,6 @@ const message = { drifted: 'Berbeza', }, ruleTargetRequired: 'Masukkan sekurang-kurangnya satu alamat IP atau port', - batchRuleLimit: 'Maksimum {0} peraturan boleh dibuat pada satu masa', resolution_adopt: 'Ambil alih pengurusan', plan_duplicate_rules: 'Peraturan pendua dengan syarat dan tindakan yang sama tidak boleh diambil alih untuk diurus. Padam peraturan pendua secara manual dan cuba lagi.', @@ -4172,13 +4171,6 @@ const message = { plan_exact_rule_conflict: 'Peraturan dengan syarat padanan yang sama mempunyai tindakan benarkan atau sekat yang bertentangan.', allRulesAlreadyExist: 'Kesemua {0} peraturan yang diperiksa sudah wujud. Tiada peraturan baharu untuk dicipta.', - ruleCheckResult: 'Keputusan semakan peraturan', - ruleCheckStatus_creatable: 'Boleh dicipta', - ruleCheckStatus_existing: 'Sudah wujud', - ruleCheckStatus_error: 'Ralat', - ruleCheckExistingHelper: 'Peraturan yang sama sudah wujud dan akan dilangkau.', - ruleCheckReadyHelper: 'Semakan lulus. Peraturan ini boleh dicipta.', - ruleCheckBlockedHelper: 'Peraturan yang mempunyai ralat tidak boleh dihantar. Kembali, edit dan semak semula.', plan_managed_rule_drifted: 'Peraturan terurus tidak sepadan dengan tembok api aktif. Selesaikan perbezaan dahulu.', plan_opaque_rule_in_target_scope: @@ -4242,8 +4234,6 @@ const message = { managedOrderDiffers: 'Susunan peraturan terurus berbeza daripada turutan pangkalan data.', managedOnlyInTarget: 'Peraturan terurus hanya wujud dalam tembok api sasaran.', managedRuntimeCannotRemove: 'Peraturan aktif terurus tidak dapat dipadam dengan selamat.', - managedOrderBlocked: - 'Peraturan terurus tidak boleh disusun semula merentasi peraturan luaran, tidak dikenali atau dilindungi.', missingFromTarget: 'Peraturan tiada dalam tembok api sasaran.', targetDiffers: 'Peraturan sasaran berbeza daripada dasar pangkalan data.', alreadyExistsInTarget: 'Peraturan sudah wujud dalam tembok api sasaran.', @@ -4352,8 +4342,6 @@ const message = { forwardPortHelper: 'Menyokong julat port, cth: 8080-8089', forwardInboundInterface: 'Antara Muka Rangkaian Masukan Penerusan', exportHelper: 'Akan mengeksport {0} peraturan firewall. Teruskan?', - importSuccess: '{0} peraturan berjaya diimport', - importPartialSuccess: 'Import selesai: {0} berjaya, {1} gagal', basicStatus: 'Firewall semasa tidak terikat. Sila ikat dahulu.', initMsg: 'Akan memulakan {0}, teruskan?', initDirectBackendConflictMsg: @@ -4386,7 +4374,7 @@ const message = { 'Peraturan ini membenarkan {0} mengakses {1}. Memadamnya mungkin menjejaskan akses kepada beberapa perkhidmatan. Teruskan?', deleteRiskRulesConfirm: 'Akan memadam {0} peraturan. {1} peraturan benarkan mungkin menjejaskan akses perkhidmatan. Teruskan?', - editRuleConfirm: 'Medan berikut akan diubah: {0}. Peraturan akan digunakan dan disahkan serta-merta. Teruskan?', + editRuleConfirm: 'Adakah anda pasti mahu mengubah peraturan ini?', }, runtime: { importEnv: 'Import Pemboleh Ubah Persekitaran', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 1de409d91e9a..6b881d7a6ec1 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -4182,7 +4182,6 @@ const message = { drifted: 'Divergente', }, ruleTargetRequired: 'Informe pelo menos um endereço IP ou uma porta', - batchRuleLimit: 'É possível criar no máximo {0} regras por vez', resolution_adopt: 'Assumir gerenciamento', plan_duplicate_rules: 'Regras duplicadas com condições e ações idênticas não podem ser gerenciadas. Exclua manualmente as regras duplicadas e tente novamente.', @@ -4190,13 +4189,6 @@ const message = { plan_exact_rule_conflict: 'Já existe uma regra com as mesmas condições, mas com uma ação oposta de permitir ou negar.', allRulesAlreadyExist: 'Todas as {0} regras verificadas já existem. Não há novas regras para criar.', - ruleCheckResult: 'Resultados da verificação de regras', - ruleCheckStatus_creatable: 'Pode ser criada', - ruleCheckStatus_existing: 'Já existe', - ruleCheckStatus_error: 'Erro', - ruleCheckExistingHelper: 'Uma regra idêntica já existe e será ignorada.', - ruleCheckReadyHelper: 'A verificação foi aprovada. Esta regra pode ser criada.', - ruleCheckBlockedHelper: 'Regras com erros não podem ser enviadas. Volte, edite-as e verifique novamente.', plan_managed_rule_drifted: 'A regra gerenciada não corresponde ao firewall ativo. Resolva a divergência primeiro.', plan_opaque_rule_in_target_scope: @@ -4259,8 +4251,6 @@ const message = { managedOrderDiffers: 'A ordem das regras gerenciadas difere da sequência do banco de dados.', managedOnlyInTarget: 'A regra gerenciada existe apenas no firewall de destino.', managedRuntimeCannotRemove: 'A regra ativa gerenciada não pode ser removida com segurança.', - managedOrderBlocked: - 'As regras gerenciadas não podem ser reordenadas através de regras externas, não reconhecidas ou protegidas.', missingFromTarget: 'A regra não existe no firewall de destino.', targetDiffers: 'A regra de destino difere da política do banco de dados.', alreadyExistsInTarget: 'A regra já existe no firewall de destino.', @@ -4372,8 +4362,6 @@ const message = { forwardPortHelper: 'Suporta intervalos de portas, ex. 8080-8089', forwardInboundInterface: 'Interface de Rede de Entrada para Encaminhamento', exportHelper: 'Prestes a exportar {0} regras de firewall. Continuar?', - importSuccess: '{0} regras importadas com sucesso', - importPartialSuccess: 'Importação concluída: {0} sucesso, {1} falha', basicStatus: 'O firewall atual não está vinculado. Vincule-o primeiro.', initMsg: 'Prestes a inicializar {0}, continuar?', initDirectBackendConflictMsg: @@ -4408,8 +4396,7 @@ const message = { 'Esta regra permite que {0} acesse {1}. Excluí-la pode afetar o acesso a vários serviços. Continuar?', deleteRiskRulesConfirm: 'Excluirá {0} regras. {1} regras de liberação podem afetar o acesso aos serviços. Continuar?', - editRuleConfirm: - 'Os seguintes campos serão alterados: {0}. A regra será aplicada e verificada imediatamente. Continuar?', + editRuleConfirm: 'Tem certeza de que deseja modificar esta regra?', }, runtime: { importEnv: 'Importar variáveis de ambiente', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 28c45205fbe1..10215dd4ed0b 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -4149,7 +4149,6 @@ const message = { drifted: 'Различие', }, ruleTargetRequired: 'Укажите хотя бы один IP-адрес или порт', - batchRuleLimit: 'За один раз можно создать не более {0} правил', resolution_adopt: 'Взять под управление', plan_duplicate_rules: 'Нельзя принять под управление правила с одинаковыми условиями и действиями. Удалите дубликаты вручную и повторите попытку.', @@ -4157,13 +4156,6 @@ const message = { plan_exact_rule_conflict: 'Уже существует правило с теми же условиями, но с противоположным действием разрешения или запрета.', allRulesAlreadyExist: 'Все проверенные правила ({0}) уже существуют. Новых правил для создания нет.', - ruleCheckResult: 'Результаты проверки правил', - ruleCheckStatus_creatable: 'Можно создать', - ruleCheckStatus_existing: 'Уже существует', - ruleCheckStatus_error: 'Ошибка', - ruleCheckExistingHelper: 'Такое же правило уже существует и будет пропущено.', - ruleCheckReadyHelper: 'Проверка пройдена. Это правило можно создать.', - ruleCheckBlockedHelper: 'Правила с ошибками нельзя отправить. Вернитесь, исправьте их и повторите проверку.', plan_managed_rule_drifted: 'Управляемое правило не совпадает с активным межсетевым экраном. Сначала устраните расхождение.', plan_opaque_rule_in_target_scope: @@ -4226,8 +4218,6 @@ const message = { managedOrderDiffers: 'Порядок управляемых правил отличается от порядка в базе данных.', managedOnlyInTarget: 'Управляемое правило существует только в целевом брандмауэре.', managedRuntimeCannotRemove: 'Активное управляемое правило нельзя безопасно удалить.', - managedOrderBlocked: - 'Управляемые правила нельзя перемещать через внешние, нераспознанные или защищённые правила.', missingFromTarget: 'Правило отсутствует в целевом брандмауэре.', targetDiffers: 'Целевое правило отличается от политики базы данных.', alreadyExistsInTarget: 'Правило уже существует в целевом брандмауэре.', @@ -4339,8 +4329,6 @@ const message = { forwardPortHelper: 'Поддерживает диапазоны портов, напр. 8080-8089', forwardInboundInterface: 'Сетевой интерфейс для пересылки входящего трафика', exportHelper: 'Собираюсь экспортировать {0} правил брандмауэра. Продолжить?', - importSuccess: 'Успешно импортировано {0} правил', - importPartialSuccess: 'Импорт завершён: {0} успешно, {1} с ошибкой', basicStatus: 'Текущий межсетевой экран не привязан. Сначала привяжите его.', initMsg: 'Собираюсь инициализировать {0}, продолжить?', initDirectBackendConflictMsg: @@ -4374,7 +4362,7 @@ const message = { 'Это правило разрешает {0} доступ к {1}. Его удаление может повлиять на доступ к нескольким сервисам. Продолжить?', deleteRiskRulesConfirm: 'Удалит {0} правил. {1} разрешающих правил могут повлиять на доступ к сервисам. Продолжить?', - editRuleConfirm: 'Будут изменены поля: {0}. Правило будет немедленно применено и проверено. Продолжить?', + editRuleConfirm: 'Вы уверены, что хотите изменить это правило?', }, runtime: { importEnv: 'Импорт переменных окружения', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 85900b85f32c..0d00d6d14860 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -4167,7 +4167,6 @@ const message = { drifted: 'Farklı', }, ruleTargetRequired: 'En az bir IP adresi veya port girin', - batchRuleLimit: 'Bir seferde en fazla {0} kural oluşturulabilir', resolution_adopt: 'Yönetimi devral', plan_duplicate_rules: 'Koşulları ve eylemleri aynı olan yinelenen kurallar yönetime alınamaz. Yinelenen kuralları elle silip yeniden deneyin.', @@ -4175,13 +4174,6 @@ const message = { plan_exact_rule_conflict: 'Aynı eşleşme koşullarına sahip, ancak izin verme veya reddetme eylemi zıt olan bir kural zaten var.', allRulesAlreadyExist: 'Kontrol edilen {0} kuralın tümü zaten mevcut. Oluşturulacak yeni kural yok.', - ruleCheckResult: 'Kural kontrol sonuçları', - ruleCheckStatus_creatable: 'Oluşturulabilir', - ruleCheckStatus_existing: 'Zaten mevcut', - ruleCheckStatus_error: 'Hata', - ruleCheckExistingHelper: 'Aynı kural zaten mevcut ve atlanacak.', - ruleCheckReadyHelper: 'Kontrol başarılı. Bu kural oluşturulabilir.', - ruleCheckBlockedHelper: 'Hatalı kurallar gönderilemez. Geri dönüp düzenleyin ve yeniden kontrol edin.', plan_managed_rule_drifted: 'Yönetilen kural etkin güvenlik duvarıyla eşleşmiyor. Önce farkı giderin.', plan_opaque_rule_in_target_scope: 'Hedef kapsamda güvenle ayrıştırılamayan bir kural var. İşlem durduruldu.', plan_runtime_permanent_mismatch: 'Etkin ve kalıcı güvenlik duvarı yapılandırmaları farklı. Önce eşitleyin.', @@ -4243,8 +4235,6 @@ const message = { managedOrderDiffers: 'Yönetilen kuralların sırası veritabanı sırasından farklı.', managedOnlyInTarget: 'Yönetilen kural yalnızca hedef güvenlik duvarında mevcut.', managedRuntimeCannotRemove: 'Yönetilen etkin kural güvenli bir şekilde kaldırılamıyor.', - managedOrderBlocked: - 'Yönetilen kurallar harici, tanınmayan veya korunan kuralların üzerinden yeniden sıralanamaz.', missingFromTarget: 'Kural hedef güvenlik duvarında bulunmuyor.', targetDiffers: 'Hedef kural veritabanı ilkesinden farklı.', alreadyExistsInTarget: 'Kural hedef güvenlik duvarında zaten mevcut.', @@ -4355,8 +4345,6 @@ const message = { forwardPortHelper: 'Port aralıklarını destekler, örn.: 8080-8089', forwardInboundInterface: 'İletme Gelen Ağ Arayüzü', exportHelper: '{0} güvenlik duvarı kuralını dışa aktarmak üzere. Devam etmek istiyor musunuz?', - importSuccess: '{0} kural başarıyla içe aktarıldı', - importPartialSuccess: 'İçe aktarma tamamlandı: {0} başarılı, {1} başarısız', basicStatus: 'Mevcut güvenlik duvarı bağlı değil. Önce bağlayın.', initMsg: '{0} başlatılmak üzere, devam etmek istiyor musunuz?', initDirectBackendConflictMsg: @@ -4390,7 +4378,7 @@ const message = { 'Bu kural {0} için {1} erişimine izin veriyor. Silinmesi birden fazla hizmete erişimi etkileyebilir. Devam edilsin mi?', deleteRiskRulesConfirm: '{0} kural silinecek. {1} izin kuralı hizmet erişimini etkileyebilir. Devam edilsin mi?', - editRuleConfirm: 'Şu alanlar değiştirilecek: {0}. Kural hemen uygulanıp doğrulanacak. Devam edilsin mi?', + editRuleConfirm: 'Bu kuralı değiştirmek istediğinizden emin misiniz?', }, runtime: { importEnv: 'Ortam Değişkenlerini İçe Aktar', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index a0239bc943cd..ba69696da6fa 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -3827,19 +3827,11 @@ const message = { drifted: '異常', }, ruleTargetRequired: '請至少填寫一個 IP 位址或連接埠', - batchRuleLimit: '單次最多可建立 {0} 條規則', resolution_adopt: '納管', plan_duplicate_rules: '存在比對條件和動作相同的重複規則,無法納管。請手動刪除重複規則後重試。', adoptRuleConfirm: '納入管理後,1Panel 將負責維護及刪除這條現有規則,是否繼續?', plan_exact_rule_conflict: '已存在比對條件相同、但允許與拒絕動作相反的規則。', allRulesAlreadyExist: '本次檢查的 {0} 條規則均已存在,沒有需要建立的新規則。', - ruleCheckResult: '規則檢查結果', - ruleCheckStatus_creatable: '可建立', - ruleCheckStatus_existing: '已存在', - ruleCheckStatus_error: '錯誤', - ruleCheckExistingHelper: '系統中已存在相同規則,本次將自動略過。', - ruleCheckReadyHelper: '檢查通過,可以建立此規則。', - ruleCheckBlockedHelper: '存在錯誤規則,無法提交。請返回修改後重新檢查。', plan_managed_rule_drifted: '1Panel 管理記錄與系統實際規則不一致,請檢查系統規則。', plan_opaque_rule_in_target_scope: '目標範圍內有無法安全解析的規則,本次操作已停止。', plan_runtime_permanent_mismatch: '防火牆執行設定與永久設定不一致,請先完成同步。', @@ -3898,7 +3890,6 @@ const message = { managedOrderDiffers: '受管規則順序與資料庫順序不一致。', managedOnlyInTarget: '此受管規則僅存在於目標防火牆中。', managedRuntimeCannotRemove: '無法安全刪除此受管執行中規則。', - managedOrderBlocked: '受管規則的排序不能跨越外部、無法識別或受保護的規則。', missingFromTarget: '目標防火牆中缺少此規則。', targetDiffers: '目標規則與資料庫策略不一致。', alreadyExistsInTarget: '目標防火牆中已存在此規則。', @@ -3995,8 +3986,6 @@ const message = { forwardPortHelper: '支援埠範圍,如:8080-8089', forwardInboundInterface: '轉發入站網路介面', exportHelper: '即將匯出 {0} 條防火牆規則,是否繼續?', - importSuccess: '成功匯入 {0} 條規則', - importPartialSuccess: '匯入完成:成功 {0} 條,失敗 {1} 條', basicStatus: '目前防火牆尚未綁定,請先綁定', initMsg: '即將初始化 {0}, 是否繼續?', initDirectBackendConflictMsg: @@ -4025,7 +4014,7 @@ const message = { deleteUsedRuleConfirm: '該規則涵蓋由 {0} 提供的監聽服務。刪除後可能導致相關服務無法存取,是否繼續?', deleteWildcardRuleConfirm: '該規則允許 {0} 存取 {1}。刪除後可能影響多個服務的存取,是否繼續?', deleteRiskRulesConfirm: '將刪除 {0} 條規則,其中 {1} 條放行規則可能影響服務存取,是否繼續?', - editRuleConfirm: '將修改以下欄位:{0}。提交後會立即套用並回讀驗證,是否繼續?', + editRuleConfirm: '是否確認修改此規則?', }, runtime: { importEnv: '匯入環境變數', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8e1dcc4c5e5a..62e984b8f415 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -3873,19 +3873,11 @@ const message = { drifted: '异常', }, ruleTargetRequired: '请至少填写一个 IP 地址或端口', - batchRuleLimit: '单次最多可创建 {0} 条规则', resolution_adopt: '纳管', plan_duplicate_rules: '存在匹配条件和动作相同的重复规则,无法纳管。请手动删除重复规则后重试。', adoptRuleConfirm: '纳入管理后,1Panel 将负责维护和删除这条现有规则,是否继续?', plan_exact_rule_conflict: '已存在匹配条件相同、但允许与拒绝动作相反的规则。', allRulesAlreadyExist: '本次检查的 {0} 条规则均已存在,没有需要创建的新规则。', - ruleCheckResult: '规则检查结果', - ruleCheckStatus_creatable: '可创建', - ruleCheckStatus_existing: '已存在', - ruleCheckStatus_error: '错误', - ruleCheckExistingHelper: '系统中已存在相同规则,本次将自动跳过。', - ruleCheckReadyHelper: '检查通过,可以创建该规则。', - ruleCheckBlockedHelper: '存在错误规则,不能提交。请返回修改后重新检查。', plan_managed_rule_drifted: '这条规则在 1Panel 中的记录与防火墙实际状态不一致,请先刷新规则;如仍异常,请重新同步。', plan_opaque_rule_in_target_scope: @@ -3946,8 +3938,6 @@ const message = { managedOrderDiffers: '防火墙中的规则顺序与 1Panel 保存的顺序不一致,本次将调整。', managedOnlyInTarget: '该规则仍存在于防火墙中,但 1Panel 已不再保存,本次将删除。', managedRuntimeCannotRemove: '该规则仅在当前运行中生效,1Panel 无法确认删除后的影响,因此本次不会删除。', - managedOrderBlocked: - '调整规则顺序时会跨过外部规则、无法识别的规则或系统保护规则,为避免改变现有访问,本次不会调整。', missingFromTarget: '防火墙中缺少该规则,本次将补充。', targetDiffers: '防火墙中的规则与 1Panel 保存的设置不一致,本次将修复。', alreadyExistsInTarget: '防火墙中已存在相同规则,无需重复添加。', @@ -4043,8 +4033,6 @@ const message = { forwardPortHelper: '支持端口范围,如:8080-8089', forwardInboundInterface: '转发入站网卡', exportHelper: '即将导出 {0} 条防火墙规则,是否继续?', - importSuccess: '成功导入 {0} 条规则', - importPartialSuccess: '导入完成:成功 {0} 条,失败 {1} 条', basicStatus: '当前防火墙未绑定,请先绑定!', initMsg: '即将初始化 {0}, 是否继续?', @@ -4074,7 +4062,7 @@ const message = { deleteUsedRuleConfirm: '该规则覆盖了由 {0} 提供的监听服务。删除后可能导致相关服务无法访问,是否继续?', deleteWildcardRuleConfirm: '该规则允许 {0} 访问 {1}。删除后可能影响多个服务的访问,是否继续?', deleteRiskRulesConfirm: '将删除 {0} 条规则,其中 {1} 条放行规则可能影响服务访问,是否继续?', - editRuleConfirm: '将修改以下字段:{0}。提交后会立即应用并回读验证,是否继续?', + editRuleConfirm: '是否确认修改该规则?', }, runtime: { importEnv: '导入环境变量', diff --git a/frontend/src/views/host/firewall/docker/detail/index.vue b/frontend/src/views/host/firewall/docker/detail/index.vue index b33da143f8e8..7df53fb65fc4 100644 --- a/frontend/src/views/host/firewall/docker/detail/index.vue +++ b/frontend/src/views/host/firewall/docker/detail/index.vue @@ -166,7 +166,9 @@ @@ -176,7 +178,9 @@ import { computed, nextTick, reactive, ref } from 'vue'; import { Firewall } from '@/api/interface/firewall'; import { deleteDockerPortGuardPolicies, upsertDockerPortGuardPolicies } from '@/api/modules/firewall'; import i18n from '@/lang'; -import { MsgSuccess, MsgWarning } from '@/utils/message'; +import { MsgError, MsgWarning } from '@/utils/message'; +import { getErrorMessage } from '@/utils/misc'; +import { isAxiosError } from 'axios'; import { ElMessageBox, type FormInstance, type FormRules } from 'element-plus'; import { dockerGuardEndpointManagementMessage, @@ -187,18 +191,19 @@ import { import { formatHostAddress, formatHostAddressList, splitTagValues } from '@/views/host/firewall/utils/validation'; const props = defineProps<{ base: Firewall.DockerGuardBase; containers: Firewall.DockerGuardContainer[] }>(); -const emit = defineEmits<{ search: [] }>(); +const emit = defineEmits<{ search: []; created: [taskID: string] }>(); const drawerVisible = ref(false); const policyVisible = ref(false); +const savingPolicy = ref(false); const activeContainerKey = ref(''); const selectedGroupKeys = ref([]); const policyEndpoints = ref([]); const familyFilter = ref<'all' | Firewall.DockerGuardEndpoint['family']>('all'); const formRef = ref(); const sourceAddressRefs = ref void }>>([]); -type PolicyMode = Firewall.DockerGuardPolicyBatch['mode']; -type PolicyForm = Omit & { mode: PolicyMode | '' }; +type PolicyMode = Firewall.DockerGuardPolicy['mode']; +type PolicyForm = Pick & { mode: PolicyMode | '' }; const form = reactive({ mode: 'deny_sources', sources: [''], @@ -340,30 +345,45 @@ const removeSourceAddress = (index: number) => { if (form.sources.length === 0) form.sources.push(''); }; const submitPolicy = async () => { + if (savingPolicy.value) return; const valid = await formRef.value?.validate().catch(() => false); if (!valid || !form.mode) return; - const sources = splitTagValues(form.sources); + const mode = form.mode; + const sources = mode === 'deny_all' ? [] : splitTagValues(form.sources); + savingPolicy.value = true; try { - await upsertDockerPortGuardPolicies({ - endpoints: policyEndpoints.value.map(({ family, hostIP, hostPort, protocol }) => ({ - family, - hostIP, - hostPort, - protocol, - })), - mode: form.mode, - sources: form.mode === 'deny_all' ? [] : sources, - description: form.description, - }); - } catch { - emit('search'); - return; + const result = ( + await upsertDockerPortGuardPolicies({ + policies: policyEndpoints.value.map(({ family, hostIP, hostPort, protocol }) => ({ + family, + hostIP, + hostPort, + protocol, + mode, + sources, + description: form.description, + })), + }) + ).data; + if (!result.taskID || !result.queued) { + MsgError(i18n.global.t('commons.msg.operationFailed')); + return; + } + policyVisible.value = false; + drawerVisible.value = false; + selectedGroupKeys.value = []; + emit('created', result.taskID); + } catch (error) { + MsgError( + (isAxiosError(error) && error.response?.data?.message) || + (error && getErrorMessage(error)) || + i18n.global.t('commons.res.commonError'), + ); + } finally { + savingPolicy.value = false; } - policyVisible.value = false; - selectedGroupKeys.value = []; - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); - emit('search'); }; + const remove = async (endpoints: Firewall.DockerGuardEndpoint[], batch: boolean) => { const uuids = policyUUIDs(endpoints); if (!uuids.length) return; @@ -379,16 +399,21 @@ const remove = async (endpoints: Firewall.DockerGuardEndpoint[], batch: boolean) return; } try { - for (let offset = 0; offset < uuids.length; offset += 256) { - await deleteDockerPortGuardPolicies({ uuids: uuids.slice(offset, offset + 256) }); + const result = (await deleteDockerPortGuardPolicies({ uuids })).data; + if (!result.taskID || !result.queued) { + MsgError(i18n.global.t('commons.msg.operationFailed')); + return; } - } catch { - emit('search'); - return; + selectedGroupKeys.value = []; + drawerVisible.value = false; + emit('created', result.taskID); + } catch (error) { + MsgError( + (isAxiosError(error) && error.response?.data?.message) || + (error && getErrorMessage(error)) || + i18n.global.t('commons.res.commonError'), + ); } - selectedGroupKeys.value = []; - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); - emit('search'); }; const portMappingLabel = (row: Firewall.DockerGuardPortGroup) => { const ports = row.endpoints.map((endpoint) => endpoint.containerPort).filter(Boolean) as number[]; diff --git a/frontend/src/views/host/firewall/docker/import/index.vue b/frontend/src/views/host/firewall/docker/import/index.vue index dffd6aeb4364..2ac17804e724 100644 --- a/frontend/src/views/host/firewall/docker/import/index.vue +++ b/frontend/src/views/host/firewall/docker/import/index.vue @@ -1,6 +1,7 @@ - - + + @@ -234,7 +240,9 @@ import { operateFirewallBackend, } from '@/api/modules/firewall'; import i18n from '@/lang'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; +import { getErrorMessage } from '@/utils/misc'; +import { isAxiosError } from 'axios'; import { ElMessageBox } from 'element-plus'; import { Lock } from '@element-plus/icons-vue'; import { downloadWithContent } from '@/utils/file'; @@ -255,6 +263,7 @@ const importRef = ref>(); const ruleSyncRef = ref>(); const cleanupConfirmRef = ref>(); const taskLogRef = ref>(); +const openRuleTask = (taskID: string) => taskLogRef.value?.openWithTaskID(taskID, true); const orphanDrawerVisible = ref(false); const searchName = ref(''); const selects = ref([]); @@ -518,13 +527,18 @@ const removePolicies = async (endpoints: Firewall.DockerGuardEndpoint[], batch: } loading.value = true; try { - for (let offset = 0; offset < uuids.length; offset += 256) { - await deleteDockerPortGuardPolicies({ uuids: uuids.slice(offset, offset + 256) }); + const result = (await deleteDockerPortGuardPolicies({ uuids })).data; + if (!result.taskID || !result.queued) { + MsgError(i18n.global.t('commons.msg.operationFailed')); + return; } - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); - await search(); - } catch { - await search(); + openRuleTask(result.taskID); + } catch (error) { + MsgError( + (isAxiosError(error) && error.response?.data?.message) || + (error && getErrorMessage(error)) || + i18n.global.t('commons.res.commonError'), + ); } finally { loading.value = false; } diff --git a/frontend/src/views/host/firewall/forward/import/index.vue b/frontend/src/views/host/firewall/forward/import/index.vue index 00f63a282ea0..9c058713f6b8 100644 --- a/frontend/src/views/host/firewall/forward/import/index.vue +++ b/frontend/src/views/host/firewall/forward/import/index.vue @@ -6,6 +6,7 @@
{{ $t('commons.msg.importHelper') }}
+
{{ $t('commons.button.cancel') }} - + {{ $t('commons.button.import') }} @@ -93,8 +94,10 @@ - - diff --git a/frontend/src/views/host/firewall/rule/operate/index.vue b/frontend/src/views/host/firewall/rule/operate/index.vue index 53b36c996cf4..ae6e838fa2fd 100644 --- a/frontend/src/views/host/firewall/rule/operate/index.vue +++ b/frontend/src/views/host/firewall/rule/operate/index.vue @@ -6,14 +6,7 @@ :auto-close="!loading" @close="handleClose" > - + @@ -102,107 +95,25 @@ -
-
- {{ $t('firewall.ruleCheckResult') }} -
-
-
-
-
-
- {{ group.label }} · {{ group.items.length }} - - {{ ruleCheckGroupDescription(group.status) }} - -
-
-
- -
-
-
-
- - {{ previewProtocol(previewRule(item)) }} - - · - {{ previewAddress(previewRule(item)) }} - - - {{ $t('commons.table.port') }} - {{ previewRule(item).destinationPort || '*' }} - -
-
-
- -