diff --git a/agent/app/api/v2/firewall.go b/agent/app/api/v2/firewall.go index b42eaba15..2d2e8a133 100644 --- a/agent/app/api/v2/firewall.go +++ b/agent/app/api/v2/firewall.go @@ -4,11 +4,9 @@ import ( "errors" "github.com/1Panel-dev/1Panel/agent/buserr" "net/http" - "strings" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/docker" @@ -90,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) { // @Security Timestamp // @Router /hosts/firewall/forward/base [post] func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) { - data, err := forwardingService.LoadBaseInfo() + data, err := forwardingService.LoadBaseInfo(c.Request.Context()) if err != nil { helper.InternalServer(c, err) return @@ -111,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) { if err := helper.CheckBindAndValidate(&request, c); err != nil { return } - total, items, err := forwardingService.SearchRules(request) + total, items, err := forwardingService.SearchRules(c.Request.Context(), request) if err != nil { helper.InternalServer(c, err) return @@ -262,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) { helper.SuccessWithData(c, info) } -// @Tags Firewall -// @Summary Adopt an external firewall rule -// @Accept json -// @Param request body dto.FirewallRuleAdopt true "request" -// @Success 200 -// @Failure 400 {object} dto.Response -// @Security ApiKeyAuth -// @Security Timestamp -// @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 - } - if err := firewallService.Adopt(c.Request.Context(), request); err != nil { - handleFirewallRuleError(c, err) - return - } - helper.Success(c) -} - // @Tags Firewall // @Summary Queue firewall rule creation // @Description Creation and import return a taskID immediately; validation and execution results are written to the task log. @@ -309,69 +285,9 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) { helper.SuccessWithData(c, result) } -// @Tags Firewall -// @Summary Preview firewall rule synchronization -// @Accept json -// @Param request body dto.FirewallRuleSyncRequest true "request" -// @Success 200 {object} dto.FirewallRuleSyncPreview -// @Failure 400 {object} dto.Response -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /hosts/firewall/rules/sync/preview [post] -func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) { - var request dto.FirewallRuleSyncRequest - if err := helper.CheckBindAndValidate(&request, c); err != nil { - return - } - result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request) - if err != nil { - handleFirewallRuleError(c, err) - return - } - helper.SuccessWithData(c, result) -} - -// @Tags Firewall -// @Summary Load the currently executing firewall rule synchronization task -// @Success 200 {object} dto.FirewallRuleSyncTask -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /hosts/firewall/rules/sync/task [get] -func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) { - result, err := firewallService.CurrentRuleSyncTask() - if err != nil { - helper.InternalServer(c, err) - return - } - helper.SuccessWithData(c, result) -} - -// @Tags Firewall -// @Summary Synchronize firewall rules to a target backend -// @Accept json -// @Param request body dto.FirewallRuleSyncRequest true "request" -// @Success 200 {object} dto.FirewallRuleSyncResult -// @Failure 400 {object} dto.Response -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /hosts/firewall/rules/sync [post] -// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"} -func (b *BaseApi) SyncFirewallRules(c *gin.Context) { - var request dto.FirewallRuleSyncRequest - if err := helper.CheckBindAndValidate(&request, c); err != nil { - return - } - result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request) - if err != nil { - handleFirewallRuleError(c, err) - return - } - helper.SuccessWithData(c, result) -} - // @Tags Firewall // @Summary Queue firewall rule deletion -// @Description Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log. +// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log. // @Accept json // @Param request body dto.FirewallRuleDelete true "request" // @Success 200 {object} dto.FirewallRuleDeleteResponse @@ -394,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) { } // @Tags Firewall -// @Summary Update a managed unified firewall v2 rule +// @Summary Update a firewall rule // @Accept json // @Param request body dto.FirewallRuleUpdate true "request" // @Success 200 @@ -402,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) { // @Security ApiKeyAuth // @Security Timestamp // @Router /hosts/firewall/rules/update [post] -// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"} +// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [instanceKey]","formatEN":"update firewall rule [instanceKey]"} func (b *BaseApi) UpdateFirewallRule(c *gin.Context) { var request dto.FirewallRuleUpdate if err := helper.CheckBindAndValidate(&request, c); err != nil { return } - if !normalizeFirewallRuleUUID(c, &request.UUID) { - return - } - if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil { + if err := firewallService.Update(c.Request.Context(), request); err != nil { handleFirewallRuleError(c, err) return } @@ -419,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) { } // @Tags Firewall -// @Summary Reorder a managed unified firewall v2 rule +// @Summary Reorder a firewall rule // @Accept json // @Param request body dto.FirewallRuleReorder true "request" // @Success 200 @@ -427,35 +340,19 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) { // @Security ApiKeyAuth // @Security Timestamp // @Router /hosts/firewall/rules/reorder [post] -// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"} +// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [instanceKey]","formatEN":"reorder firewall rule [instanceKey]"} func (b *BaseApi) ReorderFirewallRule(c *gin.Context) { var request dto.FirewallRuleReorder if err := helper.CheckBindAndValidate(&request, c); err != nil { return } - if !normalizeFirewallRuleUUID(c, &request.UUID) { - return - } - if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil { + if err := firewallService.Reorder(c.Request.Context(), request); err != nil { handleFirewallRuleError(c, err) return } helper.Success(c) } -func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool { - if value == nil { - helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid) - return false - } - *value = strings.TrimSpace(*value) - if *value == "" { - helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid) - return false - } - return true -} - func handleFirewallRuleError(c *gin.Context, err error) { var businessErr buserr.BusinessError isBusinessError := errors.As(err, &businessErr) @@ -464,21 +361,20 @@ func handleFirewallRuleError(c *gin.Context, err error) { helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err) case errors.Is(err, filter.ErrRuleStale): 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 isBusinessError && businessErr.Msg == "ErrFirewallRuleScopeChange": - helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", 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) - case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict), - errors.Is(err, repo.ErrFirewallPersistenceInvalid): + case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation): helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err) + case isBusinessError && businessErr.Msg == "ErrRecordExist": + c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()}) + c.Abort() + case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict": + c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()}) + c.Abort() case isBusinessError && businessErr.Msg == "ErrInvalidParams": c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()}) c.Abort() - case errors.Is(err, filter.ErrVerificationFailed): - helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err) default: helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err) } @@ -501,7 +397,7 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) { // @Tags Firewall // @Summary Create firewall port whitelist rules -// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed. +// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed. // @Accept json // @Param request body dto.FirewallPortWhitelistCreate true "request" // @Success 200 @@ -523,7 +419,7 @@ func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) { // @Tags Firewall // @Summary Update firewall port whitelist rules -// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed. +// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed. // @Accept json // @Param request body dto.FirewallPortWhitelistUpdate true "request" // @Success 200 @@ -545,7 +441,7 @@ func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) { // @Tags Firewall // @Summary Delete firewall port whitelist rules -// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed. +// @Description Removes whitelist configuration; existing firewall rules are not removed. // @Accept json // @Param request body dto.FirewallPortWhitelistDelete true "request" // @Success 200 @@ -622,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) { helper.SuccessWithData(c, data) } -// @Tags Firewall -// @Summary Sync Docker port guard rules -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /hosts/firewall/docker/sync [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"} -func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) { - if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil { - handleDockerPortGuardError(c, err) - return - } - helper.Success(c) -} - // @Tags Firewall // @Summary Operate Docker port guard // @Accept json @@ -737,3 +618,63 @@ func handleDockerPortGuardError(c *gin.Context, err error) { } helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err) } + +// @Tags Firewall +// @Summary List firewall rule backups +// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system) +// @Success 200 {object} dto.FirewallRuleBackups +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/rules/backups [get] +func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) { + result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system")) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, result) +} + +// @Tags Firewall +// @Summary Initialize, repair or bind one firewall address family +// @Accept json +// @Param request body dto.FirewallFamilyOperation true "request" +// @Success 200 {object} dto.FilterChainOperationResponse +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/family/operate [post] +// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"} +func (b *BaseApi) OperateFirewallFamily(c *gin.Context) { + var request dto.FirewallFamilyOperation + if err := helper.CheckBindAndValidate(&request, c); err != nil { + return + } + result, err := firewallSettingService.OperateFamily(request) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, result) +} + +// @Tags Firewall +// @Summary Update firewall IPv6 support +// @Accept json +// @Param request body dto.FirewallIPv6Operation true "request" +// @Success 200 {object} dto.FilterChainOperationResponse +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/settings/ipv6 [post] +// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"} +func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) { + var request dto.FirewallIPv6Operation + if err := helper.CheckBindAndValidate(&request, c); err != nil { + return + } + result, err := firewallSettingService.OperateIPv6(request) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, result) +} diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index 300e47fb8..33de0b41a 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -2,11 +2,13 @@ package dto import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall" + dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" - firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" ) type FirewallSubsystemStatus struct { + IPv6Enabled bool `json:"ipv6Enabled"` Name string `json:"name"` Backend string `json:"backend"` ConflictBackend string `json:"conflictBackend,omitempty"` @@ -18,7 +20,6 @@ type FirewallSubsystemStatus struct { PingStatus string `json:"pingStatus"` Message string `json:"message,omitempty"` Reason string `json:"reason,omitempty"` - SyncError string `json:"syncError,omitempty"` LifecycleTaskID string `json:"lifecycleTaskID,omitempty"` IPv4 FirewallBackendFamilyStatus `json:"ipv4"` IPv6 FirewallBackendFamilyStatus `json:"ipv6"` @@ -49,6 +50,7 @@ type FirewallBackendOption struct { } type FirewallBackendFamilyStatus struct { + Partial bool `json:"partial"` Available bool `json:"available"` Initialized bool `json:"initialized"` Bound bool `json:"bound"` @@ -64,6 +66,7 @@ type FirewallBackendGroup struct { } type FirewallSettings struct { + IPv6Enabled bool `json:"ipv6Enabled"` System FirewallBackendGroup `json:"system"` Forwarding FirewallBackendGroup `json:"forwarding"` Docker FirewallBackendGroup `json:"docker"` @@ -92,6 +95,17 @@ type FirewallBackendOperation struct { Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"` } +type FirewallIPv6Operation struct { + Status string `json:"status" validate:"required,oneof=Enable Disable"` +} + +type FirewallFamilyOperation struct { + Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"` + Backend string `json:"backend" validate:"required,oneof=iptables nftables"` + Family string `json:"family" validate:"required,oneof=ipv4 ipv6"` + Operation string `json:"operation" validate:"required,oneof=initialize repair bind"` +} + type FilterChainOperation struct { Name string `json:"name" validate:"required,eq=1PANEL_BASIC"` Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"` @@ -104,42 +118,55 @@ type FilterChainOperationResponse struct { } type FirewallInitializationTask struct { - TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` + BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"` + TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` } type FirewallSystemPort = firewall.SystemPort type FirewallRuleInventoryResponse struct { - IPv4Range filter.PositionRange `json:"ipv4Range"` - IPv6Range filter.PositionRange `json:"ipv6Range"` - Total int64 `json:"total"` - AllTotal int64 `json:"allTotal"` - ManagedTotal int64 `json:"managedTotal"` - Items []filter.InventoryItem `json:"items"` - Notices []filter.ScopeNotice `json:"notices,omitempty"` + IPv4Range filter.PositionRange `json:"ipv4Range"` + IPv6Range filter.PositionRange `json:"ipv6Range"` + Total int64 `json:"total"` + AllTotal int64 `json:"allTotal"` + Items []filter.InventoryItem `json:"items"` + Notices []filter.ScopeNotice `json:"notices,omitempty"` +} + +type FirewallRuleBackup struct { + Name string `json:"name"` + Provider filter.Provider `json:"provider"` + RuleCount int `json:"ruleCount"` + ModifiedAt int64 `json:"modifiedAt"` +} + +type FirewallRuleBackups struct { + Directory string `json:"directory"` + Files []FirewallRuleBackup `json:"files"` } type FirewallRuleResetResponse struct { - Removed int `json:"removed"` - Disabled bool `json:"disabled"` + BackupPath string `json:"backupPath"` + Removed int `json:"removed"` + Disabled bool `json:"disabled"` } type FirewallRuleReset struct { + Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"` + Backup *bool `json:"backup,omitempty" default:"true"` Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"` WithDockerRestart bool `json:"withDockerRestart"` } type FirewallRuleInventory struct { - Refresh bool `json:"refresh,omitempty"` PageInfo - Scope filter.Scope `json:"scope,omitempty"` - Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"` - All bool `json:"all,omitempty"` - Info string `json:"info"` - Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"` - Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"` - States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"` - ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"` + Scope filter.Scope `json:"scope,omitempty"` + Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"` + All bool `json:"all,omitempty"` + Info string `json:"info"` + Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"` + Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"` + ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"` } type FirewallNativeDetail struct { @@ -150,6 +177,7 @@ type FirewallNativeDetail struct { } type DockerPortGuardBase struct { + IPv6Enabled bool `json:"ipv6Enabled"` Name string `json:"name"` Version string `json:"version"` IsExist bool `json:"isExist"` @@ -162,6 +190,7 @@ type DockerPortGuardBase struct { } type DockerPortGuardFamilyStatus struct { + Partial bool `json:"partial"` State string `json:"state"` Reason string `json:"reason,omitempty"` Initialized bool `json:"initialized"` @@ -182,11 +211,8 @@ type DockerPortGuardEndpoint struct { Application string `json:"application,omitempty"` PolicyUUID string `json:"policyUUID,omitempty"` Mode string `json:"mode,omitempty"` - NativeAction string `json:"nativeAction,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` Sources []string `json:"sources"` Effective bool `json:"effective"` - Description string `json:"description,omitempty"` TrafficPath string `json:"trafficPath"` ManagementTarget string `json:"managementTarget"` ManagementReason string `json:"managementReason,omitempty"` @@ -223,6 +249,7 @@ type DockerPortGuardEndpointIdentity struct { type DockerPortGuardPolicyBatch struct { Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"` + Import bool `json:"import"` } type DockerPortGuardPolicyBatchDelete struct { @@ -231,31 +258,27 @@ type DockerPortGuardPolicyBatchDelete struct { 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"` + Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"` + Sources []string `json:"sources" validate:"dive,required,max=64"` } type DockerPortGuardOperation struct { - Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"` - TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` -} - -type FirewallRuleAdopt struct { - Scope filter.Scope `json:"scope" validate:"required"` - InstanceKey string `json:"instanceKey,omitempty" validate:"omitempty,max=128"` - Rule *filter.FirewallRule `json:"rule,omitempty"` - Marker string `json:"marker,omitempty" validate:"max=256"` + BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"` + Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"` + TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` } type FirewallRuleCreateItem struct { - Rule filter.FirewallRule `json:"rule" validate:"required"` - SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"` - SourceID string `json:"sourceID"` + Raw string `json:"raw,omitempty"` + ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"` + Rule filter.FirewallRule `json:"rule" validate:"required"` + SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"` } type FirewallRuleCreate struct { - Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"` + BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"` + Initialize bool `json:"initialize"` + Items []FirewallRuleCreateItem `json:"items" validate:"dive"` } type FirewallRuleCreateResponse struct { @@ -274,66 +297,13 @@ type FirewallRuleCreateFailure struct { Error string `json:"error,omitempty"` } -type FirewallRuleSyncRequest struct { - Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"` - SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"` - TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"` - ResetSource bool `json:"resetSource"` - TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` -} - -type FirewallRuleSyncItem struct { - SourceUUID string `json:"sourceUUID"` - Rule *filter.FirewallRule `json:"rule,omitempty"` - ForwardRule *ForwardRule `json:"forwardRule,omitempty"` - DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"` - Status firewallsync.Status `json:"status"` - ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"` - Reason string `json:"reason,omitempty"` -} - -type FirewallRuleSyncPreview struct { - Subsystem string `json:"subsystem"` - SourceProvider filter.Provider `json:"sourceProvider,omitempty"` - TargetProvider filter.Provider `json:"targetProvider"` - Total int `json:"total"` - Ready int `json:"ready"` - Existing int `json:"existing"` - Removed int `json:"removed"` - Blocked int `json:"blocked"` - Items []FirewallRuleSyncItem `json:"items"` -} - -type FirewallRuleSyncResult struct { - Subsystem string `json:"subsystem"` - SourceProvider filter.Provider `json:"sourceProvider,omitempty"` - TargetProvider filter.Provider `json:"targetProvider"` - Total int `json:"total"` - Succeeded int `json:"succeeded"` - Skipped int `json:"skipped"` - Removed int `json:"removed"` - Failed int `json:"failed"` - Errors []FirewallRuleSyncFailure `json:"errors,omitempty"` - TaskID string `json:"taskID,omitempty"` - Queued bool `json:"queued,omitempty"` -} - -type FirewallRuleSyncTask struct { - TaskID string `json:"taskID,omitempty"` - Executing bool `json:"executing"` -} - -type FirewallRuleSyncFailure struct { - SourceUUID string `json:"sourceUUID"` - Rule *filter.FirewallRule `json:"rule,omitempty"` - ForwardRule *ForwardRule `json:"forwardRule,omitempty"` - DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"` - Error string `json:"error"` -} - type FirewallRuleDelete struct { - UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"` - BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"` + Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"` +} + +type FirewallRuleDeleteItem struct { + FirewallRuleDeleteTarget + Observed filter.ObservedRule `json:"observed" validate:"required"` } type FirewallRuleDeleteTarget struct { @@ -350,13 +320,13 @@ type FirewallRuleDeleteResponse struct { } type FirewallRuleDeleteFailure struct { - Index int `json:"index"` - UUID string `json:"uuid"` - Error string `json:"error"` + Index int `json:"index"` + InstanceKey string `json:"instanceKey"` + Error string `json:"error"` } type FirewallRuleUpdate struct { - UUID string `json:"uuid" validate:"required,max=64"` + FirewallRuleDeleteTarget Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"` Description *string `json:"description,omitempty" validate:"excluded_with=Rule"` OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"` @@ -364,26 +334,21 @@ type FirewallRuleUpdate struct { } type FirewallRuleReorder struct { - UUID string `json:"uuid" validate:"required,max=64"` + FirewallRuleDeleteTarget 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++ - } - } +type FirewallRuleExportItem struct { + filter.FirewallRule + Raw string `json:"raw,omitempty"` + ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"` +} + +type FirewallSubsystemBackup struct { + Families []string `json:"families,omitempty"` + Subsystem string `json:"subsystem"` + Provider filter.Provider `json:"provider"` + Forwarding []forwarding.Rule `json:"forwarding"` + Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"` } diff --git a/agent/app/dto/forwarding.go b/agent/app/dto/forwarding.go index 03e65b19b..873c142b3 100644 --- a/agent/app/dto/forwarding.go +++ b/agent/app/dto/forwarding.go @@ -24,15 +24,11 @@ type ForwardRule struct { UsedStatus string `json:"usedStatus"` Description string `json:"description"` - - IsDesired bool `json:"isDesired"` - IsRuntime bool `json:"isRuntime"` - SyncStatus string `json:"syncStatus"` } type ForwardRuleOperate struct { - ForceDelete bool `json:"forceDelete"` - Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"` + Import bool `json:"import"` + 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 deleted file mode 100644 index eca6db098..000000000 --- a/agent/app/model/firewall.go +++ /dev/null @@ -1,51 +0,0 @@ -package model - -type DockerPortGuardPolicy struct { - BaseModel - - UUID string `gorm:"uniqueIndex" json:"uuid"` - ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"` - Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"` - HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"` - HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"` - Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"` - Mode string `json:"mode"` - Sources string `gorm:"type:text" json:"-"` - Description string `gorm:"type:text" json:"description"` - NativeAction string `gorm:"default:''" json:"-"` - NativeRules string `gorm:"type:text" json:"-"` - Sequence int64 `gorm:"default:0" json:"-"` -} - -type ForwardingRule struct { - BaseModel - - Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"` - Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"` - Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"` - TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"` - TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"` - Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"` -} - -type FirewallRule struct { - UUID string `gorm:"primaryKey" json:"uuid"` - Family string `json:"family"` - - Protocol string `json:"protocol"` - SourceAddress string `json:"sourceAddress"` - SourcePort string `json:"sourcePort"` - DestinationAddress string `json:"destinationAddress"` - DestinationPort string `json:"destinationPort"` - Interface string `json:"interface"` - ConnectionStates string `gorm:"type:text" json:"connectionStates"` - Action string `json:"action"` - Description string `gorm:"type:text" json:"description"` - CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"` - Priority *int `json:"priority,omitempty"` - Sequence *int64 `gorm:"index" json:"sequence,omitempty"` - - Origin string `json:"origin"` - Owner string `json:"owner"` - Revision uint `gorm:"default:1" json:"revision"` -} diff --git a/agent/app/repo/docker_port_guard.go b/agent/app/repo/docker_port_guard.go deleted file mode 100644 index 9b792b6a3..000000000 --- a/agent/app/repo/docker_port_guard.go +++ /dev/null @@ -1,50 +0,0 @@ -package repo - -import ( - "context" - - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/global" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type IDockerPortGuardRepo interface { - ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error) - DeleteBatch(context.Context, []string) error - UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error -} - -type DockerPortGuardRepo struct{} - -func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} } - -func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) { - var policies []model.DockerPortGuardPolicy - err := global.DB.WithContext(ctx). - Where("read_only = ?", false). - Order("family, host_ip, host_port, protocol"). - Find(&policies).Error - return policies, err -} - -func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error { - return global.DB.WithContext(ctx). - Where("read_only = ? AND uuid IN ?", false, uuids). - Delete(&model.DockerPortGuardPolicy{}).Error -} - -func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error { - return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - for i := range policies { - policies[i].ReadOnly = false - if err := tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}}, - DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}), - }).Create(&policies[i]).Error; err != nil { - return err - } - } - return nil - }) -} diff --git a/agent/app/repo/firewall_rule.go b/agent/app/repo/firewall_rule.go deleted file mode 100644 index 422fc5c3f..000000000 --- a/agent/app/repo/firewall_rule.go +++ /dev/null @@ -1,187 +0,0 @@ -package repo - -import ( - "context" - "errors" - "fmt" - - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/constant" - "github.com/1Panel-dev/1Panel/agent/global" - "github.com/google/uuid" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -var ( - ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict") - ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record") -) - -type IFirewallRuleRepo interface { - Create(context.Context, *model.FirewallRule) error - GetByUUID(context.Context, string) (model.FirewallRule, error) - List(context.Context, ...DBOption) ([]model.FirewallRule, error) - UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error - DeleteWithRevision(context.Context, string, uint) error - DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error - SaveResetOrder(context.Context, []model.FirewallRule) error -} - -type FirewallRuleRepo struct { - db *gorm.DB -} - -func NewIFirewallRuleRepo() IFirewallRuleRepo { - return &FirewallRuleRepo{} -} - -func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo { - return &FirewallRuleRepo{db: db} -} - -func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error { - if err := prepareFirewallRule(rule); err != nil { - return err - } - return r.dbFor(ctx).Create(rule).Error -} - -func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) { - var rule model.FirewallRule - err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error - return rule, err -} - -func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) { - var rules []model.FirewallRule - db := r.dbFor(ctx).Model(&model.FirewallRule{}) - for _, opt := range opts { - db = opt(db) - } - return rules, db.Find(&rules).Error -} - -func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error { - updates = sanitizeRuleUpdates(updates) - updates["revision"] = gorm.Expr("revision + 1") - result := r.dbFor(ctx).Model(&model.FirewallRule{}). - Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision). - Updates(updates) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrFirewallRuleRevisionConflict - } - return nil -} - -func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error { - result := r.dbFor(ctx). - Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision). - Delete(&model.FirewallRule{}) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrFirewallRuleRevisionConflict - } - return nil -} - -func (r *FirewallRuleRepo) DeleteBatchWithRevision(ctx context.Context, rules []model.FirewallRule) map[string]error { - failures := make(map[string]error) - for start := 0; start < len(rules); start += 500 { - batch := rules[start:min(start+500, len(rules))] - ids := make([][]interface{}, 0, len(batch)) - for _, rule := range batch { - ids = append(ids, []interface{}{rule.UUID, rule.Revision}) - failures[rule.UUID] = ErrFirewallRuleRevisionConflict - } - var deleted []model.FirewallRule - err := r.dbFor(ctx).Clauses(clause.Returning{Columns: []clause.Column{{Name: "uuid"}}}). - Where("(uuid, revision) IN ?", ids).Delete(&deleted).Error - if err != nil { - for _, rule := range batch { - failures[rule.UUID] = err - } - continue - } - for _, rule := range deleted { - delete(failures, rule.UUID) - } - } - return failures -} - -func (r *FirewallRuleRepo) SaveResetOrder(ctx context.Context, rules []model.FirewallRule) error { - if len(rules) == 0 { - return nil - } - return r.dbFor(ctx).Transaction(func(tx *gorm.DB) error { - for _, rule := range rules { - result := tx.Model(&model.FirewallRule{}). - Where("uuid = ? AND revision = ?", rule.UUID, rule.Revision). - Updates(map[string]interface{}{"sequence": rule.Sequence, "priority": rule.Priority, "revision": gorm.Expr("revision + 1")}) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrFirewallRuleRevisionConflict - } - } - return nil - }) -} - -func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB { - return firewallDB(ctx, r.db) -} - -func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB { - if ctx == nil { - ctx = context.Background() - } - if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil { - return tx.WithContext(ctx) - } - if fallback == nil { - fallback = global.DB - } - return fallback.WithContext(ctx) -} - -func prepareFirewallRule(rule *model.FirewallRule) error { - if rule == nil { - return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid) - } - if rule.Family == "" || rule.Protocol == "" || rule.Action == "" { - return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid) - } - if rule.UUID == "" { - rule.UUID = uuid.NewString() - } - if rule.Revision == 0 { - rule.Revision = 1 - } - if rule.Origin == "" { - rule.Origin = constant.FirewallRuleOriginCreated - } - if rule.Owner == "" { - rule.Owner = constant.FirewallRuleSourceUser - } - return nil -} - -func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} { - result := make(map[string]interface{}, len(updates)+1) - for key, value := range updates { - result[key] = value - } - delete(result, "id") - delete(result, "uuid") - delete(result, "revision") - delete(result, "created_at") - return result -} diff --git a/agent/app/repo/forwarding_rule.go b/agent/app/repo/forwarding_rule.go deleted file mode 100644 index 92ac0d90f..000000000 --- a/agent/app/repo/forwarding_rule.go +++ /dev/null @@ -1,38 +0,0 @@ -package repo - -import ( - "context" - - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/global" -) - -type IForwardingRuleRepo interface { - List(context.Context) ([]model.ForwardingRule, error) - CreateBatch(context.Context, []model.ForwardingRule) error - DeleteBatch(context.Context, []uint) error -} - -type ForwardingRuleRepo struct{} - -func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} } - -func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) { - var rules []model.ForwardingRule - err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error - return rules, err -} - -func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error { - if len(rules) == 0 { - return nil - } - return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error -} - -func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error { - if len(ids) == 0 { - return nil - } - return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error -} diff --git a/agent/app/repo/setting.go b/agent/app/repo/setting.go index 429d787be..812ac1d26 100644 --- a/agent/app/repo/setting.go +++ b/agent/app/repo/setting.go @@ -1,11 +1,13 @@ package repo import ( + "context" "errors" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/global" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type SettingRepo struct{} @@ -19,12 +21,16 @@ type ISettingRepo interface { WithByKey(key string) DBOption UpdateOrCreate(key, value string) error + UpdateValues(map[string]string) error GetDescription(opts ...DBOption) (model.CommonDescription, error) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) CreateDescription(data *model.CommonDescription) error + SaveDescriptions(context.Context, []model.CommonDescription) error UpdateDescription(id string, val map[string]interface{}) error DelDescription(id string) error + DeleteDescriptions(context.Context, string, []string, bool) (int64, error) + WithDescriptionIDs(ids []string) DBOption WithByDescriptionID(id string) DBOption } @@ -90,6 +96,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error { return global.DB.Model(&setting).UpdateColumn("value", value).Error } +func (s *SettingRepo) UpdateValues(values map[string]string) error { + return global.DB.Transaction(func(tx *gorm.DB) error { + for key, value := range values { + var setting model.Setting + err := tx.Where("key = ?", key).First(&setting).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil { + return err + } + } else if err != nil { + return err + } else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil { + return err + } + } + return nil + }) +} + func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) { var lists []model.CommonDescription db := global.DB.Model(&model.CommonDescription{}) @@ -111,14 +136,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription, func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error { return global.DB.Create(data).Error } + +func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error { + return global.DB.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{"description"}), + }).CreateInBatches(&descriptions, 100).Error +} + func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error { return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error } func (s *SettingRepo) DelDescription(id string) error { return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error } + +func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) { + var deleted int64 + for start := 0; start < len(ids); start += 500 { + query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))]) + if emptyOnly { + query = query.Where("description = ? AND is_pinned = ?", "", false) + } + result := query.Delete(&model.CommonDescription{}) + deleted += result.RowsAffected + if result.Error != nil { + return deleted, result.Error + } + } + return deleted, nil +} func (s *SettingRepo) WithByDescriptionID(id string) DBOption { return func(g *gorm.DB) *gorm.DB { return g.Where("id = ?", id) } } + +func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption { + return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) } +} diff --git a/agent/app/service/docker.go b/agent/app/service/docker.go index 3eb284bcc..7f6da9d1f 100644 --- a/agent/app/service/docker.go +++ b/agent/app/service/docker.go @@ -487,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error { if err := controller.Handle(req.Operation, service); err != nil { return err } + if req.Operation == "start" || req.Operation == "restart" { + return RestoreDockerPortGuard(context.Background()) + } return nil } diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index 661bb70ca..083e63712 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -38,9 +38,8 @@ var ( monitorRepo = repo.NewIMonitorRepo() vllmMonitorRepo = &repo.VLLMMonitorRepo{} - settingRepo = repo.NewISettingRepo() - forwardingRuleRepo = repo.NewIForwardingRuleRepo() - backupRepo = repo.NewIBackupRepo() + settingRepo = repo.NewISettingRepo() + backupRepo = repo.NewIBackupRepo() websiteRepo = repo.NewIWebsiteRepo() websiteDomainRepo = repo.NewIWebsiteDomainRepo() diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index e1eff330b..f781b9850 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -4,15 +4,15 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" "slices" - "sort" "strconv" "strings" "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" @@ -23,17 +23,13 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld" filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" + lifecycleproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers" "github.com/google/uuid" - "gorm.io/gorm" ) type FirewallService struct { - rules repo.IFirewallRuleRepo adapters map[filter.Provider]filter.Adapter - forwardingSync firewallDatabaseSyncAdapter - dockerSync firewallDatabaseSyncAdapter selectedProvider func(context.Context) (filter.Provider, error) requiredPorts func() ([]firewall.PortWhitelist, error) cleanupBackend func(string) error @@ -60,32 +56,19 @@ type IFirewallService interface { QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) OperateFilterChain(request dto.FilterChainOperation) error QueueFilterChainInitialization(request dto.FilterChainOperation) (dto.FilterChainOperationResponse, error) + ListRuleBackups(context.Context, string) (dto.FirewallRuleBackups, error) Reset(context.Context, dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) + ExportBackup(context.Context, filter.Provider) ([]dto.FirewallRuleExportItem, int, error) Inventory(context.Context, dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) LoadFirewallNativeDetail(context.Context, dto.FirewallNativeDetail) (string, error) - Adopt(context.Context, dto.FirewallRuleAdopt) error Create(context.Context, dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) Delete(context.Context, dto.FirewallRuleDelete) (dto.FirewallRuleDeleteResponse, error) - Update(context.Context, string, dto.FirewallRuleUpdate) error - Reorder(context.Context, string, dto.FirewallRuleReorder) error - PreviewRuleSync(context.Context, string, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) - SyncRules(context.Context, string, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) - CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) + Update(context.Context, dto.FirewallRuleUpdate) error + Reorder(context.Context, dto.FirewallRuleReorder) error } -type firewallRuleDeleteItem struct { - index int - stored model.FirewallRule - rules []filter.DesiredRule -} - -type preparedManagedUpdate struct { - Stored model.FirewallRule - Before filter.DesiredRule - After filter.FirewallRule - RuleSet filter.RuleSet - Observed filter.ObservedRule - Runtime filter.Adapter +func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error { + return s.syncPortWhitelist(ctx, nil) } func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error { @@ -102,7 +85,11 @@ func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uin } func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) { - status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"} + families, err := loadFirewallFamilies() + if err != nil { + return dto.FirewallSubsystemStatus{}, err + } + status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-", IPv6Enabled: len(families) > 1} status.LifecycleTaskID = currentFirewallLifecycleTaskID() selected, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey) if selected = strings.TrimSpace(selected); selected != "" { @@ -134,7 +121,7 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem status.Version, status.PingStatus = runtimeStatus.Version, firewall.LoadPingStatus() status.IsActive = runtimeStatus.IsActive if runtimeStatus.Name == constant.FirewallProviderIptables || runtimeStatus.Name == constant.FirewallProviderNftables { - overview, err := loadSystemFirewallOverview(runtimeStatus.Name, chainGroup) + overview, err := loadSystemFirewallOverview(runtimeStatus.Name, chainGroup, families) if err != nil { return status, err } @@ -159,13 +146,9 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp } return response, buserr.New("TaskIsExecuting") } - running, err := s.CurrentRuleSyncTask() - if err != nil { + if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { return response, err } - if running.Executing { - return response, buserr.New("TaskIsExecuting") - } loadClient := s.baseClient if loadClient == nil { loadClient = NewSelectedSystemFirewallClient @@ -192,9 +175,9 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp return response, err } taskItem.AddSubTaskWithOps(name, func(t *task.Task) error { - return s.runFirewallLifecycleTask(t, client, request) + return s.runFirewallLifecycleTask(t, client, request, true) }, nil, 0, 0) - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { + if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { closeUnstartedFirewallTask(taskItem) return response, err } @@ -213,7 +196,7 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp taskItem.Task.Status = constant.StatusFailed taskItem.Task.ErrorMsg = err.Error() taskItem.Task.EndAt = time.Now() - _ = repo.NewITaskRepo().Update(context.Background(), taskItem.Task) + _ = taskRepo.Update(context.Background(), taskItem.Task) } }() return dto.FirewallLifecycleOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil @@ -232,9 +215,7 @@ func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) e return nil } ctx := context.Background() - rulesErr := s.restoreStoredFirewallRules(ctx, filter.Provider(provider), nil) - whitelistErr := s.SyncPortWhitelist(ctx) - return errors.Join(rulesErr, whitelistErr) + return s.SyncPortWhitelist(ctx) } func (s *FirewallService) QueueFilterChainInitialization(request dto.FilterChainOperation) (dto.FilterChainOperationResponse, error) { @@ -259,18 +240,22 @@ func (s *FirewallService) QueueFilterChainInitialization(request dto.FilterChain } taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeChainsStep", provider), func(t *task.Task) error { t.Logf("backend=%s", provider) + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if !slices.Contains(families, constant.FirewallFamilyIPv6) { + t.Logf("IPv6 firewall support is disabled; skipping IPv6 host chain initialization") + } return s.operateFilterChainBase(provider, request) }, nil) taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error { - rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", provider), func() error { - return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider), t) - }) whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { return s.SyncPortWhitelist(t.TaskCtx) }) - return errors.Join(rulesErr, whitelistErr) + return whitelistErr }, nil) - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { + if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall initialization task: %w", err) } go func() { @@ -279,7 +264,69 @@ func (s *FirewallService) QueueFilterChainInitialization(request dto.FilterChain return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil } +func (s *FirewallService) ListRuleBackups(ctx context.Context, subsystem string) (dto.FirewallRuleBackups, error) { + if subsystem == "" { + subsystem = "system" + } + directory, err := firewallBackupDirectory(subsystem) + if err != nil { + return dto.FirewallRuleBackups{}, err + } + result := dto.FirewallRuleBackups{Directory: directory, Files: make([]dto.FirewallRuleBackup, 0)} + entries, err := os.ReadDir(directory) + if errors.Is(err, os.ErrNotExist) { + return result, nil + } + if err != nil { + return result, err + } + for _, entry := range entries { + if err := ctx.Err(); err != nil { + return result, err + } + if !entry.Type().IsRegular() || filepath.Ext(entry.Name()) != ".json" { + continue + } + var provider filter.Provider + count := 0 + if subsystem == "system" { + items, err := readFirewallBackup(entry.Name()) + if err != nil || len(items) == 0 { + continue + } + provider, count = items[0].Scope.Provider, len(items) + } else { + backup, err := readFirewallSubsystemBackup(entry.Name(), subsystem) + if err != nil { + continue + } + provider, count = backup.Provider, len(backup.Forwarding) + if backup.Docker != nil { + count = len(backup.Docker.Policies) + } + } + info, err := entry.Info() + if err != nil { + return result, err + } + result.Files = append(result.Files, dto.FirewallRuleBackup{Name: entry.Name(), Provider: provider, RuleCount: count, ModifiedAt: info.ModTime().UnixMilli()}) + } + slices.SortFunc(result.Files, func(a, b dto.FirewallRuleBackup) int { + if a.ModifiedAt > b.ModifiedAt { + return -1 + } + if a.ModifiedAt < b.ModifiedAt { + return 1 + } + return strings.Compare(b.Name, a.Name) + }) + return result, nil +} + func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) { + if request.Subsystem != "" && request.Subsystem != "system" { + return s.resetSubsystemRules(ctx, request) + } if err := lockFirewallLifecycleIdle(); err != nil { return dto.FirewallRuleResetResponse{}, err } @@ -295,15 +342,26 @@ func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleRes if provider == "" { provider = selected } - stored, err := s.rules.List(ctx) + var backup string + var count int + if request.Backup == nil || *request.Backup { + directory, directoryErr := firewallBackupDirectory("system") + if directoryErr != nil { + return dto.FirewallRuleResetResponse{}, directoryErr + } + var items []dto.FirewallRuleExportItem + items, count, err = s.ExportBackup(ctx, provider) + if err == nil { + backup, err = writeFirewallJSON(directory, string(provider)+"-"+time.Now().Format("20060102-150405")+"-"+uuid.NewString()+".json", items) + } + } else { + var inventory dto.FirewallRuleInventoryResponse + inventory, err = s.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider)) + count = len(inventory.Items) + } if err != nil { return dto.FirewallRuleResetResponse{}, err } - if selected == provider && len(stored) > 0 { - if err := s.saveFirewallResetOrder(ctx, provider, stored); err != nil { - return dto.FirewallRuleResetResponse{}, err - } - } if provider == filter.ProviderIptables || provider == filter.ProviderNftables { cleanup := s.cleanupBackend if (selected == filter.ProviderIptables || selected == filter.ProviderNftables) && selected != provider { @@ -317,7 +375,7 @@ func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleRes if err := cleanup(string(provider)); err != nil { return dto.FirewallRuleResetResponse{}, err } - return dto.FirewallRuleResetResponse{Removed: len(stored), Disabled: true}, nil + return dto.FirewallRuleResetResponse{Removed: count, Disabled: true, BackupPath: backup}, nil } if provider != filter.ProviderUFW && provider != filter.ProviderFirewalld { return dto.FirewallRuleResetResponse{}, fmt.Errorf("%w: unsupported firewall provider %s", filter.ErrProviderUnavailable, provider) @@ -352,7 +410,7 @@ func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleRes } restoreDockerGuard := s.restoreDockerGuard if restoreDockerGuard == nil { - restoreDockerGuard = ReconcileDockerPortGuard + restoreDockerGuard = RestoreDockerPortGuard } restoreErr := restoreFirewalldDependents( ctx, "after resetting firewalld", restartDocker, restoreForwarding, restoreDockerGuard, @@ -361,94 +419,111 @@ func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleRes return dto.FirewallRuleResetResponse{}, err } } - return dto.FirewallRuleResetResponse{Removed: len(stored), Disabled: true}, nil + return dto.FirewallRuleResetResponse{Removed: count, Disabled: true, BackupPath: backup}, nil +} + +func (s *FirewallService) ExportBackup(ctx context.Context, provider filter.Provider) ([]dto.FirewallRuleExportItem, int, error) { + if provider == "" { + var err error + provider, err = s.selectedProvider(ctx) + if err != nil { + return nil, 0, err + } + } + response, err := s.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider)) + if err != nil { + return nil, 0, err + } + descriptions, err := settingRepo.GetDescriptionList(repo.WithByType("firewall")) + if err != nil { + return nil, 0, err + } + byID := make(map[string]string, len(descriptions)) + for _, description := range descriptions { + byID[description.ID] = description.Description + } + items := make([]dto.FirewallRuleExportItem, 0, len(response.Items)) + for _, item := range response.Items { + if (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && item.Rule.Scope.Chain != filter.IptablesInputChain { + continue + } + id, err := filter.DescriptionID(*item.Observed) + if err != nil { + return nil, 0, err + } + rule := item.Rule + if description, ok := byID[id]; ok { + rule.Description = description + } + rule.UUID, rule.OrderIndex = "", nil + items = append(items, dto.FirewallRuleExportItem{FirewallRule: rule, Raw: item.Observed.Raw, ParseStatus: item.Observed.ParseStatus}) + } + return items, len(response.Items), nil } func (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) { - requestedScopes := request.Scopes - if len(requestedScopes) == 0 && request.Scope.Provider != "" { - requestedScopes = []filter.Scope{request.Scope} + scopes := request.Scopes + if len(scopes) == 0 && request.Scope.Provider != "" { + scopes = []filter.Scope{request.Scope} } - if len(requestedScopes) == 0 { + if len(scopes) == 0 { return dto.FirewallRuleInventoryResponse{}, filter.ErrInvalidScope } - scopes := make([]filter.Scope, len(requestedScopes)) - for index, requested := range requestedScopes { - scopes[index] = requested.Normalize() - } - if len(scopes) == 1 && scopes[0].Provider == filter.ProviderUFW && scopes[0].Family == filter.FamilyInet && scopes[0].Table == "" && - scopes[0].Zone == "" && scopes[0].Chain == filter.UFWInputChain && scopes[0].Direction == filter.DirectionInput { - scope := scopes[0] - if err := s.checkSelectedProvider(ctx, scope.Provider); err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - runtime, err := s.firewallAdapter(scope.Provider) - if err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - response, err := s.combinedUFWInventory(ctx, runtime, scope, request.Refresh) - if err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - return finalizeFirewallInventory(response, request), nil - } provider := scopes[0].Provider - for _, scope := range scopes { - if err := scope.ValidateMVP(); err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - if scope.Provider != provider { - return dto.FirewallRuleInventoryResponse{}, fmt.Errorf( - "%w: inventory scopes must use the same provider", filter.ErrInvalidScope, - ) - } - } if err := s.checkSelectedProvider(ctx, provider); err != nil { return dto.FirewallRuleInventoryResponse{}, err } - runtime, err := s.firewallAdapter(provider) - if err != nil { - return dto.FirewallRuleInventoryResponse{}, err + if len(scopes) == 1 && provider == filter.ProviderUFW && scopes[0].Family == filter.FamilyInet { + scopes = filter.ManagedInputScopes(provider) } - stored, err := s.rules.List(ctx) + response, err := s.readFirewallInventory(ctx, provider, scopes) if err != nil { - return dto.FirewallRuleInventoryResponse{}, err + return response, err } - desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, runtime) - response := dto.FirewallRuleInventoryResponse{Items: failures} - unavailable := make(map[filter.Family]error) - for _, group := range firewallScopeReadGroups(scopes) { - snapshots, err := readFirewallRuleScopes(runtime, ctx, group) - if errors.Is(err, filter.ErrFamilyUnavailable) { - for _, scope := range group { - if unavailable[scope.Family] == nil { - response.Notices = append(response.Notices, filter.ScopeNotice{Code: filter.ScopeNoticeFamilyUnavailable, Values: []string{string(scope.Family), err.Error()}}) - unavailable[scope.Family] = err - } - for _, desired := range desiredByScope[scope.Key()] { - response.Items = append(response.Items, filter.InventoryItem{ - Rule: desired.Rule, Desired: &desired, State: filter.InventoryStateDrifted, - Match: filter.InventoryMatchNone, Error: err.Error(), - }) - } - } - continue - } + searchDescription := strings.TrimSpace(request.Info) != "" + if !searchDescription { + response = finalizeFirewallInventory(response, request) + } + ids := make([]string, 0, len(response.Items)) + for index := range response.Items { + item := &response.Items[index] + item.DescriptionID, err = filter.DescriptionID(*item.Observed) if err != nil { - return dto.FirewallRuleInventoryResponse{}, err + return response, err } - for _, snapshot := range snapshots { - scope := snapshot.Scope - desired := desiredByScope[scope.Key()] - items, err := mergeFirewallInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) - if err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - response.Items = append(response.Items, items...) - response.Notices = append(response.Notices, snapshot.Notices...) + ids = append(ids, item.DescriptionID) + } + byID := make(map[string]string, len(ids)) + for start := 0; start < len(ids); start += 500 { + descriptions, err := settingRepo.GetDescriptionList(repo.WithByType("firewall"), settingRepo.WithDescriptionIDs(ids[start:min(start+500, len(ids))])) + if err != nil { + return response, err + } + for _, description := range descriptions { + byID[description.ID] = description.Description } } - return finalizeFirewallInventory(response, request), nil + for index := range response.Items { + item := &response.Items[index] + if description, ok := byID[item.DescriptionID]; ok { + item.Rule.Description = description + } + } + if searchDescription { + response = finalizeFirewallInventory(response, request) + } + ports, err := loadFirewallPortWhiteList() + if err != nil { + return response, err + } + whitelist := filter.NewPortWhitelistIndex(ports) + for index := range response.Items { + item := &response.Items[index] + item.IsWhitelist = item.Observed.ParseStatus == filter.ParseStatusSupported && whitelist.Matches(item.Rule) + item.Observed.Protected = item.Observed.Protected || item.IsWhitelist + } + + return response, nil } func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request dto.FirewallNativeDetail) (string, error) { @@ -480,48 +555,32 @@ func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request return reader.NativeDetail(ctx, request.Name, request.Permanent) } -func (s *FirewallService) Adopt(ctx context.Context, request dto.FirewallRuleAdopt) error { - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - if err := s.checkSelectedProvider(ctx, request.Scope.Provider); err != nil { - return err - } - runtime, err := s.firewallAdapter(request.Scope.Provider) - if err != nil { - return err - } - if runtime.Provider() != filter.ProviderNftables { - if request.Rule == nil { - return fmt.Errorf("%w: original rule is required for adoption", filter.ErrInvalidRule) - } - rule, err := filter.NormalizeRule(*request.Rule) - if err != nil { - return err - } - if rule.Scope.Key() != request.Scope.Key() { - return fmt.Errorf("%w: adoption rule scope mismatch", filter.ErrInvalidScope) - } - observed := filter.ObservedRule{Rule: rule, Marker: request.Marker, ParseStatus: filter.ParseStatusSupported} - return s.adoptRule(ctx, runtime, filter.RuleSet{Scope: rule.Scope}, observed, dto.FirewallRuleCreateItem{SourceKind: constant.FirewallRuleSourceUser}) - } - if request.InstanceKey == "" { - return fmt.Errorf("%w: instance key is required for nftables adoption", filter.ErrInvalidRule) - } - snapshot, err := readMutableFirewallRules(runtime, ctx, request.Scope) - if err != nil { - return err - } - observed, err := filter.FindCandidate(snapshot.Rules, request.InstanceKey) - if err != nil { - return filter.ErrRuleStale - } - return s.adoptRule(ctx, runtime, snapshot, observed, dto.FirewallRuleCreateItem{SourceKind: constant.FirewallRuleSourceUser}) -} - func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) { - if len(request.Items) > filter.MaxAtomicExpansion { - return dto.FirewallRuleCreateResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion) + if request.BackupFile != "" { + if len(request.Items) != 0 { + return dto.FirewallRuleCreateResponse{}, filter.ErrInvalidRule + } + items, err := readFirewallBackup(request.BackupFile) + if err != nil { + return dto.FirewallRuleCreateResponse{}, err + } + for _, item := range items { + request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported}) + } } + + for index, item := range request.Items { + if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" { + continue + } + observed := filterufw.ParseRules(item.Rule.Scope, item.Raw) + if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported { + observed[0].Rule.Description = item.Rule.Description + request.Items[index].Rule = observed[0].Rule + request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, "" + } + } + provider, err := s.selectedProvider(ctx) if err != nil { return dto.FirewallRuleCreateResponse{}, err @@ -533,13 +592,51 @@ func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCr if err != nil { return dto.FirewallRuleCreateResponse{}, err } + if request.Initialize { + taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeChainsStep", string(provider)), func(t *task.Task) error { + if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil { + return err + } + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if !slices.Contains(families, constant.FirewallFamilyIPv6) { + t.Logf("IPv6 firewall support is disabled; skipping IPv6 host chain initialization") + } + if provider == filter.ProviderIptables || provider == filter.ProviderNftables { + return s.operateFilterChainBase(string(provider), dto.FilterChainOperation{Name: "1PANEL_BASIC", Operate: string(firewall.BaseOperationInit)}) + } + client, err := s.baseClient() + if err != nil { + return err + } + return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, false) + }, nil) + } + ruleState := make(map[string]filter.RuleSet) taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error { firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - _, err := s.createRules(t.TaskCtx, request, t) - return err + if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil { + firewallRuleMutationMu.Unlock() + return err + } + _, err := s.createRules(t.TaskCtx, request, t, ruleState) + firewallRuleMutationMu.Unlock() + if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + if err != nil { + ruleState = nil + } + whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState) + if whitelistErr == nil && provider == filter.ProviderFirewalld { + whitelistErr = lifecycleproviders.RemoveFirewalldSSHService() + } + t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr) + return errors.Join(err, whitelistErr) }, nil, 0, 0) - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { + if err := taskRepo.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) @@ -556,17 +653,16 @@ func (s *FirewallService) Delete(ctx context.Context, request dto.FirewallRuleDe if err := ctx.Err(); err != nil { return dto.FirewallRuleDeleteResponse{}, err } - if len(request.UUIDs) == 0 && len(request.BeforeRules) == 0 { + if len(request.Targets) == 0 { return dto.FirewallRuleDeleteResponse{}, fmt.Errorf("%w: rules are required", filter.ErrInvalidRule) } - request.UUIDs = append([]string(nil), request.UUIDs...) - request.BeforeRules = append([]dto.FirewallRuleDeleteTarget(nil), request.BeforeRules...) + request.Targets = append([]dto.FirewallRuleDeleteItem(nil), request.Targets...) taskItem, err := task.NewTask(firewallTaskName(task.TaskDelete, firewallTaskHost, ""), task.TaskDelete, task.TaskScopeFirewall, "", 0) if err != nil { return dto.FirewallRuleDeleteResponse{}, err } taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { - t.Logf("rules=%d", len(request.UUIDs)+len(request.BeforeRules)) + t.Logf("rules=%d", len(request.Targets)) firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() if err := t.TaskCtx.Err(); err != nil { @@ -575,7 +671,7 @@ func (s *FirewallService) Delete(ctx context.Context, request dto.FirewallRuleDe _, err := s.deleteRules(t.TaskCtx, request, t) return err }, nil, 0, 0) - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { + if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { taskItem.LogFailedWithErr(taskItem.Name, err) closeUnstartedFirewallTask(taskItem) return dto.FirewallRuleDeleteResponse{}, fmt.Errorf("save firewall deletion task: %w", err) @@ -588,28 +684,16 @@ func (s *FirewallService) Delete(ctx context.Context, request dto.FirewallRuleDe return dto.FirewallRuleDeleteResponse{TaskID: taskItem.TaskID, Queued: true}, nil } -func (s *FirewallService) Update(ctx context.Context, clientIP string, request dto.FirewallRuleUpdate) error { +func (s *FirewallService) Update(ctx context.Context, request dto.FirewallRuleUpdate) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() - metadata := request.Description != nil || request.OrderIndex != nil || request.Priority != nil - if (request.Rule != nil) == metadata || request.OrderIndex != nil && request.Priority != nil { - return fmt.Errorf("%w: provide a rule or description/ordering fields", filter.ErrInvalidRule) - } - if request.Rule != nil { - rule := *request.Rule - rule.UUID = request.UUID - return s.updateRule(ctx, clientIP, request.UUID, rule) - } - if request.OrderIndex != nil || request.Priority != nil { - return s.updateRuleOrder(ctx, request.UUID, request.OrderIndex, request.Priority, request.Description) - } - return s.updateRuleDescription(ctx, request.UUID, *request.Description) + return s.updateObservedRule(ctx, request) } -func (s *FirewallService) Reorder(ctx context.Context, clientIP string, request dto.FirewallRuleReorder) error { +func (s *FirewallService) Reorder(ctx context.Context, request dto.FirewallRuleReorder) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() - return s.updateRuleOrder(ctx, request.UUID, request.TargetPosition, request.Priority, nil) + return s.updateObservedRule(ctx, dto.FirewallRuleUpdate{FirewallRuleDeleteTarget: request.FirewallRuleDeleteTarget, OrderIndex: request.TargetPosition, Priority: request.Priority}) } func NewIFirewallService() IFirewallService { @@ -667,7 +751,7 @@ func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation } return nil } - ReconcileDockerPortGuardBestEffort(context.Background()) + RestoreDockerPortGuardBestEffort(context.Background()) return nil } @@ -695,9 +779,6 @@ func (s *FirewallService) restoreFirewallAfterStart(client lifecycle.Client) err return nil } } - if err := s.restoreStoredFirewallRules(ctx, provider, nil); err != nil { - recordFailure("restore stored firewall rules", err) - } recordFailure("restore whitelist allowances", s.SyncPortWhitelist(ctx)) return errors.Join(recoveryErrors...) } @@ -709,7 +790,7 @@ func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, } restoreDockerGuard := s.restoreDockerGuard if restoreDockerGuard == nil { - restoreDockerGuard = ReconcileDockerPortGuard + restoreDockerGuard = RestoreDockerPortGuard } dockerActive := s.dockerActive if dockerActive == nil { @@ -726,18 +807,18 @@ func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, return restoreErr } -func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation) error { +func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, syncWhitelist bool) error { ctx := t.TaskCtx provider := filter.Provider(client.Name()) - operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, request.Operation, request.WithDockerRestart, func(lifecycle.Client) error { - rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", client.Name()), func() error { - return s.restoreStoredFirewallRules(ctx, provider, t) - }) - whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { - return s.SyncPortWhitelist(ctx) - }) - return errors.Join(rulesErr, whitelistErr) - }, t) + var prepareStart func(lifecycle.Client) error + if syncWhitelist { + prepareStart = func(lifecycle.Client) error { + return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { + return s.SyncPortWhitelist(ctx) + }) + } + } + operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, request.Operation, request.WithDockerRestart, prepareStart, t) if request.Operation == string(lifecycle.OperationStop) { return operationErr } @@ -768,135 +849,30 @@ func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycl if s.restoreDockerGuard != nil { return s.restoreDockerGuard(ctx) } - return ReconcileDockerPortGuard(ctx) + return RestoreDockerPortGuard(ctx) }) return errors.Join(operationErr, forwardingErr, dockerErr) } -func (s *FirewallService) saveFirewallResetOrder(ctx context.Context, provider filter.Provider, stored []model.FirewallRule) error { - runtime, err := s.firewallAdapter(provider) - if err != nil { - return err - } - desiredByScope := make(map[string][]filter.DesiredRule) - byUUID := make(map[string]model.FirewallRule, len(stored)) - for _, record := range stored { - if record.Origin != constant.FirewallRuleOriginCreated && record.Origin != constant.FirewallRuleOriginAdopted { - continue - } - desired, err := compileStoredFirewallRules(ctx, record, runtime) - if isFirewallPolicyIncompatible(err) { - continue - } - if err != nil { - return err - } - byUUID[record.UUID] = record - for _, rule := range desired { - key := rule.Rule.Scope.Key() - desiredByScope[key] = append(desiredByScope[key], rule) - } - } - captured := make(map[string]model.FirewallRule) - for _, group := range firewallScopeReadGroups(filter.ManagedInputScopes(provider)) { - snapshots, err := listFirewallRuleScopes(runtime, ctx, group) - if errors.Is(err, filter.ErrFamilyUnavailable) { - continue - } - if err != nil { - return err - } - for _, snapshot := range snapshots { - items, err := mergeFirewallInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desiredByScope[snapshot.Scope.Key()]}) - if err != nil { - return err - } - for _, item := range items { - if item.Desired == nil || item.Observed == nil { - continue - } - record := byUUID[item.Desired.UUID] - if provider == filter.ProviderFirewalld { - record.Priority = item.Observed.Rule.Priority - record.Sequence = nil - } else { - if item.Observed.Locator.Position == nil { - return filter.ErrVerificationFailed - } - position := int64(*item.Observed.Locator.Position) - if previous, exists := captured[record.UUID]; exists && *previous.Sequence <= position { - continue - } - record.Sequence, record.Priority = &position, nil - } - captured[record.UUID] = record - } - } - } - orders := make([]model.FirewallRule, 0, len(captured)) - for _, record := range stored { - if snapshot, exists := captured[record.UUID]; exists { - orders = append(orders, snapshot) - } - } - return s.rules.SaveResetOrder(ctx, orders) -} - -func (s *FirewallService) combinedUFWInventory(ctx context.Context, runtime filter.Adapter, scope filter.Scope, refresh bool) (dto.FirewallRuleInventoryResponse, error) { - scopes := []filter.Scope{scope, scope} - scopes[0].Family = filter.FamilyIPv4 - scopes[1].Family = filter.FamilyIPv6 - snapshots, err := readFirewallRuleScopes(runtime, ctx, scopes) - if err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - if len(snapshots) != len(scopes) { - return dto.FirewallRuleInventoryResponse{}, fmt.Errorf("%w: incomplete UFW multi-family inventory", filter.ErrAdapterUnavailable) - } - - stored, err := s.rules.List(ctx) - if err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, runtime) - response := dto.FirewallRuleInventoryResponse{Items: failures} - seenNotices := make(map[string]struct{}) - for index, snapshot := range snapshots { - if snapshot.Scope.Key() != scopes[index].Key() { - return dto.FirewallRuleInventoryResponse{}, fmt.Errorf("%w: unexpected UFW inventory scope %q", filter.ErrInvalidScope, snapshot.Scope.Key()) - } - desired := desiredByScope[snapshot.Scope.Key()] - items, err := mergeFirewallInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) - if err != nil { - return dto.FirewallRuleInventoryResponse{}, err - } - response.Items = append(response.Items, items...) - for _, notice := range snapshot.Notices { - key := string(notice.Code) + "\x00" + strings.Join(notice.Values, "\x00") - if _, exists := seenNotices[key]; exists { - continue - } - seenNotices[key] = struct{}{} - response.Notices = append(response.Notices, notice) - } - } - return response, nil -} - func finalizeFirewallInventory(response dto.FirewallRuleInventoryResponse, request dto.FirewallRuleInventory) dto.FirewallRuleInventoryResponse { provider := request.Scope.Provider if len(request.Scopes) > 0 { provider = request.Scopes[0].Provider } - response.IPv4Range, response.IPv6Range = firewallInventoryPositionRanges(provider, response.Items) - response.AllTotal = int64(len(response.Items)) - for _, item := range response.Items { - if isDeletableManagedInventoryItem(item) { - response.ManagedTotal++ - } + chain := filter.IptablesInputChain + if len(request.Scopes) == 1 { + chain = request.Scopes[0].Normalize().Chain + } else if len(request.Scopes) == 0 { + chain = request.Scope.Normalize().Chain } + response.IPv4Range, response.IPv6Range = firewallInventoryPositionRanges(provider, chain, response.Items) + response.AllTotal = 0 filtered := make([]filter.InventoryItem, 0, len(response.Items)) for _, item := range response.Items { + if item.Observed != nil && item.Observed.Protected { + continue + } + response.AllTotal++ if matchesFirewallInventoryRequest(item, request) { filtered = append(filtered, item) } @@ -917,21 +893,6 @@ func finalizeFirewallInventory(response dto.FirewallRuleInventoryResponse, reque return response } -func isDeletableManagedInventoryItem(item filter.InventoryItem) bool { - if item.Desired == nil || item.Desired.Protected || item.State == filter.InventoryStateProtected { - return false - } - if item.Desired.Origin != filter.RuleOriginCreated && item.Desired.Origin != filter.RuleOriginAdopted { - return false - } - if (item.Rule.Scope.Provider == filter.ProviderIptables || item.Rule.Scope.Provider == filter.ProviderNftables) && - (item.Rule.Scope.Chain == filter.BasicBeforeChain || item.Rule.Scope.Chain == filter.BasicAfterChain) { - return false - } - return item.State != filter.InventoryStateDrifted || - (item.Match == filter.InventoryMatchMissing && item.Observed == nil) -} - func matchesFirewallInventoryRequest(item filter.InventoryItem, request dto.FirewallRuleInventory) bool { if slices.Contains(request.ExcludeChains, item.Rule.Scope.Chain) { return false @@ -942,9 +903,6 @@ func matchesFirewallInventoryRequest(item filter.InventoryItem, request dto.Fire if len(request.Actions) > 0 && !matchesFirewallInventoryAction(item.Rule.Action, request.Actions) { return false } - if len(request.States) > 0 && !slices.Contains(request.States, item.State) { - return false - } keyword := strings.ToLower(strings.TrimSpace(request.Info)) if keyword == "" { return true @@ -952,14 +910,11 @@ func matchesFirewallInventoryRequest(item filter.InventoryItem, request dto.Fire rule := item.Rule values := []string{ firewallInventoryProtocol(rule), rule.SourceAddress, rule.SourcePort, rule.DestinationAddress, - rule.DestinationPort, rule.Description, string(rule.Action), string(item.State), + rule.DestinationPort, rule.Description, string(rule.Action), } if item.Observed != nil { values = append(values, item.Observed.Rule.Description) } - if item.Desired != nil { - values = append(values, item.Desired.Rule.Description) - } for _, value := range values { if strings.Contains(strings.ToLower(value), keyword) { return true @@ -1006,477 +961,6 @@ func firewallInventoryProtocol(rule filter.FirewallRule) string { return rule.Protocol } -func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallRuleDelete, t *task.Task) (result dto.FirewallRuleDeleteResponse, taskErr error) { - var firstFailure error - defer func() { - sort.SliceStable(result.Errors, func(i, j int) bool { return result.Errors[i].Index < result.Errors[j].Index }) - if t != nil { - t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{ - "succeeded": result.Succeeded, "failed": result.Failed, - })) - } - if taskErr == nil { - taskErr = firstFailure - } - }() - record := func(index int, ruleUUID string, err error) { - if err != nil { - result.Failed++ - result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{Index: index, UUID: ruleUUID, Error: err.Error()}) - if firstFailure == nil { - firstFailure = err - } - } else { - result.Succeeded++ - } - if t != nil { - label := fmt.Sprintf("[%d/%d] %s", result.Succeeded+result.Failed, len(request.UUIDs)+len(request.BeforeRules), ruleUUID) - t.LogWithStatus(label, err) - } - } - selectedProvider, err := s.selectedProvider(ctx) - if err != nil { - return result, err - } - type beforeGroup struct { - targets []dto.FirewallRuleDeleteTarget - indexes []int - } - beforeGroups := make(map[string]*beforeGroup) - for index, target := range request.BeforeRules { - key := target.Scope.Normalize().Key() - if beforeGroups[key] == nil { - beforeGroups[key] = &beforeGroup{} - } - group := beforeGroups[key] - group.targets = append(group.targets, target) - group.indexes = append(group.indexes, len(request.UUIDs)+index) - } - for _, group := range beforeGroups { - failures, batchErr := s.deleteBeforeRules(ctx, selectedProvider, group.targets) - for index, target := range group.targets { - err := batchErr - if failures != nil && failures[index] != nil { - err = failures[index] - } - record(group.indexes[index], target.InstanceKey, err) - } - } - if len(request.UUIDs) == 0 { - return result, nil - } - ports, err := loadFirewallPortWhiteList() - var runtime filter.Adapter - if err == nil { - runtime, err = s.firewallAdapter(selectedProvider) - } - if err != nil { - for index, value := range request.UUIDs { - record(index, strings.TrimSpace(value), err) - } - return result, err - } - whitelist := filter.NewPortWhitelistIndex(ports) - groups := make([][]firewallRuleDeleteItem, 0) - groupIndexes := make(map[string]int) - seen := make(map[string]bool, len(request.UUIDs)) - for index, value := range request.UUIDs { - ruleUUID := strings.TrimSpace(value) - if err := ctx.Err(); err != nil { - record(index, ruleUUID, err) - continue - } - if ruleUUID == "" { - record(index, ruleUUID, fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid)) - continue - } - if seen[ruleUUID] { - record(index, ruleUUID, fmt.Errorf("duplicate firewall rule UUID")) - continue - } - seen[ruleUUID] = true - stored, err := s.rules.GetByUUID(ctx, ruleUUID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - err = fmt.Errorf("%w: managed rule %q was not found", filter.ErrInvalidRule, ruleUUID) - } - record(index, ruleUUID, err) - continue - } - if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { - record(index, ruleUUID, fmt.Errorf("%w: only created or adopted rules can be deleted", filter.ErrInvalidRule)) - continue - } - rules, err := compileStoredFirewallRules(ctx, stored, runtime) - if err == nil && len(rules) == 0 { - err = fmt.Errorf("%w: policy %q has no compiled target rules", filter.ErrInvalidRule, ruleUUID) - } - if err == nil { - for _, rule := range rules { - if whitelist.Matches(rule.Rule) { - err = filter.ErrProtectedRule - break - } - } - } - if err != nil { - record(index, ruleUUID, err) - continue - } - key := rules[0].Rule.Scope.Key() - if selectedProvider == filter.ProviderUFW { - key = string(selectedProvider) - } - groupIndex, exists := groupIndexes[key] - if !exists { - groupIndex = len(groups) - groupIndexes[key] = groupIndex - groups = append(groups, nil) - } - groups[groupIndex] = append(groups[groupIndex], firewallRuleDeleteItem{index: index, stored: stored, rules: rules}) - } - for _, group := range groups { - failures, batchErr := s.deleteFirewallRuleBatch(ctx, runtime, group) - for index, item := range group { - err := batchErr - if failures != nil && failures[index] != nil { - err = failures[index] - } - record(item.index, item.stored.UUID, err) - } - } - - return result, nil -} - -func (s *FirewallService) deleteBeforeRules(ctx context.Context, provider filter.Provider, targets []dto.FirewallRuleDeleteTarget) ([]error, error) { - scope := targets[0].Scope.Normalize() - if scope.Provider != provider || (provider != filter.ProviderIptables && provider != filter.ProviderNftables) || scope.Chain != filter.BasicBeforeChain { - return nil, fmt.Errorf("%w: native deletion only supports the selected firewall before chain", filter.ErrUnsupportedScope) - } - runtime, err := s.firewallAdapter(provider) - if err != nil { - return nil, err - } - snapshot, err := readMutableFirewallRules(runtime, ctx, scope) - if err != nil { - return nil, err - } - failures := make([]error, len(targets)) - changes := make([]filter.RuleChange, 0, len(targets)) - seen := make(map[string]bool, len(targets)) - byInstance := make(map[string][]int, len(snapshot.Rules)) - for index, observed := range snapshot.Rules { - key, err := filter.InstanceKey(observed) - if err == nil { - byInstance[key] = append(byInstance[key], index) - } - } - for index, target := range targets { - if target.Scope.Normalize().Key() != scope.Key() || seen[target.InstanceKey] { - failures[index] = fmt.Errorf("%w: duplicate or mismatched before rule", filter.ErrInvalidRule) - continue - } - seen[target.InstanceKey] = true - matches := byInstance[target.InstanceKey] - if len(matches) != 1 { - failures[index] = filter.ErrRuleStale - continue - } - observed := snapshot.Rules[matches[0]] - if err := filter.GuardMutation(observed); err != nil { - failures[index] = err - continue - } - if observed.ParseStatus != filter.ParseStatusSupported || observed.Locator.Position == nil { - failures[index] = fmt.Errorf("%w: before rule cannot be deleted", filter.ErrUnsupportedScope) - continue - } - rule := observed.Rule - if rule.UUID == "" && strings.HasPrefix(observed.Marker, "1panel-rule:") { - rule.UUID = strings.TrimSpace(strings.TrimPrefix(observed.Marker, "1panel-rule:")) - } - if rule.UUID == "" { - rule.UUID = uuid.NewString() - } - locator := observed.Locator - changes = append(changes, filter.RuleChange{ - Operation: filter.ChangeDelete, Before: &rule, Locator: &locator, UnmarkedAdopted: observed.Marker == "", CommandOnly: true, - }) - } - if len(changes) == 0 { - return failures, nil - } - sort.Slice(changes, func(i, j int) bool { return *changes[i].Locator.Position > *changes[j].Locator.Position }) - if err := ctx.Err(); err != nil { - return failures, err - } - plan, err := runtime.BuildCommands(snapshot, changes) - if err == nil { - plan.CommandOnly = true - err = runtime.RunCommands(ctx, plan) - } - if err == nil { - err = persistFirewallRules(ctx, runtime, plan) - } - return failures, err -} - -func (s *FirewallService) deleteFirewallRuleBatch(ctx context.Context, runtime filter.Adapter, items []firewallRuleDeleteItem) ([]error, error) { - byScope := make(map[string]filter.RuleSet) - byMarker := make(map[string]map[string][]filter.ObservedRule) - if runtime.Provider() == filter.ProviderNftables { - scopes := make([]filter.Scope, 0) - for _, item := range items { - for _, desired := range item.rules { - scopes = append(scopes, desired.Rule.Scope) - } - } - snapshots, err := readMutableFirewallRuleScopes(runtime, ctx, scopes) - if err != nil { - return nil, err - } - for _, snapshot := range snapshots { - byScope[snapshot.Scope.Key()] = snapshot - byMarker[snapshot.Scope.Key()] = firewallRulesByMarker(snapshot.Rules) - } - } - failures := make([]error, len(items)) - changes := make([]firewallRuleBatchItem, 0, len(items)) - indexes := make([]int, 0, len(items)) - for index, item := range items { - for _, desired := range item.rules { - if desired.Protected { - failures[index] = filter.ErrProtectedRule - break - } - snapshot := filter.RuleSet{Scope: desired.Rule.Scope} - change := filter.RuleChange{Operation: filter.ChangeDelete, Before: &desired.Rule, CommandOnly: true} - if runtime.Provider() == filter.ProviderNftables { - snapshot = byScope[desired.Rule.Scope.Key()] - candidates := snapshot - if desired.Marker != "" { - candidates.Rules = byMarker[snapshot.Scope.Key()][desired.Marker] - } - observed, err := managedFirewallObserved(candidates, desired) - if errors.Is(err, filter.ErrRuleStale) { - missing, missingErr := managedFirewallRuleMissing(snapshot, desired) - if missingErr != nil { - err = missingErr - } else if missing { - continue - } - } - if err == nil { - change, err = firewallDeleteChange(observed, desired) - } - if err != nil { - failures[index] = err - break - } - } - changes = append(changes, firewallRuleBatchItem{snapshot: snapshot, change: change}) - indexes = append(indexes, index) - } - } - validChanges, validIndexes := changes[:0], indexes[:0] - for index, change := range changes { - if failures[indexes[index]] == nil { - validChanges = append(validChanges, change) - validIndexes = append(validIndexes, indexes[index]) - } - } - executeFirewallRuleBatches(ctx, runtime, validChanges, nil, func(index int, failure error) { - if failure != nil { - failures[validIndexes[index]] = failure - } - }) - deleted := make([]model.FirewallRule, 0, len(items)) - for index, item := range items { - if failures[index] == nil { - deleted = append(deleted, item.stored) - } - } - persistFailures := s.rules.DeleteBatchWithRevision(context.WithoutCancel(ctx), deleted) - for index, item := range items { - if failures[index] == nil { - failures[index] = persistFailures[item.stored.UUID] - } - } - return failures, nil -} - -func managedFirewallRuleMissing(snapshot filter.RuleSet, desired filter.DesiredRule) (bool, error) { - wanted, err := filter.RuleKey(desired.Rule) - if err != nil { - return false, err - } - if desired.RuleKey != "" && desired.RuleKey != wanted { - return false, filter.ErrInvalidRule - } - wanted, err = firewallInventoryRuleKey(desired.Rule) - if err != nil { - return false, err - } - for _, observed := range snapshot.Rules { - if desired.Marker != "" { - if observed.Marker == desired.Marker { - return false, nil - } - adopted := desired.Origin == filter.RuleOriginAdopted && observed.Marker == "" - legacy := observed.Marker == "1panel-rule:"+desired.UUID && observed.Marker != desired.Marker - if (adopted || legacy) && filter.ObservedRuleMatchesExpected(observed, desired.Rule) { - return false, nil - } - continue - } - if desired.ObservedInstanceKey != "" { - key, err := filter.InstanceKey(observed) - if err == nil && key == desired.ObservedInstanceKey { - return false, nil - } - continue - } - if observed.ParseStatus != filter.ParseStatusSupported { - continue - } - key, err := firewallInventoryRuleKey(observed.Rule) - if err != nil { - return false, err - } - if key == wanted { - return false, nil - } - } - return true, nil -} - -func (s *FirewallService) updateRule(ctx context.Context, clientIP, ruleUUID string, requestedRule filter.FirewallRule) error { - requestedRule, err := filter.NormalizeRule(requestedRule) - if err != nil { - return err - } - stored, err := s.rules.GetByUUID(ctx, ruleUUID) - if err != nil { - return err - } - previousRules, compileErr := expandStoredFirewallRule(stored, requestedRule.Scope.Provider) - if compileErr == nil && len(previousRules) == 1 { - sameContent, err := filter.SameRuleContent(previousRules[0], requestedRule) - if err != nil { - return err - } - if sameContent { - if requestedRule.Scope.Provider == filter.ProviderFirewalld { - if requestedRule.Priority != nil { - return s.updateRuleOrder(ctx, ruleUUID, nil, requestedRule.Priority, &requestedRule.Description) - } - } else if requestedRule.OrderIndex != nil { - return s.updateRuleOrder(ctx, ruleUUID, requestedRule.OrderIndex, nil, &requestedRule.Description) - } - return s.updateRuleDescription(ctx, ruleUUID, requestedRule.Description) - } - } - prepared, err := s.prepareManagedUpdate(ctx, clientIP, ruleUUID, requestedRule) - if err != nil { - return err - } - metadataOnly, err := isFirewallMetadataOnlyUpdate(prepared.Before.Rule, prepared.After, prepared.Observed.Locator) - if err != nil { - return err - } - if metadataOnly { - if prepared.After.Description == prepared.Before.Rule.Description { - return nil - } - return s.rules.UpdateWithRevision(ctx, prepared.Stored.UUID, prepared.Stored.Revision, map[string]interface{}{ - "description": prepared.After.Description, - }) - } - if prepared.Runtime.Provider() != filter.ProviderNftables { - return s.replaceManagedRule(ctx, prepared) - } - return s.executeManagedMutation(ctx, managedMutationRequest{ - Stored: prepared.Stored, Before: prepared.Before.Rule, After: prepared.After, - RuleSet: prepared.RuleSet, Locator: prepared.Observed.Locator, - AdapterOperation: filter.ChangeUpdate, Runtime: prepared.Runtime, - }) -} - -func (s *FirewallService) prepareManagedUpdate(ctx context.Context, clientIP string, ruleUUID string, requestedRule filter.FirewallRule) (preparedManagedUpdate, error) { - ruleUUID = strings.TrimSpace(ruleUUID) - if ruleUUID == "" { - return preparedManagedUpdate{}, fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid) - } - stored, before, runtime, err := s.loadManagedRule(ctx, ruleUUID) - if err != nil { - return preparedManagedUpdate{}, err - } - after, err := filter.NormalizeRule(requestedRule) - if err != nil { - return preparedManagedUpdate{}, err - } - after.UUID = stored.UUID - after, err = prepareFirewallBackendRule(ctx, runtime, after) - if err != nil { - return preparedManagedUpdate{}, err - } - if after.Scope.Key() != before.Rule.Scope.Key() { - return preparedManagedUpdate{}, buserr.New("ErrFirewallRuleScopeChange") - } - if !supportsManagedNativeKindTransition(before.Rule, after) { - return preparedManagedUpdate{}, fmt.Errorf("%w: native rule conversion requires an explicit workflow", filter.ErrUnsupportedScope) - } - if runtime.Provider() != filter.ProviderNftables { - if after.OrderIndex != nil && *after.OrderIndex < 1 { - return preparedManagedUpdate{}, fmt.Errorf("%w: target position must be positive", filter.ErrInvalidRule) - } - return preparedManagedUpdate{Stored: stored, Before: before, After: after, Runtime: runtime}, nil - } - snapshot, err := readMutableFirewallRules(runtime, ctx, before.Rule.Scope) - if err != nil { - return preparedManagedUpdate{}, err - } - observed, err := managedFirewallObserved(snapshot, before) - if err != nil { - return preparedManagedUpdate{}, err - } - capabilities, err := runtime.Capabilities(ctx) - if err != nil { - return preparedManagedUpdate{}, err - } - if capabilities.ExplicitPosition || capabilities.OwnedChains { - if observed.Locator.Position == nil { - return preparedManagedUpdate{}, fmt.Errorf("%w: managed rule has no positional locator", filter.ErrInvalidRule) - } - currentPosition := int64(*observed.Locator.Position) - if after.OrderIndex == nil { - after.OrderIndex = ¤tPosition - } else if *after.OrderIndex != currentPosition { - if err := validateFirewallRulePosition(snapshot, before.Rule, *after.OrderIndex); err != nil { - return preparedManagedUpdate{}, err - } - } - } - if err := filter.GuardMutation(observed); err != nil { - return preparedManagedUpdate{}, err - } - return preparedManagedUpdate{ - Stored: stored, Before: before, After: after, RuleSet: snapshot, Observed: observed, Runtime: runtime, - }, nil -} - -func supportsManagedNativeKindTransition(before, after filter.FirewallRule) bool { - if before.NativeKind == after.NativeKind { - return true - } - if before.Scope.Key() != after.Scope.Key() || before.Scope.Provider != filter.ProviderFirewalld { - return false - } - return before.NativeKind == filter.NativeKindZonePort && after.NativeKind == filter.NativeKindRichRule || - before.NativeKind == filter.NativeKindRichRule && after.NativeKind == filter.NativeKindZonePort -} - func (s *FirewallService) ensureWebsitePorts(ctx context.Context, ports map[string]firewall.SystemPort) error { if len(ports) == 0 { return nil @@ -1617,89 +1101,209 @@ func ensureFirewallPorts(ports []int) error { return newFirewallService().ensureWebsitePorts(context.Background(), normalized) } -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) +func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallRuleDelete, t *task.Task) (dto.FirewallRuleDeleteResponse, error) { + result := dto.FirewallRuleDeleteResponse{} + provider, err := s.selectedProvider(ctx) if err != nil { - return err + return result, 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.firewallAdapter(selected) + runtime, err := s.firewallAdapter(provider) if err != nil { - return err + return result, err } - stored, err := s.rules.List(ctx) + ports, err := loadFirewallPortWhiteList() if err != nil { - return err + return result, err } - desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, runtime) - if len(failures) > 0 { - return errors.New(failures[0].Error) + whitelist := filter.NewPortWhitelistIndex(ports) + plans := make([]filter.CommandBatch, len(request.Targets)) + groups := make([][]int, 0) + byScope := make(map[string]int) + seen := make(map[string]bool) + var failures []error + record := func(index int, err error) { + label := fmt.Sprintf("[%d/%d] %s", index+1, len(request.Targets), request.Targets[index].Scope.Key()) + if errors.Is(err, filter.ErrRuleNotFound) { + if t != nil { + t.Logf("%s: %v", label, err) + } else if global.LOG != nil { + global.LOG.Infof("%s: %v", label, err) + } + err = nil + } + if err == nil { + result.Succeeded++ + } else { + result.Failed++ + result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{Index: index, InstanceKey: request.Targets[index].InstanceKey, Error: err.Error()}) + failures = append(failures, err) + } + if t != nil { + t.LogWithStatus(label, err) + } } - for _, scope := range filter.ManagedInputScopes(selected) { - desired := desiredByScope[scope.Key()] - if len(desired) == 0 { + for index, target := range request.Targets { + if seen[target.InstanceKey] { continue } - snapshot, err := readMutableFirewallRules(runtime, ctx, scope) - if err != nil { - return err + seen[target.InstanceKey] = true + scope := target.Scope.Normalize() + if scope.Provider != provider || target.Observed.Rule.Scope.Key() != scope.Key() { + record(index, filter.ErrInvalidScope) + continue } - items, err := mergeFirewallInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) + plan, err := runtime.BuildCommands(filter.RuleSet{Scope: scope}, []filter.RuleChange{{Operation: filter.ChangeDelete, Target: &target.Observed, CommandOnly: true}}) if err != nil { - return err + record(index, err) + continue } - for _, item := range items { - 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 + if plan.Rules[0].Expected.Protected || whitelist.Matches(plan.Rules[0].Expected.Rule) { + record(index, filter.ErrProtectedRule) + continue + } + plan.CommandOnly = true + plans[index] = plan + key := scope.Key() + if provider == filter.ProviderFirewalld { + key += ":" + string(plan.Rules[0].Expected.Rule.NativeKind) + } + group, exists := byScope[key] + if !exists { + group = len(groups) + byScope[key] = group + groups = append(groups, nil) + } + groups[group] = append(groups[group], index) + } + var attempted []filter.CommandBatch + var indexes []int + var results []error + for _, group := range groups { + for start := 0; start < len(group); { + end := start + 1 + if provider != filter.ProviderUFW { + end = min(start+filter.MaxAtomicExpansion, len(group)) } - if err := ctx.Err(); err != nil { - return err + batch := group[start:end] + start = end + plan := plans[batch[0]] + err := ctx.Err() + if err == nil && len(batch) > 1 { + changes := make([]filter.RuleChange, 0, len(batch)) + for _, index := range batch { + changes = append(changes, filter.RuleChange{Operation: filter.ChangeDelete, Target: &plans[index].Rules[0].Expected, CommandOnly: true}) + } + plan, err = runtime.BuildCommands(filter.RuleSet{Scope: plan.Scope}, changes) } - matches := make([]filter.ObservedRule, 0, 1) - for _, observed := range snapshot.Rules { - if observed.Marker != "" || observed.Rule.SourceAddress != item.Observed.Rule.SourceAddress || - observed.Rule.DestinationAddress != item.Observed.Rule.DestinationAddress || - observed.Rule.SourcePort != item.Observed.Rule.SourcePort || observed.Rule.DestinationPort != item.Observed.Rule.DestinationPort { + if err == nil { + plan.CommandOnly = true + err = runtime.RunCommands(ctx, plan) + } + retry := errors.Is(err, filter.ErrRuleNotFound) + if err != nil && provider == filter.ProviderIptables { + retry = retry || strings.Contains(err.Error(), "iptables-restore: line ") && strings.Contains(err.Error(), " failed") + } + for _, index := range batch { + itemErr := err + if len(batch) > 1 && retry { + itemErr = ctx.Err() + if itemErr == nil { + itemErr = runtime.RunCommands(ctx, plans[index]) + } + } + if errors.Is(itemErr, filter.ErrRuleNotFound) { + record(index, itemErr) continue } - if filter.ObservedRuleMatchesExpected(observed, item.Desired.Rule) { - matches = append(matches, observed) - } + attempted = append(attempted, plans[index]) + indexes = append(indexes, index) + results = append(results, itemErr) } - if len(matches) != 1 { - return filter.ErrRuleStale - } - after := item.Desired.Rule - before := matches[0].Rule - locator := matches[0].Locator - _, verification, err := applyFirewallChanges(runtime, ctx, snapshot, []filter.RuleChange{{ - Operation: filter.ChangeAdopt, Before: &before, After: &after, Locator: &locator, PreviousMarker: matches[0].Marker, - }}) - if err != nil { - return err - } - if !verification.Matched { - return filter.ErrVerificationFailed - } - snapshot = verification.RuleSet } } - if selected == filter.ProviderIptables { - if err := iptables_helper.CleanupLegacyAdvancedChains(ctx); err != nil { - return err - } + for index, saveErr := range persistFirewallRuleBatches(ctx, runtime, attempted) { + record(indexes[index], errors.Join(results[index], saveErr)) } - return nil + return result, errors.Join(failures...) +} + +func (s *FirewallService) resetSubsystemRules(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) { + result := dto.FirewallRuleResetResponse{} + if err := lockFirewallLifecycleIdle(); err != nil { + return result, err + } + defer firewallLifecycleTaskMu.Unlock() + if request.Subsystem != "forwarding" && request.Subsystem != "docker" { + return result, filter.ErrInvalidRule + } + if request.Subsystem == "forwarding" { + forwardingMutationMu.Lock() + defer forwardingMutationMu.Unlock() + } else { + dockerPortGuardServiceMu.Lock() + defer dockerPortGuardServiceMu.Unlock() + } + backend := string(request.Provider) + if backend == "" { + if request.Subsystem == "forwarding" { + manager, err := newForwardingAdapter(ctx) + if err != nil { + return result, err + } + backend = manager.Name() + } else { + backend = selectedDockerFirewallBackend("") + } + } + if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables { + return result, filter.ErrInvalidRule + } + backup := dto.FirewallSubsystemBackup{Subsystem: request.Subsystem, Provider: filter.Provider(backend)} + var cleanup func() error + if request.Subsystem == "forwarding" { + manager, err := newForwardingAdapterFor(ctx, backend) + if err != nil { + return result, err + } + backup, err = newForwardingService().ExportBackup(ctx, filter.Provider(backend)) + if err != nil { + return result, err + } + result.Removed = len(backup.Forwarding) + cleanup = manager.Cleanup + } else { + runtime := newDockerFirewallRuntime(ctx, backend) + var err error + backup, err = newDockerPortGuardService().ExportBackup(ctx, filter.Provider(backend)) + if err != nil { + return result, err + } + result.Removed = len(backup.Docker.Policies) + cleanup = runtime.Cleanup + } + if request.Backup == nil || *request.Backup { + directory, err := firewallBackupDirectory(request.Subsystem) + if err != nil { + return result, err + } + result.BackupPath, err = writeFirewallJSON(directory, request.Subsystem+"-"+backend+"-"+time.Now().Format("20060102-150405")+"-"+uuid.NewString()+".json", backup) + if err != nil { + return result, err + } + } + if err := errors.Join(cleanup(), ctx.Err()); err != nil { + return result, err + } + if err := os.Remove(filepath.Join(global.Dir.FirewallDir, request.Subsystem+"-"+backend+".rules")); err != nil && !errors.Is(err, os.ErrNotExist) { + return result, err + } + key := constant.FirewallForwardingInitializedKey + if request.Subsystem == "docker" { + key = constant.FirewallDockerPortGuardStatusKey + } + if err := settingRepo.UpdateOrCreate(key, constant.StatusDisable); err != nil { + return result, err + } + result.Disabled = true + return result, nil } diff --git a/agent/app/service/firewall_docker.go b/agent/app/service/firewall_docker.go index 82ca60ff5..4d2730bdf 100644 --- a/agent/app/service/firewall_docker.go +++ b/agent/app/service/firewall_docker.go @@ -2,17 +2,16 @@ package service import ( "context" - "encoding/json" "errors" "fmt" "os" + "slices" "sort" + "strconv" "strings" "sync" "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" @@ -39,9 +38,8 @@ const ( ) type DockerPortGuardService struct { - policies repo.IDockerPortGuardRepo runtime dockerfirewall.Runtime - runtimeForBackend func(string) dockerfirewall.Runtime + runtimeForBackend func(context.Context, string) dockerfirewall.Runtime client func() (*client.Client, error) version func(string) string } @@ -50,24 +48,35 @@ var dockerPortGuardServiceMu sync.Mutex type IDockerPortGuardService interface { LoadOverview(context.Context) (dto.DockerPortGuardList, error) + ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error) LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error) Operate(context.Context, dto.DockerPortGuardOperation) error QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) - Reconcile(context.Context) error + Restore(context.Context) error +} + +func NewIDockerPortGuardService() IDockerPortGuardService { + return newDockerPortGuardService() } func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) { - policies, err := s.policies.ListManaged(ctx) + families, err := loadFirewallFamilies() if err != nil { return dto.DockerPortGuardList{}, err } + backend := selectedDockerFirewallBackend("") + inventory, err := s.guardRuntime(ctx, backend).ListPolicies() + if err != nil { + return dto.DockerPortGuardList{}, err + } + policies := dockerGuardInventoryEndpoints(inventory) unavailable := func() dto.DockerPortGuardList { backend := selectedDockerFirewallBackend("") - base := s.runtimeStatus(s.guardRuntime(backend), backend) + base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1) base.Message = i18n.Get("ErrDockerFailed") - return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)} + return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: policies} } cli, err := s.client() if err != nil { @@ -79,13 +88,13 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo return unavailable(), nil } detectedBackend := dockerFirewallBackend(info) - backend := selectedDockerFirewallBackend(detectedBackend) - base := s.runtimeStatus(s.guardRuntime(backend), backend) + backend = selectedDockerFirewallBackend(detectedBackend) + base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1) endpoints, err := discoverDockerEndpoints(ctx, cli, true) if err != nil { return dto.DockerPortGuardList{}, err } - annotateDockerEndpointManagement(endpoints, detectedBackend) + annotateDockerEndpointManagement(ctx, endpoints, detectedBackend) endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints) sort.Slice(endpoints, func(i, j int) bool { return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol) @@ -96,6 +105,24 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil } +func (s *DockerPortGuardService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) { + backend := string(provider) + if backend == "" { + backend = selectedDockerFirewallBackend("") + } + if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables { + return dto.FirewallSubsystemBackup{}, filter.ErrInvalidRule + } + inventory, err := s.guardRuntime(ctx, backend).ListPolicies() + if err != nil { + return dto.FirewallSubsystemBackup{}, err + } + if inventory.Policies == nil { + inventory.Policies = []dockerfirewall.Policy{} + } + return dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}, nil +} + func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) { cli, err := s.client() if err != nil { @@ -117,7 +144,7 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto. if info, infoErr := cli.Info(ctx); infoErr == nil { backend = dockerFirewallBackend(info) } - annotateDockerEndpointManagement(endpoints, backend) + annotateDockerEndpointManagement(ctx, endpoints, backend) return groupDockerGuardContainers(endpoints), nil } @@ -126,34 +153,17 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker defer dockerPortGuardServiceMu.Unlock() switch request.Operation { case "initialize": - runtime, backend, err := s.runtimeForDocker(ctx) - if err != nil { - return err - } - policies, err := s.runtimePolicies(ctx) - if err != nil { - return err - } - inventory, err := runtime.ListPolicies() - if err != nil { - return err - } - if err := runtime.Initialize(policies, inventory); err != nil { - return err - } - if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil { - return err - } - if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil { - return err - } - return nil + return s.initialize(ctx, request, nil) case "bind": runtime, _, err := s.runtimeForDocker(ctx) if err != nil { return err } - if err := runtime.Bind(); err != nil { + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if err := errors.Join(runtime.Bind(families...), ctx.Err()); err != nil { return err } return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable) @@ -162,9 +172,9 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker if s.runtime != nil { err = s.runtime.Unbind() } else { - err = errors.Join(dockerfirewall.NewIptables().Unbind(), dockerfirewall.NewNftables().Unbind()) + err = errors.Join(dockerfirewall.NewIptables(ctx).Unbind(), dockerfirewall.NewNftables(ctx).Unbind()) } - if err != nil { + if err = errors.Join(err, ctx.Err()); err != nil { return err } return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable) @@ -174,55 +184,22 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker } func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) { - if request.Operation != "initialize" { - return dto.FilterChainOperationResponse{}, fmt.Errorf("only Docker port guard initialization can be queued") - } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { return dto.FilterChainOperationResponse{}, err } - taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskDocker, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0) - if err != nil { - return dto.FilterChainOperationResponse{}, fmt.Errorf("create Docker port guard initialization task: %w", err) + if request.Operation != "initialize" { + return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule } - var runtime dockerfirewall.Runtime - var backend string - taskItem.AddSubTask(i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func(t *task.Task) error { - var err error - runtime, backend, err = s.runtimeForDocker(t.TaskCtx) - if err != nil { - return err + if request.BackupFile != "" { + if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil { + return dto.FilterChainOperationResponse{}, err } - t.Logf("backend=%s", backend) - return nil - }, nil) - taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeDockerGuardStep", "Docker"), func(t *task.Task) error { + } + return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error { dockerPortGuardServiceMu.Lock() defer dockerPortGuardServiceMu.Unlock() - policies, err := s.runtimePolicies(t.TaskCtx) - if err != nil { - return err - } - t.Logf("backend=%s", backend) - inventory, err := runtime.ListPolicies() - if err != nil { - return err - } - return runtime.Initialize(policies, inventory) - }, nil) - taskItem.AddSubTask(i18n.GetMsgByKey("FirewallPersistDockerGuardStep"), func(t *task.Task) error { - if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil { - return err - } - if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil { - return err - } - return nil - }, nil) - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { - return dto.FilterChainOperationResponse{}, fmt.Errorf("save Docker port guard initialization task: %w", err) - } - go func() { _ = taskItem.Execute() }() - return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil + return s.initialize(t.TaskCtx, request, t) + }) } func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) { @@ -230,20 +207,34 @@ func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolic if err != nil { return dto.FilterChainOperationResponse{}, err } - labels := make([]string, len(uuids)) - for i, id := range uuids { - labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id) - } - return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, labels, func(ctx context.Context) error { + return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error { + ctx := t.TaskCtx dockerPortGuardServiceMu.Lock() defer dockerPortGuardServiceMu.Unlock() - if err := ctx.Err(); err != nil { + runtime, backend, err := s.runtimeForDocker(ctx) + if err != nil { return err } - if err := s.policies.DeleteBatch(ctx, uuids); err != nil { + inventory, err := runtime.ListPolicies() + if err != nil { return err } - return s.reconcileLocked(ctx) + wanted := make(map[string]bool, len(uuids)) + for _, id := range uuids { + wanted[id] = true + } + remaining := make([]dockerfirewall.Policy, 0, len(inventory.Policies)) + for _, policy := range inventory.Policies { + if wanted[policy.UUID] { + delete(wanted, policy.UUID) + } else { + remaining = append(remaining, policy) + } + } + if len(wanted) > 0 { + return filter.ErrRuleStale + } + return applyDockerPolicies(ctx, runtime, backend, inventory, remaining) }) } @@ -252,7 +243,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion) } labels := make([]string, len(request.Policies)) - policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies)) + policies := make([]dockerfirewall.Policy, 0, len(request.Policies)) endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies)) count := 0 for i, policy := range request.Policies { @@ -264,7 +255,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic if err != nil { return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err) } - if normalized.Mode == dockerfirewall.ModeAll { + if normalized.Mode == dockerfirewall.ModeAll || normalized.Mode == dockerfirewall.ModeAcceptAll { count++ } else { count += len(normalized.Sources) @@ -275,42 +266,142 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic if count > filter.MaxAtomicExpansion { return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion) } - encoded, err := json.Marshal(normalized.Sources) - if err != nil { - return dto.FilterChainOperationResponse{}, 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, dto.DockerPortGuardEndpointIdentity{ - Family: normalized.Family, HostIP: normalized.HostIP, HostPort: normalized.HostPort, Protocol: normalized.Protocol, - }) + normalized.UUID = uuid.NewString() + policies = append(policies, normalized) } - return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, labels, func(ctx context.Context) error { + return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, "", labels, func(t *task.Task) error { + ctx := t.TaskCtx dockerPortGuardServiceMu.Lock() defer dockerPortGuardServiceMu.Unlock() if err := ctx.Err(); err != nil { return err } + runtime, backend, err := s.runtimeForDocker(ctx) + if err != nil { + return err + } + inventory, err := runtime.ListPolicies() + if err != nil { + return err + } + current := append([]dockerfirewall.Policy(nil), inventory.Policies...) + if request.Import { + backup := dto.FirewallSubsystemBackup{Provider: filter.Provider(backend), Docker: &dockerfirewall.PolicyInventory{Policies: policies}} + merged, err := mergeDockerBackup(inventory, backup, backend, t) + if err != nil { + return err + } + current = merged.Policies + } + byEndpoint := make(map[string]int, len(current)) + for i, policy := range current { + byEndpoint[dockerPolicyEndpointKey(policy)] = i + } + for i := range policies { + key := dockerPolicyEndpointKey(policies[i]) + index, exists := byEndpoint[key] + if request.Import { + if !exists || current[index].UUID != policies[i].UUID { + labels[i] = "" + continue + } + } else if exists { + policies[i].UUID = current[index].UUID + current[index] = policies[i] + } else { + byEndpoint[key] = len(current) + current = append(current, policies[i]) + } + endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{ + Family: policies[i].Family, HostIP: policies[i].HostIP, HostPort: policies[i].HostPort, Protocol: policies[i].Protocol, + }) + } + if len(endpoints) == 0 { + return nil + } if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil { return err } - if err := s.policies.UpsertBatch(ctx, policies); err != nil { + if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil { return err } - return s.reconcileLocked(ctx) + return nil }) } -func NewIDockerPortGuardService() IDockerPortGuardService { - return newDockerPortGuardService() +func (s *DockerPortGuardService) Restore(ctx context.Context) error { + dockerPortGuardServiceMu.Lock() + defer dockerPortGuardServiceMu.Unlock() + enabled, err := dockerPortGuardPersistedEnabled() + if err != nil || !enabled { + return err + } + runtime, backend, err := s.runtimeForDocker(ctx) + if err != nil { + return err + } + backup, err := readFirewallSubsystemBackup("docker-"+backend+".rules", "docker") + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if !slices.Contains(families, constant.FirewallFamilyIPv6) { + before := len(backup.Docker.Policies) + backup.Docker.Policies = slices.DeleteFunc(backup.Docker.Policies, func(policy dockerfirewall.Policy) bool { return policy.Family == constant.FirewallFamilyIPv6 }) + logFirewallIPv6Skipped(nil, "Docker startup", before-len(backup.Docker.Policies)) + } + missing := make(map[string]bool) + needsBind := false + for _, family := range families { + status := runtime.Status(family) + if status.Reason == dockerfirewall.ReasonInspectFailed { + return fmt.Errorf("inspect Docker guard %s failed", family) + } + if status.Reason == dockerfirewall.ReasonCommandMissing { + continue + } + missing[family] = !status.Initialized + needsBind = needsBind || (status.Initialized && !status.Effective) + } + if !missing[dockerfirewall.FamilyIPv4] && !missing[dockerfirewall.FamilyIPv6] { + if needsBind { + return runtime.Bind(families...) + } + return nil + } + inventory, err := runtime.ListPolicies() + if err != nil { + return err + } + for _, policy := range inventory.Policies { + missing[policy.Family] = false + } + if inventory.RuleOrders == nil { + inventory.RuleOrders = make(map[string][]int64) + } + for _, policy := range backup.Docker.Policies { + if !missing[policy.Family] { + continue + } + inventory.Policies = append(inventory.Policies, policy) + key := policy.Family + "\x00" + policy.UUID + inventory.RuleOrders[key] = backup.Docker.RuleOrders[key] + } + return runtime.Initialize(inventory.Policies, inventory, families...) } -func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string) dto.DockerPortGuardBase { +func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string, ipv6Enabled bool) dto.DockerPortGuardBase { ipv4 := runtime.Status(dockerfirewall.FamilyIPv4) - ipv6 := runtime.Status(dockerfirewall.FamilyIPv6) + var ipv6 dockerfirewall.FamilyStatus + if ipv6Enabled { + ipv6 = runtime.Status(dockerfirewall.FamilyIPv6) + } version := "-" if s.version != nil { version = s.version(backend) @@ -320,83 +411,68 @@ func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, b name = "nftables-docker" } return dto.DockerPortGuardBase{ + IPv6Enabled: ipv6Enabled, Name: name, Version: version, Backend: backend, - IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || ipv6.Reason != dockerfirewall.ReasonCommandMissing, + IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || (ipv6Enabled && ipv6.Reason != dockerfirewall.ReasonCommandMissing), Initialized: ipv4.Initialized || ipv6.Initialized, Bound: ipv4.Bound || ipv6.Bound, - IPv4: dto.DockerPortGuardFamilyStatus{State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective}, - IPv6: dto.DockerPortGuardFamilyStatus{State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective}, + IPv4: dto.DockerPortGuardFamilyStatus{Partial: ipv4.Partial, State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective}, + IPv6: dto.DockerPortGuardFamilyStatus{Partial: ipv6.Partial, State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective}, } } -func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.DockerPortGuardEndpoint { - endpoints := make([]dto.DockerPortGuardEndpoint, 0, len(policies)) - for _, policy := range policies { - sources := []string{} - _ = json.Unmarshal([]byte(policy.Sources), &sources) - endpoints = append(endpoints, dto.DockerPortGuardEndpoint{ - Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, - PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources, - Description: policy.Description, TrafficPath: dockerTrafficPathUnknown, - ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath, - }) - } - return endpoints -} - -func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []model.DockerPortGuardPolicy, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) { - byEndpoint := make(map[string]model.DockerPortGuardPolicy, len(policies)) - for _, policy := range policies { - byEndpoint[fmt.Sprintf("%s|%s|%d|%s", policy.Family, policy.HostIP, policy.HostPort, policy.Protocol)] = policy +func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) { + matched := make(map[int]bool, len(policies)) + byEndpoint := make(map[string]int, len(policies)) + for index, policy := range policies { + key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00") + if _, exists := byEndpoint[key]; !exists { + byEndpoint[key] = index + } } for i := range endpoints { - key := fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) - policy, ok := byEndpoint[key] - if !ok { + key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00") + index, exists := byEndpoint[key] + if !exists { continue } - sources := []string{} - _ = json.Unmarshal([]byte(policy.Sources), &sources) - endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, sources - endpoints[i].Description = policy.Description - endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && - ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective)) - delete(byEndpoint, key) + policy := policies[index] + endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources + endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective)) + matched[index] = true } - orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint)) - for _, policy := range byEndpoint { - sources := []string{} - _ = json.Unmarshal([]byte(policy.Sources), &sources) - orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{ - Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, - PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources, Description: policy.Description, - TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, - ManagementReason: dockerReasonNoMatchingPath, - }) + orphans := make([]dto.DockerPortGuardEndpoint, 0) + for i, policy := range policies { + if !matched[i] { + orphans = append(orphans, policy) + } } - return endpoints, orphanPolicies + return endpoints, orphans } func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error { if s.client == nil || len(requested) == 0 { - return nil + return ctx.Err() } cli, err := s.client() if err != nil { - return nil + return ctx.Err() } defer cli.Close() info, err := cli.Info(ctx) if err != nil { - return nil + return ctx.Err() } endpoints, err := discoverDockerEndpoints(ctx, cli, true) if err != nil { - return nil + return ctx.Err() + } + annotateDockerEndpointManagement(ctx, endpoints, dockerFirewallBackend(info)) + if err := ctx.Err(); err != nil { + return err } - annotateDockerEndpointManagement(endpoints, dockerFirewallBackend(info)) targets := make(map[string]string, len(endpoints)) for _, endpoint := range endpoints { targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget diff --git a/agent/app/service/firewall_setting.go b/agent/app/service/firewall_setting.go index dc1fbd1b8..b231cdf01 100644 --- a/agent/app/service/firewall_setting.go +++ b/agent/app/service/firewall_setting.go @@ -10,7 +10,7 @@ import ( "sync" "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" + "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" @@ -18,7 +18,10 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall" dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" + "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/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "gorm.io/gorm" ) @@ -28,106 +31,115 @@ type IFirewallSettingService interface { DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error Load(context.Context) (dto.FirewallSettings, error) Operate(context.Context, dto.FirewallBackendOperation) error + OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) + OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) } type FirewallSettingService struct{} var firewallWhitelistMu sync.Mutex -func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error { +func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) { firewallWhitelistMu.Lock() - defer firewallWhitelistMu.Unlock() firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - current, err := loadPortWhitelistSetting(tx) - if err != nil { - return err + defer func() { + firewallRuleMutationMu.Unlock() + firewallWhitelistMu.Unlock() + if result == nil { + result = newFirewallService().SyncPortWhitelist(ctx) } - current = append(current, request.Rule) - current, err = firewall.ValidatePortWhitelist(current) - if err != nil { - return err - } - value, err := json.Marshal(current) - if err != nil { - return err - } - err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error - if err != nil { - return err - } - return nil - }) + }() + current, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + current = append(current, request.Rule) + current, err = firewall.ValidatePortWhitelist(current) + if err != nil { + return err + } + if err := validateFirewallWhitelistFamilies(current); err != nil { + return err + } + value, err := json.Marshal(current) + if err != nil { + return err + } + return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value)) } -func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error { +func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) { firewallWhitelistMu.Lock() - defer firewallWhitelistMu.Unlock() firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - current, err := loadPortWhitelistSetting(tx) - if err != nil { - return err + defer func() { + firewallRuleMutationMu.Unlock() + firewallWhitelistMu.Unlock() + if result == nil { + result = newFirewallService().SyncPortWhitelist(ctx) } - index, err := findPortWhitelistRule(current, request.OldRule) - if err != nil { - return err - } - current[index] = request.Rule - current, err = firewall.ValidatePortWhitelist(current) - if err != nil { - return err - } - value, err := json.Marshal(current) - if err != nil { - return err - } - err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error - if err != nil { - return err - } - return nil - }) + }() + current, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + index, err := findPortWhitelistRule(current, request.OldRule) + if err != nil { + return err + } + current[index] = request.Rule + current, err = firewall.ValidatePortWhitelist(current) + if err != nil { + return err + } + if err := validateFirewallWhitelistFamilies(current); err != nil { + return err + } + value, err := json.Marshal(current) + if err != nil { + return err + } + return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value)) } func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error { - if request.Rule == nil { - return fmt.Errorf("select one firewall port whitelist rule to delete") - } firewallWhitelistMu.Lock() - defer firewallWhitelistMu.Unlock() firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - current, err := loadPortWhitelistSetting(tx) - if err != nil { - return err - } - index, err := findPortWhitelistRule(current, *request.Rule) - if err != nil { - return err - } - current = slices.Delete(current, index, index+1) - current, err = firewall.ValidatePortWhitelist(current) - if err != nil { - return err - } - value, err := json.Marshal(current) - if err != nil { - return err - } - err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error - if err != nil { - return err - } - return nil - }) + defer func() { + firewallRuleMutationMu.Unlock() + firewallWhitelistMu.Unlock() + }() + current, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + if request.Rule == nil { + return filter.ErrInvalidRule + } + index, err := findPortWhitelistRule(current, *request.Rule) + if err != nil { + return err + } + current = slices.Delete(current, index, index+1) + current, err = firewall.ValidatePortWhitelist(current) + if err != nil { + return err + } + if err := validateFirewallWhitelistFamilies(current); err != nil { + return err + } + value, err := json.Marshal(current) + if err != nil { + return err + } + return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value)) } func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) { - result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()} + families, err := loadFirewallFamilies() + if err != nil { + return dto.FirewallSettings{}, err + } + result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)} installed := make(map[string]bool) for _, name := range lifecycle.InstalledProviders() { @@ -153,7 +165,7 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings if err != nil { option.Message = err.Error() } else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables { - overview, err := loadSystemFirewallOverview(name, "base") + overview, err := loadSystemFirewallOverview(name, "base", families) if err != nil { option.Message = err.Error() } @@ -180,18 +192,18 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} { option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true} if option.Installed && name == result.Forwarding.Selected { - manager, err := newForwardingAdapterFor(name) + manager, err := newForwardingAdapterFor(ctx, name) if err != nil { option.Message = err.Error() } else { - status, statusErr := loadForwardingFirewallOverview(manager) + status, statusErr := loadForwardingFirewallOverview(manager, families) option.IPv4, option.IPv6 = status.IPv4, status.IPv6 if statusErr != nil { option.Message = statusErr.Error() } else { option.Initialized, option.Bound = status.IsInit, status.IsBind } - if name == constant.FirewallProviderIptables && !option.IPv6.Available { + if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available { if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() { option.IPv6.Reason = dockerfirewall.ReasonCommandMissing } @@ -228,20 +240,25 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings option.Active = false } if option.Active { - guard := newDockerFirewallRuntime(name) - ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6) - option.Initialized = ipv4.Initialized || ipv6.Initialized - option.Bound = ipv4.Bound || ipv6.Bound - option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound - option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound - option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing - option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing - option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason + guard := newDockerFirewallRuntime(ctx, name) + for _, family := range families { + status := guard.Status(family) + option.Initialized = option.Initialized || status.Initialized + option.Bound = option.Bound || status.Bound + info := dto.FirewallBackendFamilyStatus{ + Available: status.Reason != dockerfirewall.ReasonCommandMissing, + Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason, + } + if family == constant.FirewallFamilyIPv4 { + option.IPv4 = info + } else { + option.IPv6 = info + } + } } result.Docker.Options = append(result.Docker.Options, option) } - var err error - result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx)) + result.PortWhitelist, err = loadPortWhitelistSetting() if err != nil { return result, err } @@ -256,6 +273,10 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings } func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error { + if request.Operation == "cleanup" { + _, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)}) + return err + } if err := lockFirewallLifecycleIdle(); err != nil { return err } @@ -273,13 +294,12 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa } if request.Operation == "initialize" { service := newFirewallService() - rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil) whitelistErr := service.SyncPortWhitelist(ctx) - return errors.Join(rulesErr, whitelistErr) + return whitelistErr } return nil case "forwarding": - return s.operateForwarding(request) + return s.operateForwarding(ctx, request) case "docker": return s.operateDocker(ctx, request) default: @@ -287,6 +307,211 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa } } +func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) { + if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 { + return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope + } + if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables { + return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope + } + if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" { + return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation + } + subsystem := "" + switch request.Subsystem { + case "system": + subsystem = firewallTaskHost + case "forwarding": + subsystem = firewallTaskForwarding + case "docker": + subsystem = firewallTaskDocker + default: + return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope + } + if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { + return dto.FilterChainOperationResponse{}, err + } + return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error { + if err := lockFirewallLifecycleIdle(); err != nil { + return err + } + defer firewallLifecycleTaskMu.Unlock() + t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation) + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if !slices.Contains(families, request.Family) { + return fmt.Errorf("IPv6 firewall support is disabled") + } + initialize := request.Operation != "bind" + switch request.Subsystem { + case "system": + firewallWhitelistMu.Lock() + defer firewallWhitelistMu.Unlock() + if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil { + return err + } + ports, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + required, err := firewall.RequiredPortWhitelist(ports) + if err != nil { + return err + } + firewallRuleMutationMu.Lock() + if request.Backend == constant.FirewallProviderIptables { + err = iptables_helper.OperateFamily(request.Family, initialize, required) + } else { + err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required) + } + firewallRuleMutationMu.Unlock() + if err != nil { + return err + } + if initialize { + if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil { + return err + } + } + return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable) + case "forwarding": + forwardingMutationMu.Lock() + defer forwardingMutationMu.Unlock() + manager, err := newForwardingAdapter(t.TaskCtx) + if err != nil { + return err + } + if manager.Name() != request.Backend { + return filter.ErrProviderUnavailable + } + if err := manager.OperateFamily(request.Family, initialize); err != nil { + return err + } + rules, err := manager.List() + if err != nil { + return err + } + if err := persistForwardingRules(manager, rules); err != nil { + return err + } + return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable) + default: + dockerPortGuardServiceMu.Lock() + defer dockerPortGuardServiceMu.Unlock() + runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx) + if err != nil { + return err + } + if backend != request.Backend { + return filter.ErrProviderUnavailable + } + if err := runtime.OperateFamily(request.Family, initialize); err != nil { + return err + } + inventory, err := runtime.ListPolicies() + if err != nil { + return err + } + if err := persistDockerRules(backend, inventory); err != nil { + return err + } + return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable) + } + }) +} + +func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) { + if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable { + return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule + } + if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { + return dto.FilterChainOperationResponse{}, err + } + return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error { + if err := lockFirewallLifecycleIdle(); err != nil { + return err + } + defer firewallLifecycleTaskMu.Unlock() + firewallWhitelistMu.Lock() + defer firewallWhitelistMu.Unlock() + firewallRuleMutationMu.Lock() + defer firewallRuleMutationMu.Unlock() + forwardingMutationMu.Lock() + defer forwardingMutationMu.Unlock() + dockerPortGuardServiceMu.Lock() + defer dockerPortGuardServiceMu.Unlock() + if request.Status == constant.StatusEnable { + return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status) + } + ports, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + value, err := json.Marshal(ipv4PortWhitelist(ports)) + if err != nil { + return err + } + installed := lifecycle.InstalledProviders() + for _, selection := range []struct{ subsystem, key string }{ + {"system", constant.FirewallSystemBackendKey}, + {"forwarding", constant.FirewallForwardingBackendKey}, + {"docker", constant.FirewallDockerBackendKey}, + } { + backend, err := settingRepo.GetValueByKey(selection.key) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + backend = strings.ToLower(strings.TrimSpace(backend)) + if backend == "" { + if selection.subsystem == "system" { + if len(installed) == 0 { + continue + } + client, err := NewSelectedSystemFirewallClient() + if err != nil { + return err + } + backend = client.Name() + } else { + backend = constant.FirewallProviderIptables + } + } + if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) { + continue + } + t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend) + switch selection.subsystem { + case "system": + if backend == constant.FirewallProviderIptables { + err = iptables_helper.UnbindIPv6BaseChains() + } else { + err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName) + if err == nil { + err = nftables_helper.PersistRuleset(t.TaskCtx) + } + } + case "forwarding": + var manager forwarding.Adapter + manager, err = newForwardingAdapterFor(t.TaskCtx, backend) + if err == nil { + err = manager.UnbindFamily(constant.FirewallFamilyIPv6) + } + case "docker": + err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6) + } + if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) { + return err + } + } + if err := t.TaskCtx.Err(); err != nil { + return err + } + return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)}) + }) +} + func NewIFirewallSettingService() IFirewallSettingService { return &FirewallSettingService{} } @@ -297,9 +522,6 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat if _, err := lifecycle.NewClient(request.Backend); err != nil { return err } - if request.Operation == "cleanup" { - return cleanupSystemBackend(request.Backend) - } previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey) if previous == "" { if client, err := lifecycle.NewClient(""); err == nil { @@ -363,32 +585,21 @@ func systemFirewallBackendInitialized(backend string) (bool, error) { return client.Status() } -func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error { - manager, err := newForwardingAdapterFor(request.Backend) - if err != nil { +func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error { + if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil { return err } - if request.Operation == "cleanup" { - if err := manager.Cleanup(); err != nil { - return err - } - if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusDisable); err != nil { - return err - } - recordForwardingSyncError(nil) - return nil - } previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey) if request.Operation == "select" { current := previous if current == "" { - detected, err := newForwardingAdapter() + detected, err := newForwardingAdapter(ctx) if err != nil { return err } current = detected.Name() } - initialized, err := forwardingBackendInitialized(current) + initialized, err := forwardingBackendInitialized(ctx, current) if err != nil { return err } @@ -400,14 +611,13 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp return err } if request.Operation == "initialize" { - return newForwardingService().Enable() + return newForwardingService().Enable(ctx) } - recordForwardingSyncError(nil) return nil } -func forwardingBackendInitialized(backend string) (bool, error) { - manager, err := newForwardingAdapterFor(backend) +func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) { + manager, err := newForwardingAdapterFor(ctx, backend) if err != nil { if errors.Is(err, lifecycle.ErrNotInstalled) { return false, nil @@ -427,13 +637,7 @@ func forwardingBackendInitialized(backend string) (bool, error) { } func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error { - guard := newDockerFirewallRuntime(request.Backend) - if request.Operation == "cleanup" { - if err := guard.Cleanup(); err != nil { - return err - } - return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable) - } + previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey) if request.Operation == "select" { current := previous @@ -443,7 +647,7 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto. current = constant.FirewallProviderIptables } } - initialized, err := dockerGuardBackendInitialized(current) + initialized, err := dockerGuardBackendInitialized(ctx, current) if err != nil { return err } @@ -469,8 +673,8 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto. return nil } -func dockerGuardBackendInitialized(backend string) (bool, error) { - guard := newDockerFirewallRuntime(backend) +func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) { + guard := newDockerFirewallRuntime(ctx, backend) for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} { initialized, err := guard.Initialized(family) if err != nil { diff --git a/agent/app/service/firewall_sync.go b/agent/app/service/firewall_sync.go deleted file mode 100644 index 34cb943bc..000000000 --- a/agent/app/service/firewall_sync.go +++ /dev/null @@ -1,396 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - "sync" - - "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/constant" - "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/i18n" - dockerfirewall "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/forwarding" - firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" -) - -var ( - firewallRuleSyncTaskMu sync.Mutex - firewallRuleSyncTaskID string -) - -type firewallDatabaseSyncAdapter interface { - previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) - syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) -} - -func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error { - firewallWhitelistMu.Lock() - defer firewallWhitelistMu.Unlock() - - ports, err := loadFirewallPortWhiteList() - if err != nil { - return err - } - provider, err := s.selectedProvider(ctx) - if err != nil { - return err - } - if _, err := s.syncPortWhitelist(ctx, provider, ports); err != nil { - return err - } - return s.removeTransferredSystemPortRules(ctx, provider, ports) -} - -func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) { - switch strings.TrimSpace(request.Subsystem) { - case "forwarding": - service := s.forwardingSync - if service == nil { - service = newForwardingService() - } - return service.previewRuleSync(ctx, request) - case "docker": - service := s.dockerSync - if service == nil { - service = newDockerPortGuardService() - } - return service.previewRuleSync(ctx, request) - } - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - _, rules, _, err := s.loadFirewallSyncRules(ctx, request) - preview := dto.FirewallRuleSyncPreview{Subsystem: "system", TargetProvider: request.TargetProvider, Items: make([]dto.FirewallRuleSyncItem, 0, len(rules))} - for _, rule := range rules { - preview.Add(rule.FirewallRuleSyncItem) - } - return preview, err -} - -func (s *FirewallService) SyncRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) { - switch strings.TrimSpace(request.Subsystem) { - case "forwarding": - service := s.forwardingSync - if service == nil { - service = newForwardingService() - } - return service.syncRules(ctx, request) - case "docker": - service := s.dockerSync - if service == nil { - service = newDockerPortGuardService() - } - return service.syncRules(ctx, request) - default: - return s.syncSystemRules(ctx, clientIP, request) - } -} - -func (s *FirewallService) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) { - firewallRuleSyncTaskMu.Lock() - defer firewallRuleSyncTaskMu.Unlock() - return currentFirewallRuleSyncTaskLocked() -} - -func (s *DockerPortGuardService) Reconcile(ctx context.Context) error { - dockerPortGuardServiceMu.Lock() - defer dockerPortGuardServiceMu.Unlock() - return s.reconcileLocked(ctx) -} - -func (s *ForwardingService) Restore(ctx context.Context) error { - forwardingMutationMu.Lock() - defer forwardingMutationMu.Unlock() - enabled, err := s.forwardingEnabled() - if err != nil || !enabled { - if err != nil { - recordForwardingSyncError(err) - } - return err - } - manager, err := s.clientFactory() - if err != nil { - recordForwardingSyncError(err) - return err - } - stored, err := s.rules.List(ctx) - if err != nil { - recordForwardingSyncError(err) - return err - } - if err := s.initializeForwarding(manager); err != nil { - recordForwardingSyncError(err) - return err - } - err = manager.ReplaceRules(forwardingRulesFromModels(stored)) - recordForwardingSyncError(err) - return err -} - -func (s *ForwardingService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) { - targetProvider, err := databaseRuleSyncTarget(request, "forwarding") - if err != nil { - return dto.FirewallRuleSyncPreview{}, err - } - target, candidates, targetRules, _, err := s.loadRuleSyncCandidates(ctx, targetProvider) - if err != nil { - return dto.FirewallRuleSyncPreview{}, err - } - return forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules), nil -} - -func (s *ForwardingService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) { - forwardingMutationMu.Lock() - defer forwardingMutationMu.Unlock() - - targetProvider, err := databaseRuleSyncTarget(request, "forwarding") - if err != nil { - return dto.FirewallRuleSyncResult{}, err - } - target, candidates, targetRules, targetInitialized, err := s.loadRuleSyncCandidates(ctx, targetProvider) - if err != nil { - return dto.FirewallRuleSyncResult{}, err - } - preview := forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules) - desired := make([]forwarding.Rule, 0, len(candidates)) - for _, candidate := range candidates { - if candidate.err == nil { - desired = append(desired, candidate.rule) - } - } - if preview.Blocked > 0 { - return firewallSyncResult(preview, nil, false), nil - } - if len(desired) == 0 && !targetInitialized { - return firewallSyncResult(preview, nil, true), nil - } - reconcileErr := func() error { - if len(desired) > 0 { - if err := s.persistForwardingEnabled(); err != nil { - return err - } - if err := s.initializeForwarding(target); err != nil { - return err - } - } - if err := target.ReplaceRules(desired); err != nil { - return err - } - return verifyForwardingRuleSync(target, desired) - }() - result := firewallSyncResult(preview, reconcileErr, true) - recordForwardingSyncError(reconcileErr) - if reconcileErr != nil { - if preview.Ready == 0 { - return result, reconcileErr - } - return result, nil - } - return result, nil -} - -func (s *DockerPortGuardService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) { - target, policies, runtime, err := s.loadRuleSyncCandidates(ctx, request) - if err != nil { - return dto.FirewallRuleSyncPreview{}, err - } - targetInventory, err := runtime.ListPolicies() - if err != nil { - return dto.FirewallRuleSyncPreview{}, err - } - return dockerSyncPreview(filter.Provider(target), policies, targetInventory), nil -} - -func (s *DockerPortGuardService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) { - dockerPortGuardServiceMu.Lock() - defer dockerPortGuardServiceMu.Unlock() - - target, policies, targetRuntime, err := s.loadRuleSyncCandidates(ctx, request) - if err != nil { - return dto.FirewallRuleSyncResult{}, err - } - runtimePolicies := make([]dockerfirewall.Policy, 0, len(policies)) - for _, policy := range policies { - sources := []string{} - _ = json.Unmarshal([]byte(policy.Sources), &sources) - runtimePolicies = append(runtimePolicies, dockerfirewall.Policy{ - UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, - Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources, - }) - } - targetInventory, err := targetRuntime.ListPolicies() - if err != nil { - return dto.FirewallRuleSyncResult{}, err - } - preview := dockerSyncPreview(filter.Provider(target), policies, targetInventory) - for _, item := range preview.Items { - if item.Status == firewallsync.StatusBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule { - return firewallSyncResult(preview, nil, false), nil - } - } - if preview.Ready == 0 && preview.Removed == 0 { - return firewallSyncResult(preview, nil, false), nil - } - reconcileErr := func() error { - if err := reconcileDockerFirewall(target, runtimePolicies, targetRuntime, targetInventory); err != nil { - return err - } - if err := verifyDockerFirewall(targetRuntime, runtimePolicies, targetInventory.ReadOnly); err != nil { - return err - } - if len(policies) == 0 { - return nil - } - if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, target); err != nil { - return err - } - return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable) - }() - result := firewallSyncResult(preview, reconcileErr, true) - if reconcileErr != nil { - return result, reconcileErr - } - return result, nil -} - -func (s *FirewallService) syncSystemRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) { - subsystem := strings.TrimSpace(request.Subsystem) - if subsystem == "" { - subsystem = "system" - } - if err := lockFirewallLifecycleIdle(); err != nil { - return dto.FirewallRuleSyncResult{}, err - } - defer firewallLifecycleTaskMu.Unlock() - firewallRuleSyncTaskMu.Lock() - defer firewallRuleSyncTaskMu.Unlock() - - running, err := currentFirewallRuleSyncTaskLocked() - if err != nil { - return dto.FirewallRuleSyncResult{}, err - } - if running.Executing { - return dto.FirewallRuleSyncResult{ - Subsystem: subsystem, - TargetProvider: request.TargetProvider, - TaskID: running.TaskID, - Queued: true, - }, nil - } - if subsystem != "system" { - return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule) - } - taskItem, err := task.NewTask(firewallTaskName(task.TaskSync, firewallTaskHost, string(request.TargetProvider)), task.TaskSync, task.TaskScopeFirewall, "", 0) - if err != nil { - return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall sync task: %w", err) - } - taskItem.AddSubTaskWithOps(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error { - result, err := s.syncRules(t.TaskCtx, clientIP, request, t) - if err != nil { - return err - } - if result.Failed > 0 { - return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": result.Failed})) - } - return nil - }, nil, 0, 0) - - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { - taskItem.LogFailedWithErr(taskItem.Name, err) - closeUnstartedFirewallTask(taskItem) - return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall sync task: %w", err) - } - firewallRuleSyncTaskID = taskItem.TaskID - go func() { - defer func() { - firewallRuleSyncTaskMu.Lock() - if firewallRuleSyncTaskID == taskItem.TaskID { - firewallRuleSyncTaskID = "" - } - firewallRuleSyncTaskMu.Unlock() - }() - if err := taskItem.Execute(); err != nil && global.LOG != nil { - global.LOG.Errorf("firewall sync task %s failed: %v", taskItem.TaskID, err) - } - }() - return dto.FirewallRuleSyncResult{ - Subsystem: "system", - TargetProvider: request.TargetProvider, - TaskID: taskItem.TaskID, - Queued: true, - }, nil -} - -func verifyForwardingRuleSync(target forwarding.Adapter, 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 !firewallRuleStatesEqual(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 reconcileDockerFirewall(backend string, policies []dockerfirewall.Policy, runtime dockerfirewall.Runtime, inventory dockerfirewall.PolicyInventory) error { - families := make(map[string]struct{}, len(policies)) - needsInitialize, needsBind := false, false - for _, policy := range policies { - families[policy.Family] = struct{}{} - } - if len(families) == 0 { - initialized := false - for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} { - status := runtime.Status(family) - if status.Reason == dockerfirewall.ReasonInspectFailed { - return fmt.Errorf("inspect Docker firewall target %s for %s failed", backend, family) - } - initialized = initialized || status.Initialized - } - if initialized { - return runtime.ReplacePolicies(nil, inventory) - } - return nil - } - for family := range families { - status := runtime.Status(family) - needsInitialize = needsInitialize || !status.Initialized - needsBind = needsBind || !status.Bound || !status.Effective - } - var err error - if needsInitialize { - err = runtime.Initialize(policies, inventory) - } else { - if needsBind { - err = runtime.Bind() - } - if err == nil { - err = runtime.ReplacePolicies(policies, inventory) - } - } - if err != nil { - return err - } - for family := range families { - if !runtime.Status(family).Effective { - return fmt.Errorf("Docker firewall target %s is not effective for %s", backend, family) - } - } - return nil -} - -func ReconcileDockerPortGuardBestEffort(ctx context.Context) { - if err := ReconcileDockerPortGuard(ctx); err != nil { - global.LOG.Warnf("reconcile Docker port guard failed, err: %v", err) - } -} diff --git a/agent/app/service/firewall_utils.go b/agent/app/service/firewall_utils.go index e2d04018b..3ff18d409 100644 --- a/agent/app/service/firewall_utils.go +++ b/agent/app/service/firewall_utils.go @@ -6,8 +6,10 @@ import ( "errors" "fmt" "io" + "net" "net/netip" "os" + "path/filepath" "reflect" "slices" "sort" @@ -38,7 +40,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" lifecycleproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" - firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" @@ -56,16 +57,6 @@ const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_fir type firewallLifecycleClient struct{ lifecycle.Client } -type managedMutationRequest struct { - Stored model.FirewallRule - Before filter.FirewallRule - After filter.FirewallRule - RuleSet filter.RuleSet - Locator filter.Locator - AdapterOperation filter.ChangeOperation - Runtime filter.Adapter -} - type firewallDockerRestartError struct { Err error } @@ -75,50 +66,11 @@ type firewallCompletedOperationError struct { Err error } -type firewallVerification struct { - RuleSet filter.RuleSet - Matched bool -} - type firewallRuleBatchItem struct { snapshot filter.RuleSet change filter.RuleChange } -type firewallSyncRule struct { - dto.FirewallRuleSyncItem - desired filter.DesiredRule - observed *filter.ObservedRule - done bool -} - -type forwardingRuleSyncCandidate struct { - rule forwarding.Rule - err error -} - -type forwardingInventoryItem struct { - ID uint - Rule forwarding.Rule - IsDesired bool - IsRuntime bool -} - -type observedInventoryCandidate struct { - rule filter.ObservedRule - ruleKey string - instanceKey string - claimed bool -} - -type firewallRuleCollisionIndex map[string][]filter.Action - -type firewallSyncDesired[T any] struct { - Value T - Payload dto.FirewallRuleSyncItem - Err error -} - func LoadPanelPort() string { if !global.IsMaster { return global.CONF.Base.Port @@ -128,249 +80,6 @@ func LoadPanelPort() string { return portSetting.Value } -func (s *FirewallService) syncPortWhitelist(ctx context.Context, provider filter.Provider, ports []firewall.PortWhitelist) ([]filter.FirewallRule, error) { - required, err := firewall.RequiredPortWhitelist(ports) - if err != nil { - return nil, err - } - rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required)) - if provider != filter.ProviderIptables && provider != filter.ProviderNftables && len(rules) > 0 { - client, err := s.baseClient() - if err != nil { - return nil, err - } - active, err := client.Status() - if err != nil || !active { - return nil, err - } - } - client, err := s.firewallAdapter(provider) - if err != nil { - return nil, err - } - prepared := make([]dto.FirewallRuleCreateItem, 0, len(rules)) - var failures []error - activeFamilies := make(map[filter.Family]bool) - for _, rule := range rules { - if err := ctx.Err(); err != nil { - return nil, err - } - if provider == filter.ProviderIptables || provider == filter.ProviderNftables { - active, inspected := activeFamilies[rule.Scope.Family] - if !inspected { - initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), string(rule.Scope.Family)) - if err != nil { - return nil, err - } - active = initialized && bound - activeFamilies[rule.Scope.Family] = active - } - if !active { - continue - } - } - port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress} - item, err := prepareFirewallCreateRule(ctx, client, dto.FirewallRuleCreateItem{ - Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity, SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + firewall.SystemPortKey(firewall.SystemPort(port)), - }) - if err != nil { - failures = append(failures, fmt.Errorf("prepare whitelist rule %s: %w", firewall.SystemPortKey(port), err)) - continue - } - prepared = append(prepared, item) - } - if len(failures) > 0 { - return nil, errors.Join(failures...) - } - if len(prepared) == 0 { - return nil, nil - } - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - - scopes := make([]filter.Scope, 0, len(prepared)) - for _, item := range prepared { - scopes = append(scopes, item.Rule.Scope) - } - snapshots, err := readMutableFirewallRuleScopes(client, ctx, scopes) - if err != nil { - return nil, err - } - byScope := make(map[string]filter.RuleSet, len(snapshots)) - byScopeIdentity := make(map[string]firewallRuleCollisionIndex, len(snapshots)) - for _, snapshot := range snapshots { - identities, err := observedFirewallRuleCollisionIndex(snapshot) - if err != nil { - return nil, err - } - byScope[snapshot.Scope.Key()] = snapshot - byScopeIdentity[snapshot.Scope.Key()] = identities - } - var byMatch map[string][]filter.DesiredRule - type whitelistBatch struct { - snapshot filter.RuleSet - changes []filter.RuleChange - records []*model.FirewallRule - } - batches := make([]whitelistBatch, 0) - batchByScope := make(map[string]int) - for _, item := range prepared { - if err := ctx.Err(); err != nil { - return nil, err - } - rule := item.Rule - scope := rule.Scope - identities := byScopeIdentity[scope.Key()] - if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) { - continue - } else if err != nil { - return nil, err - } - if err := identities.Check(rule); err != nil { - return nil, err - } - key, err := filter.RuleMatchKey(rule) - if err != nil { - return nil, err - } - if byMatch == nil { - stored, err := s.rules.List(ctx) - if err != nil { - return nil, err - } - byMatch = make(map[string][]filter.DesiredRule) - for _, record := range stored { - rules, err := compileStoredFirewallRules(ctx, record, client) - if isFirewallPolicyIncompatible(err) { - continue - } - if err != nil { - return nil, err - } - for _, candidate := range rules { - key, err := filter.RuleMatchKey(candidate.Rule) - if err != nil { - return nil, err - } - byMatch[key] = append(byMatch[key], candidate) - } - } - } - var existing *filter.DesiredRule - for _, candidate := range byMatch[key] { - if collision := checkCollisionActions(rule.Action, candidate.Rule.Action); errors.Is(collision, filter.ErrRuleOperation) { - existing = &candidate - } else if collision != nil { - return nil, collision - } - } - var record *model.FirewallRule - if existing != nil && scope.Chain != filter.BasicBeforeChain { - rule = existing.Rule - } else { - rule.UUID = uuid.NewString() - if scope.Chain != filter.BasicBeforeChain { - created, err := firewallRuleModelForCreate(rule, item, constant.FirewallRuleOriginCreated) - if err != nil { - return nil, err - } - created.UUID = rule.UUID - record = &created - } - } - if provider != filter.ProviderFirewalld { - position := int64(1) - rule.OrderIndex = &position - } - index, exists := batchByScope[scope.Key()] - if !exists || provider != filter.ProviderIptables && provider != filter.ProviderNftables { - index = len(batches) - batchByScope[scope.Key()] = index - batches = append(batches, whitelistBatch{snapshot: byScope[scope.Key()]}) - } - batches[index].changes = append(batches[index].changes, filter.RuleChange{Operation: filter.ChangeCreate, After: &rule, CommandOnly: true}) - batches[index].records = append(batches[index].records, record) - if err := identities.Add(rule); err != nil { - return nil, err - } - } - saveRecords := func(batch whitelistBatch) { - saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - for _, record := range batch.records { - if record == nil { - continue - } - if err := s.saveFirewallRule(saveCtx, record); err != nil { - failures = append(failures, err) - if global.LOG != nil { - global.LOG.Errorf("save firewall whitelist rule %s failed: %v", record.UUID, err) - } - } - } - cancel() - } - _, savesRules := client.(filter.RuleSaver) - plans := make([]filter.CommandBatch, 0, len(batches)) - completed := make([]int, 0, len(batches)) - for batchIndex, batch := range batches { - if err := ctx.Err(); err != nil { - failures = append(failures, err) - break - } - commands, err := client.BuildCommands(batch.snapshot, batch.changes) - commands.CommandOnly = true - if err == nil { - err = client.RunCommands(ctx, commands) - } - if err != nil { - failures = append(failures, err) - if global.LOG != nil { - global.LOG.Errorf("create firewall whitelist rules for %s failed: %v", batch.snapshot.Scope.Key(), err) - } - continue - } - verification := filter.CommandBatch{Provider: commands.Provider, Scope: commands.Scope} - for _, command := range commands.Rules { - expected := command.Expected - expected.Locator.Position = nil - verification.Rules = append(verification.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected}) - } - plans = append(plans, verification) - completed = append(completed, batchIndex) - if !savesRules { - saveRecords(batch) - } - } - persistenceErrors := persistFirewallRuleBatches(ctx, client, plans) - for index, batchIndex := range completed { - if err := persistenceErrors[index]; err != nil { - failures = append(failures, err) - continue - } - if savesRules { - saveRecords(batches[batchIndex]) - } - } - - verification, err := verifyFirewallCommands(ctx, client, plans...) - if err != nil { - return nil, errors.Join(append(failures, err)...) - } - if !verification.Matched { - return nil, errors.Join(append(failures, filter.ErrVerificationFailed)...) - } - created := make([]filter.FirewallRule, 0) - for index, plan := range plans { - if persistenceErrors[index] != nil { - continue - } - for _, command := range plan.Rules { - created = append(created, command.Expected.Rule) - } - } - return created, errors.Join(failures...) -} - func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error { if len(ports) == 0 { return fmt.Errorf("firewall whitelist %s requires a port", serviceType) @@ -390,9 +99,17 @@ func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, s servicePorts = append(servicePorts, entries[index]) } if len(servicePorts) == 0 { + families, err := loadFirewallFamilies() + if err != nil { + return err + } + sources := []string{"0.0.0.0/0"} + if slices.Contains(families, constant.FirewallFamilyIPv6) { + sources = append(sources, "::/0") + } servicePorts = append(servicePorts, firewall.PortWhitelist{ Type: serviceType, Port: ports[0], Protocol: "tcp", - Sources: []string{"0.0.0.0/0", "::/0"}, + Sources: sources, }) entries = append(entries, servicePorts...) } @@ -404,7 +121,7 @@ func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, s if err != nil { return err } - client, err := s.baseClient() + _, err = s.baseClient() if err != nil && !errors.Is(err, lifecycle.ErrNotInstalled) { return err } @@ -416,23 +133,32 @@ func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, s allowances = append(allowances, rule) } } - if _, err := s.syncPortWhitelist(ctx, filter.Provider(client.Name()), allowances); err != nil { + if err := s.applyPortWhitelist(ctx, allowances, nil); err != nil { return err } } return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value)) } -func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) { - var setting model.Setting - if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) { +func loadPortWhitelistSetting() ([]firewall.PortWhitelist, error) { + setting, err := settingRepo.Get(settingRepo.WithByKey(constant.FirewallPortWhiteList)) + if errors.Is(err, gorm.ErrRecordNotFound) { setting.Value = constant.FirewallPortWhiteListValue } else if err != nil { return nil, err } var rules []firewall.PortWhitelist - err := json.Unmarshal([]byte(setting.Value), &rules) - return rules, err + if err = json.Unmarshal([]byte(setting.Value), &rules); err != nil { + return nil, err + } + families, err := loadFirewallFamilies() + if err != nil { + return nil, err + } + if !slices.Contains(families, constant.FirewallFamilyIPv6) { + rules = ipv4PortWhitelist(rules) + } + return rules, nil } func NewSelectedSystemFirewallClient() (lifecycle.Client, error) { @@ -449,6 +175,13 @@ func NewSelectedSystemFirewallClient() (lifecycle.Client, error) { } func loadFirewallInitStatus(provider, tab string) (bool, bool, error) { + families, err := loadFirewallFamilies() + if err != nil { + return false, false, err + } + if tab == "base" && len(families) == 1 { + return loadSystemFirewallFamilyStatus(provider, constant.FirewallFamilyIPv4) + } switch provider { case constant.FirewallProviderNftables: return nftables_helper.LoadInitStatus(tab) @@ -459,18 +192,20 @@ func loadFirewallInitStatus(provider, tab string) (bool, bool, error) { } } -func loadSystemFirewallOverview(provider, chainGroup string) (dto.FirewallSubsystemStatus, error) { - var status dto.FirewallSubsystemStatus +func loadSystemFirewallOverview(provider, chainGroup string, families []string) (dto.FirewallSubsystemStatus, error) { + status := dto.FirewallSubsystemStatus{IPv6Enabled: len(families) > 1} var ipv4Err, ipv6Err error status.IPv4, ipv4Err = loadSystemFirewallFamilyInfo(provider, constant.FirewallFamilyIPv4) - status.IPv6, ipv6Err = loadSystemFirewallFamilyInfo(provider, constant.FirewallFamilyIPv6) + if status.IPv6Enabled { + status.IPv6, ipv6Err = loadSystemFirewallFamilyInfo(provider, constant.FirewallFamilyIPv6) + } if chainGroup != "base" { return status, nil } if ipv4Err != nil { return status, ipv4Err } - if provider == constant.FirewallProviderIptables { + if provider == constant.FirewallProviderIptables || !status.IPv6Enabled { status.IsInit, status.IsBind = status.IPv4.Initialized, status.IPv4.Bound return status, nil } @@ -497,9 +232,16 @@ func loadSystemFirewallFamilyInfo(provider, family string) (dto.FirewallBackendF return dto.FirewallBackendFamilyStatus{Reason: dockerfirewall.ReasonCommandMissing}, nil } } - initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family) + var initialized, bound, partial bool + var err error + if provider == constant.FirewallProviderIptables { + initialized, bound, partial, err = iptables_helper.LoadFamilyState(family, "base") + } else { + initialized, bound, partial, err = nftables_helper.LoadFamilyState(filter.Family(family)) + } return dto.FirewallBackendFamilyStatus{ Available: err == nil, + Partial: partial, Initialized: initialized, Bound: bound, }, err @@ -516,131 +258,6 @@ func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) } } -func (s *FirewallService) updateRuleOrder(ctx context.Context, ruleUUID string, targetPosition *int64, priority *int, description *string) error { - if (targetPosition == nil) == (priority == nil) { - return fmt.Errorf("%w: provide either position or priority", filter.ErrInvalidRule) - } - if ruleUUID == "" { - return fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid) - } - stored, before, runtime, err := s.loadManagedRule(ctx, ruleUUID) - if err != nil { - return err - } - var snapshot filter.RuleSet - var observed filter.ObservedRule - if runtime.Provider() == filter.ProviderNftables { - snapshot, err = readMutableFirewallRules(runtime, ctx, before.Rule.Scope) - if err != nil { - return err - } - observed, err = managedFirewallObserved(snapshot, before) - if err != nil { - return err - } - } - capabilities, err := runtime.Capabilities(ctx) - if err != nil { - return err - } - after := before.Rule - adapterOperation := filter.ChangeReorder - switch { - case capabilities.ExplicitPosition || capabilities.OwnedChains: - if targetPosition == nil || *targetPosition < 1 { - return fmt.Errorf("%w: target position is required", filter.ErrInvalidRule) - } - if runtime.Provider() == filter.ProviderNftables { - if err := validateFirewallRulePosition(snapshot, before.Rule, *targetPosition); err != nil { - return err - } - } - after.OrderIndex = targetPosition - case capabilities.ExplicitPriority: - if before.Rule.NativeKind != filter.NativeKindRichRule { - return fmt.Errorf("%w: only rich rules support explicit priority", filter.ErrUnsupportedScope) - } - if priority == nil { - return fmt.Errorf("%w: priority is required", filter.ErrInvalidRule) - } - after.Priority = priority - adapterOperation = filter.ChangeUpdate - default: - return fmt.Errorf("%w: provider does not support rule reordering", filter.ErrUnsupportedScope) - } - if description != nil { - after.Description = strings.TrimSpace(*description) - } - after, err = prepareFirewallBackendRule(ctx, runtime, after) - if err != nil { - return err - } - metadataOnly, err := isFirewallMetadataOnlyUpdate(before.Rule, after, observed.Locator) - if err != nil { - return err - } - if metadataOnly { - return s.updateRuleDescription(ctx, stored.UUID, after.Description) - } - if runtime.Provider() != filter.ProviderNftables { - return s.replaceManagedRule(ctx, preparedManagedUpdate{Stored: stored, Before: before, After: after, Runtime: runtime}) - } - if err := filter.GuardMutation(observed); err != nil { - return err - } - return s.executeManagedMutation(ctx, managedMutationRequest{ - Stored: stored, Before: before.Rule, After: after, RuleSet: snapshot, Locator: observed.Locator, - AdapterOperation: adapterOperation, Runtime: runtime, - }) -} - -func (s *FirewallService) loadManagedRule(ctx context.Context, ruleUUID string) (model.FirewallRule, filter.DesiredRule, filter.Adapter, error) { - stored, err := s.rules.GetByUUID(ctx, ruleUUID) - if err != nil { - return model.FirewallRule{}, filter.DesiredRule{}, nil, err - } - if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { - return model.FirewallRule{}, filter.DesiredRule{}, nil, - fmt.Errorf("%w: only created or adopted rules can be changed", filter.ErrInvalidRule) - } - selected, err := s.selectedProviderForStoredRule(ctx, stored) - if err != nil { - return model.FirewallRule{}, filter.DesiredRule{}, nil, err - } - if err := checkFirewallRuleWhitelistProtection(selected, stored); err != nil { - return model.FirewallRule{}, filter.DesiredRule{}, nil, err - } - runtime, err := s.resolveRuntime(ctx, selected) - if err != nil { - return model.FirewallRule{}, filter.DesiredRule{}, nil, err - } - desiredRules, err := compileStoredFirewallRules(ctx, stored, runtime) - if err != nil { - return model.FirewallRule{}, filter.DesiredRule{}, nil, err - } - if len(desiredRules) != 1 { - return model.FirewallRule{}, filter.DesiredRule{}, nil, - fmt.Errorf("%w: policy %q expands to %d target rules and cannot be edited atomically", filter.ErrUnsupportedScope, ruleUUID, len(desiredRules)) - } - return stored, desiredRules[0], runtime, nil -} - -func (s *FirewallService) selectedProviderForStoredRule(ctx context.Context, _ model.FirewallRule) (filter.Provider, error) { - if s.selectedProvider != nil { - return s.selectedProvider(ctx) - } - if s.adapters != nil { - providers := make([]filter.Provider, 0, len(s.adapters)) - for provider := range s.adapters { - providers = append(providers, provider) - } - if len(providers) == 1 { - return providers[0], nil - } - } - return "", fmt.Errorf("%w: selected provider is unavailable", filter.ErrProviderUnavailable) -} - func (s *FirewallService) resolveRuntime(ctx context.Context, provider filter.Provider) (filter.Adapter, error) { if s.selectedProvider != nil { selected, err := s.selectedProvider(ctx) @@ -675,137 +292,14 @@ func (s *FirewallService) firewallAdapter(provider filter.Provider) (filter.Adap } } -func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error { - ports, err := loadFirewallPortWhiteList() - if err != nil { - return err - } - rules, err := expandStoredFirewallRule(record, provider) - if err != nil { - return err - } - whitelist := filter.NewPortWhitelistIndex(ports) - for _, rule := range rules { - if whitelist.Matches(rule) { - return filter.ErrProtectedRule - } - } - return nil -} - func loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) { - ports, err := loadPortWhitelistSetting(global.DB) + ports, err := loadPortWhitelistSetting() if err != nil { return nil, err } return firewall.ValidatePortWhitelist(ports) } -func expandStoredFirewallRule(rule model.FirewallRule, provider filter.Provider) ([]filter.FirewallRule, error) { - if rule.CompatibilityError != "" { - return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError) - } - connectionStates := make([]string, 0) - if rule.ConnectionStates != "" { - connectionStates = strings.Split(rule.ConnectionStates, ",") - } - base := filter.FirewallRule{ - Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort, - DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort, - Interface: rule.Interface, ConnectionStates: connectionStates, - Action: filter.Action(rule.Action), Description: rule.Description, - } - if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") && - strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" { - base.Protocol = "tcp/udp" - } - if provider == filter.ProviderFirewalld { - base.Priority = rule.Priority - } - families := []filter.Family{filter.Family(rule.Family)} - if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet { - hasIPv4, hasIPv6 := false, false - for _, address := range []string{base.SourceAddress, base.DestinationAddress} { - address = strings.TrimSpace(address) - if address == "" { - continue - } - if strings.Contains(address, ":") { - hasIPv6 = true - } else { - hasIPv4 = true - } - } - switch { - case hasIPv4 && hasIPv6: - return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope) - case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"): - families = []filter.Family{filter.FamilyIPv6} - case hasIPv4: - families = []filter.Family{filter.FamilyIPv4} - default: - families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} - } - } - result := make([]filter.FirewallRule, 0, len(families)) - for _, family := range families { - compiled := base - compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput} - switch provider { - case filter.ProviderIptables, filter.ProviderNftables: - compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain - case filter.ProviderFirewalld: - compiled.Scope.Zone = filter.FirewalldInputZone - case filter.ProviderUFW: - compiled.Scope.Chain = filter.UFWInputChain - default: - return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider) - } - expanded, err := filter.ExpandAtomicRules(compiled) - if err != nil { - return nil, err - } - result = append(result, expanded...) - } - return result, nil -} - -func compileStoredFirewallRules(ctx context.Context, stored model.FirewallRule, client filter.Adapter) ([]filter.DesiredRule, error) { - rules, err := expandStoredFirewallRule(stored, client.Provider()) - if err != nil { - return nil, err - } - capabilities, err := client.Capabilities(ctx) - if err != nil { - return nil, err - } - result := make([]filter.DesiredRule, 0, len(rules)) - for ordinal, rule := range rules { - prepared, err := prepareFirewallBackendRule(ctx, client, rule) - if err != nil { - return nil, err - } - ruleKey, err := filter.RuleKey(prepared) - if err != nil { - return nil, err - } - prepared.UUID = stored.UUID - if ordinal > 0 { - suffix := ruleKey - if len(suffix) > 12 { - suffix = suffix[:12] - } - prepared.UUID = fmt.Sprintf("%s-%d-%s", stored.UUID, ordinal+1, suffix) - } - desired := filter.DesiredRule{UUID: stored.UUID, Rule: prepared, RuleKey: ruleKey, Origin: filter.RuleOrigin(stored.Origin)} - if capabilities.Marker { - desired.Marker = "1panel-rule:" + prepared.UUID - } - result = append(result, desired) - } - return result, nil -} - func prepareFirewallBackendRule(ctx context.Context, client filter.Adapter, rule filter.FirewallRule) (filter.FirewallRule, error) { if preparer, ok := client.(filter.RulePreparer); ok { var err error @@ -823,11 +317,17 @@ func prepareFirewallBackendRule(ctx context.Context, client filter.Adapter, rule } func readMutableFirewallRules(client filter.Adapter, ctx context.Context, scope filter.Scope) (filter.RuleSet, error) { - snapshots, err := readMutableFirewallRuleScopes(client, ctx, []filter.Scope{scope}) + snapshots, err := listFirewallRuleScopes(client, ctx, []filter.Scope{scope}) if err != nil { return filter.RuleSet{}, err } - return snapshots[0], nil + snapshot := snapshots[0] + for _, notice := range snapshot.Notices { + if notice.Code == filter.ScopeNoticeManagedScopeInactive || notice.Code == filter.ScopeNoticeManagedScopeMissing { + return filter.RuleSet{}, fmt.Errorf("%w: managed firewall scope is unavailable", filter.ErrProviderUnavailable) + } + } + return snapshot, nil } func firewallScopeReadGroups(scopes []filter.Scope) [][]filter.Scope { @@ -889,277 +389,18 @@ func listFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes [ return snapshots, nil } -func readMutableFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) { - snapshots, err := readFirewallRuleScopes(client, ctx, scopes) - if err != nil { - return nil, err - } - for _, snapshot := range snapshots { - for _, notice := range snapshot.Notices { - if notice.Code == filter.ScopeNoticeManagedScopeInactive || notice.Code == filter.ScopeNoticeManagedScopeMissing { - return nil, fmt.Errorf("%w: managed firewall scope is unavailable", filter.ErrProviderUnavailable) - } - } - } - return snapshots, nil -} - -func readFirewallRules(client filter.Adapter, ctx context.Context, scope filter.Scope) (filter.RuleSet, error) { - snapshot, err := client.ListRules(ctx, scope) - if err != nil { - return filter.RuleSet{}, err - } - ports, err := loadFirewallPortWhiteList() - if err != nil { - return filter.RuleSet{}, err - } - return filter.ProtectRuleSet(snapshot, ports) -} - -func firewallRulesByMarker(rules []filter.ObservedRule) map[string][]filter.ObservedRule { - index := make(map[string][]filter.ObservedRule) - for _, rule := range rules { - if rule.Marker != "" { - index[rule.Marker] = append(index[rule.Marker], rule) - } - } - return index -} - -func managedFirewallObserved(snapshot filter.RuleSet, desired filter.DesiredRule) (filter.ObservedRule, error) { - matches := make([]filter.ObservedRule, 0, 1) - for _, observed := range snapshot.Rules { - if desired.Marker != "" { - if observed.Marker != desired.Marker { - continue - } - } else if desired.ObservedInstanceKey != "" { - key, err := filter.InstanceKey(observed) - if err != nil || key != desired.ObservedInstanceKey { - continue - } - } else { - key, err := firewallInventoryRuleKey(observed.Rule) - wanted, wantErr := firewallInventoryRuleKey(desired.Rule) - if err != nil || wantErr != nil || key != wanted { - continue - } - } - matches = append(matches, observed) - } - if len(matches) != 1 { - return filter.ObservedRule{}, filter.ErrRuleStale - } - observed := matches[0] - if observed.Protected || desired.Protected { - return filter.ObservedRule{}, filter.ErrProtectedRule - } - if observed.Rule.Scope.Provider != filter.ProviderFirewalld && observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged { - return filter.ObservedRule{}, filter.ErrRuleStale - } - if desired.Marker != "" && observed.ParseStatus == filter.ParseStatusOpaque { - position := observed.Rule.OrderIndex - observed.Rule = desired.Rule - observed.Rule.OrderIndex = position - observed.ParseStatus = filter.ParseStatusSupported - observed.UncertainFields = nil - } else { - expected := desired.Rule - if expected.Scope.Provider == filter.ProviderFirewalld { - expected.Priority = observed.Rule.Priority - expected.NativeKind = observed.Rule.NativeKind - expected.OrderBucket = observed.Rule.OrderBucket - } - if !filter.ObservedRuleMatchesExpected(observed, expected) { - return filter.ObservedRule{}, filter.ErrRuleStale - } - } - return observed, nil -} - -func (s *FirewallService) updateRuleDescription(ctx context.Context, ruleUUID, description string) error { - stored, err := s.rules.GetByUUID(ctx, ruleUUID) - if err != nil { - return err - } - selected, err := s.selectedProviderForStoredRule(ctx, stored) - if err != nil { - return err - } - if err := checkFirewallRuleWhitelistProtection(selected, stored); err != nil { - return err - } - if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { - return fmt.Errorf("%w: only created or adopted rules can be changed", filter.ErrInvalidRule) - } - description = strings.TrimSpace(description) - if stored.Description == description { - return nil - } - return s.rules.UpdateWithRevision(ctx, stored.UUID, stored.Revision, map[string]interface{}{"description": description}) -} - -func (s *FirewallService) replaceManagedRule(ctx context.Context, prepared preparedManagedUpdate) error { - runtime := prepared.Runtime - snapshot := filter.RuleSet{Scope: prepared.Before.Rule.Scope} - remove, err := runtime.BuildCommands(snapshot, []filter.RuleChange{{ - Operation: filter.ChangeDelete, Before: &prepared.Before.Rule, CommandOnly: true, - }}) - if err != nil { - return err - } - create, err := runtime.BuildCommands(snapshot, []filter.RuleChange{{ - Operation: filter.ChangeCreate, After: &prepared.After, CommandOnly: true, Append: prepared.After.OrderIndex == nil, - }}) - if err != nil { - return err - } - updates, err := firewallRuleSemanticUpdates(prepared.After) - if err != nil { - return err - } - if runtime.Provider() == filter.ProviderFirewalld { - updates["priority"] = prepared.After.Priority - } - remove.CommandOnly, create.CommandOnly = true, true - if err := runtime.RunCommands(ctx, remove); err != nil { - return err - } - saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - err = s.rules.UpdateWithRevision(saveCtx, prepared.Stored.UUID, prepared.Stored.Revision, updates) - cancel() - if err != nil { - return err - } - runErr := runtime.RunCommands(ctx, create) - if err := errors.Join(runErr, persistFirewallRules(ctx, runtime, create)); err != nil { - return buserr.WithDetail("ErrFirewallRuleSavedApplyFailed", err.Error(), err) - } - return nil -} - -func (s *FirewallService) executeManagedMutation(ctx context.Context, request managedMutationRequest) (err error) { - defer func() { - if err != nil && global.LOG != nil { - global.LOG.Errorf("update firewall rule %s failed: %v", request.Stored.UUID, err) - } - }() - before, after := request.Before, request.After - appendRule := after.Scope.Provider == filter.ProviderUFW && after.OrderIndex != nil && *after.OrderIndex == maxObservedFirewallPosition(request.RuleSet) - backendPlan, err := request.Runtime.BuildCommands(request.RuleSet, []filter.RuleChange{{ - Operation: request.AdapterOperation, Before: &before, After: &after, - Locator: &request.Locator, Append: appendRule, CommandOnly: true, - }}) - if err != nil { - return err - } - backendPlan.CommandOnly = true - updates, err := firewallRuleSemanticUpdates(request.After) - if err != nil { - return err - } - if len(backendPlan.Rules) == 1 && len(backendPlan.Rules[0].Commands) == 2 { - commands := backendPlan.Rules[0] - backendPlan.Rules[0].Commands = commands.Commands[:1] - backendPlan.Rules[0].RollbackCommands = commands.RollbackCommands[:1] - if err := request.Runtime.RunCommands(ctx, backendPlan); err != nil { - return err - } - if after.Scope.Provider == filter.ProviderFirewalld { - updates["priority"] = after.Priority - } - saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - err = s.rules.UpdateWithRevision(saveCtx, request.Stored.UUID, request.Stored.Revision, updates) - cancel() - if err != nil { - return err - } - backendPlan.Rules[0].Commands = commands.Commands[1:] - backendPlan.Rules[0].RollbackCommands = commands.RollbackCommands[1:] - runErr := request.Runtime.RunCommands(ctx, backendPlan) - if err := errors.Join(runErr, persistFirewallRules(ctx, request.Runtime, backendPlan)); err != nil { - return buserr.WithDetail("ErrFirewallRuleSavedApplyFailed", err.Error(), err) - } - return nil - } - runErr := request.Runtime.RunCommands(ctx, backendPlan) - if runErr != nil && request.Runtime.Provider() != filter.ProviderFirewalld { - return runErr - } - if err := errors.Join(runErr, persistFirewallRules(ctx, request.Runtime, backendPlan)); err != nil { - return err - } - sameContent, err := filter.SameRuleContent(request.Before, request.After) - if err != nil { - return err - } - if sameContent && request.Stored.Description == request.After.Description { - return nil - } - return s.rules.UpdateWithRevision(ctx, request.Stored.UUID, request.Stored.Revision, updates) -} - -func isFirewallPolicyIncompatible(err error) bool { - return errors.Is(err, filter.ErrInvalidRule) || errors.Is(err, filter.ErrUnsupportedScope) || - errors.Is(err, filter.ErrInvalidScope) || errors.Is(err, filter.ErrCompositeRule) -} - -func maxObservedFirewallPosition(snapshot filter.RuleSet) int64 { - maximum := int64(snapshot.LastPosition) - for _, observed := range snapshot.Rules { - if observed.Locator.Position != nil && int64(*observed.Locator.Position) > maximum { - maximum = int64(*observed.Locator.Position) - } - } - return maximum -} - -func applyFirewallChanges(client filter.Adapter, ctx context.Context, snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, firewallVerification, error) { +func applyFirewallChanges(client filter.Adapter, ctx context.Context, snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) { plan, err := client.BuildCommands(snapshot, changes) if err != nil { - return filter.CommandBatch{}, firewallVerification{}, err + return filter.CommandBatch{}, err } - err = client.RunCommands(ctx, plan) - if err == nil { - err = persistFirewallRules(ctx, client, plan) + if err := client.RunCommands(ctx, plan); err != nil { + return plan, err } - if err != nil { - return plan, firewallVerification{}, err + if saver, ok := client.(filter.RuleSaver); ok { + return plan, saver.SaveRules(ctx, plan.Scope) } - verification, err := verifyFirewallCommands(ctx, client, plan) - if plan.Provider == filter.ProviderUFW && len(plan.Rules) == 1 && plan.Rules[0].Operation == filter.ChangeAdopt && (err != nil || !verification.Matched) { - if err == nil { - err = filter.ErrVerificationFailed - } - err = buserr.WithDetail("ErrUFWRuleAdopt", err.Error(), err) - } - if err != nil { - if plan.CreatesOnly() { - return plan, verification, err - } - return plan, verification, rollbackFirewallPlan(ctx, client, plan, err) - } - if !verification.Matched && !plan.CreatesOnly() { - if rollbackErr := restoreFirewallCommands(client, ctx, plan); rollbackErr != nil { - return plan, verification, errors.Join(filter.ErrVerificationFailed, rollbackErr) - } - } - return plan, verification, nil -} - -func persistFirewallRules(ctx context.Context, client filter.Adapter, commands filter.CommandBatch) error { - saver, ok := client.(filter.RuleSaver) - if !ok { - return nil - } - err := saver.SaveRules(ctx, commands.Scope) - if err == nil || commands.CreatesOnly() || commands.CommandOnly { - return err - } - if rollbackErr := restoreFirewallCommands(client, ctx, commands); rollbackErr != nil { - return errors.Join(err, rollbackErr) - } - return err + return plan, nil } func persistFirewallRuleBatches(ctx context.Context, client filter.Adapter, plans []filter.CommandBatch) []error { @@ -1186,319 +427,31 @@ func persistFirewallRuleBatches(ctx context.Context, client filter.Adapter, plan return failures } -func restoreFirewallCommands(client filter.Adapter, ctx context.Context, plan filter.CommandBatch) error { - ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - defer cancel() - return client.Rollback(ctx, plan) -} - -func readFirewallCommandResults(ctx context.Context, client filter.Adapter, plans ...filter.CommandBatch) ([]filter.RuleSet, map[string]map[string][]filter.ObservedRule, error) { - scopes := make([]filter.Scope, 0, len(plans)) - for _, plan := range plans { - scopes = append(scopes, plan.Scope) - if plan.Provider == filter.ProviderUFW { - related := plan.Scope - if related.Family == filter.FamilyIPv4 { - related.Family = filter.FamilyIPv6 - } else { - related.Family = filter.FamilyIPv4 - } - scopes = append(scopes, related) - } - } - snapshots, err := listFirewallRuleScopes(client, ctx, scopes) - if err != nil { - return nil, nil, err - } - byIdentity := make(map[string]map[string][]filter.ObservedRule, len(snapshots)) - for _, snapshot := range snapshots { - if snapshot.Scope.Provider == filter.ProviderFirewalld { - canonical := make(map[string][]filter.ObservedRule) - for _, observed := range snapshot.Rules { - canonical[observed.Locator.Canonical] = append(canonical[observed.Locator.Canonical], observed) - } - byIdentity[snapshot.Scope.Key()] = canonical - } else { - byIdentity[snapshot.Scope.Key()] = firewallRulesByMarker(snapshot.Rules) - } - } - return snapshots, byIdentity, nil -} - -func verifyFirewallCommands(ctx context.Context, client filter.Adapter, plans ...filter.CommandBatch) (firewallVerification, error) { - snapshots, byIdentity, err := readFirewallCommandResults(ctx, client, plans...) - if err != nil { - return firewallVerification{}, err - } - result := firewallVerification{Matched: true} - for _, plan := range plans { - result.Matched = false - for _, snapshot := range snapshots { - if snapshot.Scope.Key() != plan.Scope.Key() { - continue - } - result.RuleSet = snapshot - result.Matched = firewallCommandsMatch(plan, snapshot, snapshots, byIdentity) - break - } - if !result.Matched { - return result, nil - } - } - return result, nil -} - -func firewallCommandsMatch(commands filter.CommandBatch, current filter.RuleSet, all []filter.RuleSet, byIdentity map[string]map[string][]filter.ObservedRule) bool { - for _, command := range commands.Rules { - if commands.Provider == filter.ProviderFirewalld { - previous, matched := 0, 0 - canonical := byIdentity[current.Scope.Key()] - if command.Previous != nil { - previous = len(canonical[command.Previous.Locator.Canonical]) - } - for _, observed := range canonical[command.Expected.Locator.Canonical] { - if observed.Persistence == filter.PersistenceStatusConverged { - want, wantErr := filter.RuleKey(command.Expected.Rule) - got, gotErr := filter.RuleKey(observed.Rule) - if wantErr == nil && gotErr == nil && want == got { - matched++ - } - } - } - if command.Operation == filter.ChangeDelete { - if previous != 0 { - return false - } - continue - } - if command.Operation == filter.ChangeUpdate && command.Previous != nil && command.Previous.Locator.Canonical != command.Expected.Locator.Canonical && previous != 0 { - return false - } - if matched != 1 { - return false - } - continue - } - markerMatches, semanticMatches := 0, 0 - positionMatches := true - requiresPosition := command.Expected.Locator.Position != nil && (commands.Provider == filter.ProviderUFW || command.Operation == filter.ChangeCreate || commands.Provider == filter.ProviderIptables && (command.Operation == filter.ChangeReorder || command.Operation == filter.ChangeUpdate && command.Expected.Rule.OrderIndex != nil)) - if requiresPosition { - positionMatches = false - } - for _, rules := range all { - if commands.Provider != filter.ProviderUFW && rules.Scope.Key() != commands.Scope.Key() { - continue - } - candidates := byIdentity[rules.Scope.Key()][command.Expected.Marker] - if command.Expected.Marker == "" { - candidates = rules.Rules - } - for _, observed := range candidates { - if observed.Marker != command.Expected.Marker { - continue - } - markerMatches++ - if rules.Scope.Key() != commands.Scope.Key() { - continue - } - same := false - if commands.Provider == filter.ProviderUFW { - same = observed.ParseStatus == filter.ParseStatusOpaque || filter.ObservedRuleMatchesExpected(observed, command.Expected.Rule) - } else { - want, wantErr := filter.RuleKey(command.Expected.Rule) - got, gotErr := filter.RuleKey(observed.Rule) - same = wantErr == nil && gotErr == nil && want == got - } - if same { - semanticMatches++ - } - if observed.Locator.Position != nil && command.Expected.Locator.Position != nil && *observed.Locator.Position == *command.Expected.Locator.Position { - positionMatches = true - } - } - } - if command.Operation == filter.ChangeDelete { - if markerMatches != 0 { - return false - } - continue - } - if markerMatches != 1 || semanticMatches != 1 || !positionMatches { - return false - } - } - return true -} - -func rollbackFirewallPlan(ctx context.Context, runtime filter.Adapter, plan filter.CommandBatch, cause error) error { - if runtime == nil { - return cause - } - if err := restoreFirewallCommands(runtime, ctx, plan); err != nil { - return errors.Join(cause, fmt.Errorf("rollback applied firewall plan: %w", err)) - } - return cause -} - -func firewallRuleSemanticUpdates(rule filter.FirewallRule) (map[string]interface{}, error) { - record, err := firewallRuleFromDomain(rule) - if err != nil { - return nil, err - } - return map[string]interface{}{ - "family": record.Family, "protocol": record.Protocol, - "source_address": record.SourceAddress, "source_port": record.SourcePort, - "destination_address": record.DestinationAddress, "destination_port": record.DestinationPort, - "interface": record.Interface, "connection_states": record.ConnectionStates, "action": record.Action, - "description": record.Description, "compatibility_error": "", - }, nil -} - -func validateFirewallRulePosition(snapshot filter.RuleSet, rule filter.FirewallRule, target int64) error { - if target < 1 { - return fmt.Errorf("%w: target position must be positive", filter.ErrInvalidRule) - } - if rule.Scope.Provider == filter.ProviderUFW { - minimum, maximum := positionBounds(snapshot) - if target < minimum || target > maximum { - return fmt.Errorf( - "%w: target position %d is outside the %s range %d-%d", - filter.ErrInvalidRule, target, rule.Scope.Family, minimum, maximum, - ) - } - return nil - } - maximum := maxObservedFirewallPosition(snapshot) - if target > maximum { - return fmt.Errorf("%w: target position %d is out of range 1-%d", filter.ErrInvalidRule, target, maximum) - } - return nil -} - -func positionBounds(snapshot filter.RuleSet) (int64, int64) { - minimum, maximum := int64(0), int64(0) - for _, observed := range snapshot.Rules { - if observed.Locator.Position == nil { - continue - } - position := int64(*observed.Locator.Position) - if minimum == 0 || position < minimum { - minimum = position - } - if position > maximum { - maximum = position - } - } - return minimum, maximum -} - -func isFirewallMetadataOnlyUpdate(before, after filter.FirewallRule, locator filter.Locator) (bool, error) { - beforeKey, err := filter.RuleKey(before) - if err != nil { - return false, err - } - afterKey, err := filter.RuleKey(after) - if err != nil { - return false, err - } - if beforeKey != afterKey { - return false, nil - } - if after.Scope.Provider == filter.ProviderFirewalld { - return true, nil - } - return locator.Position != nil && after.OrderIndex != nil && *after.OrderIndex == int64(*locator.Position), nil -} - -func (index firewallRuleCollisionIndex) Check(rule filter.FirewallRule) error { - key, err := filter.RuleMatchKey(rule) - if err != nil { - return err - } - for _, action := range index[key] { - if err := checkCollisionActions(rule.Action, action); err != nil { - return err - } - } - return nil -} - -func checkCollisionActions(requested, existing filter.Action) error { - if requested == existing { - return fmt.Errorf("%w: equivalent rule already exists", filter.ErrRuleOperation) - } - if filter.OppositeActions(requested, existing) { - return filter.ErrRuleConflict - } - return nil -} - -func firewallRuleCollisions(stored []model.FirewallRule, provider filter.Provider) (firewallRuleCollisionIndex, error) { - identities := make(firewallRuleCollisionIndex, len(stored)) - for _, candidate := range stored { - rules, err := expandStoredFirewallRule(candidate, provider) - if err != nil { - continue - } - for _, rule := range rules { - if err := identities.Add(rule); err != nil { - return nil, err - } - } - } - return identities, nil -} - -func (index firewallRuleCollisionIndex) Add(rule filter.FirewallRule) error { - key, err := filter.RuleMatchKey(rule) - if err != nil { - return err - } - index[key] = append(index[key], rule.Action) - return nil -} - -func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider, t *task.Task) error { - result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, t) - if err != nil { - return fmt.Errorf("restore database firewall rules: %w", err) - } - failures := make([]error, 0, len(result.Errors)) - for _, failure := range result.Errors { - failures = append(failures, fmt.Errorf("rule %s: %s", failure.SourceUUID, failure.Error)) - } - return errors.Join(failures...) -} - -func executeFirewallRuleBatches(ctx context.Context, runtime filter.Adapter, items []firewallRuleBatchItem, t *task.Task, record func(int, error)) { +func executeFirewallRuleBatches(ctx context.Context, runtime filter.Adapter, items []firewallRuleBatchItem) []error { groups := make([][]int, 0) - byScope := make(map[string]int) + groupIndexes := make(map[string]int) for index, item := range items { key := string(item.change.Operation) + ":" + item.snapshot.Scope.Key() - if runtime.Provider() == filter.ProviderUFW && item.change.Operation == filter.ChangeDelete { + if runtime.Provider() == filter.ProviderUFW { key = string(item.change.Operation) + } else if runtime.Provider() == filter.ProviderFirewalld { + key += ":" + string(item.change.After.NativeKind) } - group, exists := byScope[key] + group, exists := groupIndexes[key] if !exists { group = len(groups) - byScope[key] = group + groupIndexes[key] = group groups = append(groups, nil) } groups[group] = append(groups[group], index) } - var plans []filter.CommandBatch - var completed [][]int - _, savesRules := runtime.(filter.RuleSaver) + failures := make([]error, len(items)) + type appliedBatch struct { + plan filter.CommandBatch + sourceIndexes []int + } + var applied []appliedBatch for _, group := range groups { - if items[group[0]].change.Operation == filter.ChangeDelete { - sort.SliceStable(group, func(i, j int) bool { - left, right := items[group[i]].change.Locator, items[group[j]].change.Locator - if left == nil || right == nil || left.Position == nil || right.Position == nil { - return false - } - return *left.Position > *right.Position - }) - } for start := 0; start < len(group); { end := start + 1 if runtime.Provider() != filter.ProviderUFW { @@ -1512,208 +465,38 @@ func executeFirewallRuleBatches(ctx context.Context, runtime filter.Adapter, ite change.CommandOnly = true changes = append(changes, change) } + var plan filter.CommandBatch err := ctx.Err() if err == nil { - var plan filter.CommandBatch plan, err = runtime.BuildCommands(items[batch[0]].snapshot, changes) - if err == nil { - if len(batch) > 1 && changes[0].Operation == filter.ChangeCreate && t != nil { - t.Log(i18n.GetMsgWithMap("FirewallCreateBatchStep", map[string]interface{}{"backend": runtime.Provider(), "count": len(batch)})) - } - plan.CommandOnly = true - err = runtime.RunCommands(ctx, plan) - if err == nil && savesRules { - plans = append(plans, plan) - completed = append(completed, batch) - continue - } - } } - for _, index := range batch { - record(index, err) + if err == nil { + plan.CommandOnly = true + err = runtime.RunCommands(ctx, plan) } - } - } - failures := persistFirewallRuleBatches(ctx, runtime, plans) - for index, batch := range completed { - for _, item := range batch { - record(item, failures[index]) - } - } -} - -func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.FirewallRuleSyncRequest, t *task.Task) (result dto.FirewallRuleSyncResult, err error) { - if request.SourceProvider != "" || request.ResetSource { - return result, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule) - } - if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil { - return result, err - } - result = dto.FirewallRuleSyncResult{Subsystem: "system", TargetProvider: request.TargetProvider} - - created, removed, unexecuted := 0, 0, 0 - failedRemovals := make(map[string]error) - record := func(operation string, rule *firewallSyncRule, cause error, skipped bool) { - item := rule.FirewallRuleSyncItem - if operation == "TaskDelete" && rule.observed != nil { - item.Rule = &rule.observed.Rule - } - switch { - case skipped: - result.Skipped++ - unexecuted++ - rule.done = true - case cause != nil: - appendDatabaseSyncFailure(&result, item, cause) - rule.done = true - if operation == "TaskDelete" { - failedRemovals[rule.SourceUUID] = cause - } - case operation == "TaskDelete": - removed++ - if rule.Status == firewallsync.StatusRemove { - result.Removed++ - rule.done = true - } - case operation == "TaskCreate": - created++ - result.Succeeded++ - rule.done = true - default: - result.Skipped++ - rule.done = true - } - if t == nil { - return - } - label := fmt.Sprintf("%s %s", i18n.GetMsgByKey(operation), item.SourceUUID) - if r := item.Rule; r != nil { - label += fmt.Sprintf(" [%s] %s %s:%s -> %s:%s %s", r.Scope.Key(), r.Protocol, r.SourceAddress, r.SourcePort, r.DestinationAddress, r.DestinationPort, r.Action) - } - if skipped { - t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause) - } else if operation == task.TaskSync && cause == nil { - t.Logf("%s %s", label, i18n.GetMsgByKey("FirewallSyncRuleUnchanged")) - } else { - t.LogWithStatus(label, cause) - } - } - firewallWhitelistMu.Lock() - ports, whitelistErr := loadFirewallPortWhiteList() - var whitelistCreated []filter.FirewallRule - if whitelistErr == nil { - whitelistCreated, whitelistErr = s.syncPortWhitelist(ctx, request.TargetProvider, ports) - } - firewallWhitelistMu.Unlock() - createdWhitelistKeys := make(map[string]bool, len(whitelistCreated)) - for _, rule := range whitelistCreated { - key, err := filter.RuleMatchKey(rule) - if err != nil { - return result, err - } - createdWhitelistKeys[key] = true - result.Total++ - record("TaskCreate", &firewallSyncRule{FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: rule.UUID, Rule: &rule}}, nil, false) - } - if t != nil { - t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr) - } - if whitelistErr != nil { - return result, whitelistErr - } - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - if err := ctx.Err(); err != nil { - return result, err - } - runtime, rules, snapshots, err := s.loadFirewallSyncRules(ctx, request) - if err != nil { - return result, err - } - - defer func() { - if t != nil { - t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted, "unchanged": result.Skipped - unexecuted})) - } - }() - for _, rule := range rules { - if rule.Status == firewallsync.StatusExisting && rule.Rule != nil { - key, err := filter.RuleMatchKey(*rule.Rule) if err != nil { - return result, err - } - if createdWhitelistKeys[key] { - rule.done = true + for _, index := range batch { + failures[index] = err + } continue } - } - if rule.Status != firewallsync.StatusRemove { - result.Total++ - } - if rule.Status == firewallsync.StatusBlocked { - record("TaskSync", rule, errors.New(rule.Reason), false) + applied = append(applied, appliedBatch{plan: plan, sourceIndexes: batch}) } } - for _, rule := range rules { - if !rule.done && rule.Status == firewallsync.StatusExisting { - record("TaskSync", rule, nil, false) + if len(applied) == 0 { + return failures + } + plans := make([]filter.CommandBatch, len(applied)) + for index, batch := range applied { + plans[index] = batch.plan + } + saved := persistFirewallRuleBatches(ctx, runtime, plans) + for index, batch := range applied { + for _, item := range batch.sourceIndexes { + failures[item] = saved[index] } } - byScope := make(map[string]filter.RuleSet, len(snapshots)) - for _, snapshot := range snapshots { - byScope[snapshot.Scope.Key()] = snapshot - } - for _, operation := range []filter.ChangeOperation{filter.ChangeDelete, filter.ChangeCreate} { - name := "TaskCreate" - if operation == filter.ChangeDelete { - name = "TaskDelete" - } - pending := make([]*firewallSyncRule, 0) - items := make([]firewallRuleBatchItem, 0) - for _, rule := range rules { - if rule.done { - continue - } - if operation == filter.ChangeDelete && rule.observed == nil || operation == filter.ChangeCreate && rule.Status != firewallsync.StatusReady { - continue - } - if operation == filter.ChangeCreate { - var cause error - for id, err := range failedRemovals { - if id == rule.SourceUUID || strings.HasPrefix(id, rule.SourceUUID+"-") { - cause = err - break - } - } - if cause != nil { - record(name, rule, cause, true) - continue - } - } - item := firewallRuleBatchItem{snapshot: filter.RuleSet{Scope: rule.Rule.Scope}} - if operation == filter.ChangeDelete { - item.snapshot = byScope[rule.Rule.Scope.Key()] - item.change, err = firewallDeleteChange(*rule.observed, rule.desired) - if err != nil { - record(name, rule, err, false) - continue - } - if runtime.Provider() == filter.ProviderUFW { - item.snapshot.Rules = []filter.ObservedRule{*rule.observed} - } - } else { - after := *rule.Rule - after.OrderIndex = nil - item.change = filter.RuleChange{Operation: operation, After: &after, Append: true} - } - pending = append(pending, rule) - items = append(items, item) - } - executeFirewallRuleBatches(ctx, runtime, items, t, func(index int, failure error) { - record(name, pending[index], failure, false) - }) - } - return result, nil + return failures } func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested filter.Provider) error { @@ -1727,33 +510,22 @@ func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested f return nil } -func (s *FirewallService) saveFirewallRule(ctx context.Context, record *model.FirewallRule) error { - if err := s.rules.Create(ctx, record); err != nil { - message := "FirewallCreateRulePersistenceFailed" - if record.Origin == constant.FirewallRuleOriginAdopted { - message = "FirewallAdoptRulePersistenceFailed" - } - return fmt.Errorf("%s: %w", i18n.GetMsgByKey(message), err) - } - return nil -} - -func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task) (result dto.FirewallRuleCreateResponse, taskErr error) { +func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task, ruleState map[string]filter.RuleSet) (result dto.FirewallRuleCreateResponse, taskErr error) { var firstFailure error defer func() { + summary := i18n.GetMsgWithMap("FirewallCreateRulesResult", map[string]interface{}{ + "succeeded": result.Succeeded, "failed": result.Failed, "skipped": result.Skipped, + }) if t != nil { - t.Log(i18n.GetMsgWithMap("FirewallCreateRulesResult", map[string]interface{}{ - "succeeded": result.Succeeded, "failed": result.Failed, "skipped": result.Skipped, - })) + t.Log(summary) } - if taskErr == nil { - taskErr = firstFailure + if result.Failed > 0 { + taskErr = errors.Join(errors.New(summary), taskErr, firstFailure) } }() type createItem struct { index, part, count int request dto.FirewallRuleCreateItem - stored model.FirewallRule } describe := func(rule filter.FirewallRule) string { return fmt.Sprintf("%s %s %s:%s -> %s:%s %s", rule.Scope.Family, rule.Protocol, @@ -1806,7 +578,11 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR } return result, err } - items := make([]createItem, 0, len(request.Items)) + families, err := loadFirewallFamilies() + if err != nil { + return result, err + } + preparedItems := make([]*createItem, 0, len(request.Items)) var stop error for index, item := range request.Items { if stop == nil { @@ -1819,7 +595,11 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR item.Rule.OrderIndex = nil rules, err := expandFirewallCreateRule(item, selected) if err != nil { - record(createItem{index: index, request: item}, "failed", err) + status := "failed" + if item.SourceKind == constant.FirewallRuleSourceImported && errors.Is(err, filter.ErrUnsupportedScope) { + status = "skipped" + } + record(createItem{index: index, request: item}, status, err) continue } if item.SourceKind == constant.FirewallRuleSourceImported && t != nil { @@ -1832,139 +612,284 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR child := item child.Rule = rule entry := createItem{index: index, part: part, count: len(rules), request: child} + if (request.Initialize || item.SourceKind == constant.FirewallRuleSourceImported) && rule.Scope.Family == filter.FamilyIPv6 && !slices.Contains(families, constant.FirewallFamilyIPv6) { + record(entry, "skipped", fmt.Errorf("IPv6 firewall support is disabled")) + continue + } prepared, err := prepareFirewallCreateRule(ctx, runtime, child) if err != nil { - record(entry, "failed", err) + status := "failed" + if item.SourceKind == constant.FirewallRuleSourceImported && (errors.Is(err, filter.ErrUnsupportedScope) || errors.Is(err, filter.ErrInvalidRule)) { + status = "skipped" + } + record(entry, status, err) continue } entry.request = prepared - items = append(items, entry) + if child.ParseStatus == "" || child.ParseStatus == filter.ParseStatusSupported { + entry.request.Raw = "" + } + preparedItems = append(preparedItems, &entry) } } - stored, err := s.rules.List(ctx) - if err != nil { - for _, item := range items { - record(item, "failed", err) - } - return result, err + if len(preparedItems) == 0 { + return result, stop } - identities, err := firewallRuleCollisions(stored, selected) - if err != nil { - return result, err + if ruleState == nil { + ruleState = make(map[string]filter.RuleSet) } - valid := items[:0] - for _, item := range items { - if stop == nil { - stop = ctx.Err() + if len(ruleState) == 0 { + if err := loadFirewallRuleState(ctx, runtime, ruleState); err != nil { + return result, err } - if stop != nil { - record(item, "skipped", stop) - continue + } + existing := make(map[string]bool) + ufwActions := make(map[string]map[filter.Action]bool) + for _, snapshot := range ruleState { + for _, observed := range snapshot.Rules { + key, err := firewallRuleDuplicateKey(observed, false) + if err != nil { + return result, err + } + existing[key] = true + if selected == filter.ProviderUFW && observed.ParseStatus == filter.ParseStatusSupported { + matchKey, err := filter.RuleMatchKey(observed.Rule) + if err != nil { + return result, err + } + if ufwActions[matchKey] == nil { + ufwActions[matchKey] = make(map[filter.Action]bool) + } + ufwActions[matchKey][observed.Rule.Action] = true + } + if selected == filter.ProviderFirewalld && observed.ParseStatus == filter.ParseStatusSupported { + families := []filter.Family{observed.Rule.Scope.Family} + if families[0] == filter.FamilyInet { + families = append(families, filter.FamilyIPv4, filter.FamilyIPv6) + } + for _, family := range families { + candidate := observed + candidate.Rule.Scope.Family = family + key, err := firewallRuleDuplicateKey(candidate, true) + if err != nil { + return result, err + } + existing[key] = true + } + } } - rule := item.request.Rule - if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) { - record(item, "skipped", err) - continue - } else if err != nil { - record(item, "failed", err) - continue - } - item.stored, err = firewallRuleModelForCreate(rule, item.request, constant.FirewallRuleOriginCreated) + } + mergedItems := make([]*createItem, 0, len(preparedItems)) + pending := make(map[string]bool) + priority := 0 + for _, item := range preparedItems { + converted := item.request.SourceKind == constant.FirewallRuleSourceImported && selected == filter.ProviderFirewalld && request.Items[item.index].Rule.Scope.Provider != selected + observed := filter.ObservedRule{Rule: item.request.Rule, Raw: item.request.Raw, ParseStatus: item.request.ParseStatus} + key, err := firewallRuleDuplicateKey(observed, converted) if err != nil { - record(item, "failed", err) + return result, err + } + if pending[key] { + record(*item, "skipped", buserr.New("ErrRecordExist")) continue } - item.stored.UUID = uuid.NewString() - rule.UUID = item.stored.UUID - item.request.Rule = rule - if err := identities.Add(rule); err != nil { - record(item, "failed", err) + if converted { + priority++ + } + if existing[key] { + pending[key] = true + record(*item, "skipped", buserr.New("ErrRecordExist")) continue } - valid = append(valid, item) + if selected == filter.ProviderUFW && item.request.ParseStatus == filter.ParseStatusSupported { + matchKey, err := filter.RuleMatchKey(item.request.Rule) + if err != nil { + return result, err + } + actions := ufwActions[matchKey] + if len(actions) > 1 || len(actions) == 1 && !actions[item.request.Rule.Action] { + record(*item, "failed", buserr.New("ErrFirewallRuleConflict")) + continue + } + ufwActions[matchKey] = map[filter.Action]bool{item.request.Rule.Action: true} + } + if converted { + if priority > 32767 { + return result, fmt.Errorf("%w: firewalld import order exceeds the priority range", filter.ErrExpansionLimit) + } + value := priority + item.request.Rule.Priority = &value + prepared, err := prepareFirewallCreateRule(ctx, runtime, item.request) + if err != nil { + status := "failed" + if errors.Is(err, filter.ErrUnsupportedScope) || errors.Is(err, filter.ErrInvalidRule) { + status = "skipped" + } + record(*item, status, err) + continue + } + item.request = prepared + actualKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: prepared.Rule, ParseStatus: prepared.ParseStatus}, false) + if err != nil { + return result, err + } + pending[key] = true + if existing[actualKey] || pending[actualKey] { + record(*item, "skipped", buserr.New("ErrRecordExist")) + continue + } + pending[actualKey] = true + } + pending[key] = true + mergedItems = append(mergedItems, item) } - changes := make([]firewallRuleBatchItem, 0, len(valid)) - for index := range valid { - rule := &valid[index].request.Rule + for _, item := range mergedItems { + item.request.Rule.UUID = uuid.NewString() + } + creates := mergedItems + rawScopes := make([]filter.Scope, 0) + rawByScope := make(map[string]filter.RuleSet) + for _, item := range creates { + if item.request.Raw != "" { + scope := item.request.Rule.Scope + if snapshot, exists := ruleState[scope.Key()]; exists { + rawByScope[scope.Key()] = snapshot + } else { + rawScopes = append(rawScopes, scope) + } + } + } + rawSnapshots, err := listFirewallRuleScopes(runtime, ctx, rawScopes) + if err != nil { + return result, err + } + for _, snapshot := range rawSnapshots { + rawByScope[snapshot.Scope.Key()] = snapshot + } + changes := make([]firewallRuleBatchItem, 0, len(creates)) + for _, item := range creates { + rule := &item.request.Rule + snapshot := filter.RuleSet{Scope: rule.Scope} + if current, exists := rawByScope[rule.Scope.Key()]; exists { + snapshot = current + } changes = append(changes, firewallRuleBatchItem{ - snapshot: filter.RuleSet{Scope: rule.Scope}, - change: filter.RuleChange{Operation: filter.ChangeCreate, After: rule, Append: true}, + snapshot: snapshot, + change: filter.RuleChange{Operation: filter.ChangeCreate, After: rule, Append: true, Raw: item.request.Raw}, }) } - executeFirewallRuleBatches(ctx, runtime, changes, t, func(index int, err error) { - item := valid[index] + descriptions := make([]model.CommonDescription, 0, len(creates)) + for index, err := range executeFirewallRuleBatches(ctx, runtime, changes) { + item := creates[index] if err != nil { - if errors.Is(err, filterfirewalld.ErrAlreadyEnabled) { - record(item, "skipped", err) + if errors.Is(err, filterfirewalld.ErrAlreadyEnabled) || errors.Is(err, filterufw.ErrAlreadyEnabled) { + record(*item, "skipped", err) } else if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - record(item, "skipped", err) + record(*item, "skipped", err) stop = err } else { - record(item, "failed", fmt.Errorf("%s: %w", i18n.GetMsgByKey("FirewallCreateRuleExecutionFailed"), err)) + record(*item, "failed", fmt.Errorf("%s: %w", i18n.GetMsgByKey("FirewallCreateRuleExecutionFailed"), err)) } - return + continue } + scope := item.request.Rule.Scope + snapshot := ruleState[scope.Key()] + snapshot.Scope = scope + snapshot.Rules = append(snapshot.Rules, filter.ObservedRule{Rule: item.request.Rule, Raw: item.request.Raw, ParseStatus: item.request.ParseStatus}) + ruleState[scope.Key()] = snapshot + if item.request.Rule.Description != "" { + observed := filter.ObservedRule{Rule: item.request.Rule, Raw: item.request.Raw, ParseStatus: item.request.ParseStatus} + id, err := filter.DescriptionID(observed) + if err != nil { + return result, err + } + descriptions = append(descriptions, model.CommonDescription{ID: id, Type: "firewall", Description: item.request.Rule.Description}) + } + record(*item, "succeeded", nil) + } + if len(descriptions) > 0 { persistCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - defer cancel() - if err := s.saveFirewallRule(persistCtx, &item.stored); err != nil { - record(item, "failed", err) - } else { - record(item, "succeeded", nil) + err := settingRepo.SaveDescriptions(persistCtx, descriptions) + cancel() + if err != nil { + if t != nil { + t.Logf("save firewall rule descriptions: %v", err) + } + global.LOG.Warnf("save firewall rule descriptions: %v", err) } - }) + } return result, stop } func validateFirewallCreateBatch(request dto.FirewallRuleCreate, provider filter.Provider) error { - count := 0 - if len(request.Items) > filter.MaxAtomicExpansion { - return fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion) + if len(request.Items) == 0 && !request.Initialize { + return filter.ErrInvalidRule } + count := 0 + importing := true for _, item := range request.Items { - rules, expandErr := expandFirewallCreateRule(item, provider) - if errors.Is(expandErr, filter.ErrExpansionLimit) { - return fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion) + importing = importing && item.SourceKind == constant.FirewallRuleSourceImported + rules, err := expandFirewallCreateRule(item, provider) + if errors.Is(err, filter.ErrExpansionLimit) { + return err } count += len(rules) - if count > filter.MaxAtomicExpansion { - return fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion) - } + } + if !importing && count > filter.MaxAtomicExpansion { + return filter.ErrExpansionLimit } return nil } func expandFirewallCreateRule(item dto.FirewallRuleCreateItem, provider filter.Provider) ([]filter.FirewallRule, error) { - if item.SourceKind == constant.FirewallRuleSourceImported { - source := item.Rule.Scope.Normalize().Provider - if source == "" { - source = provider + rule := item.Rule + if item.Raw != "" && item.ParseStatus != "" && item.ParseStatus != filter.ParseStatusSupported { + if item.SourceKind != constant.FirewallRuleSourceImported || rule.Scope.Provider != provider { + return nil, fmt.Errorf("%w: native rule can only be imported into its original backend", filter.ErrUnsupportedScope) } - rules, err := filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(item.Rule, source)) + return []filter.FirewallRule{rule}, rule.Scope.ValidateMVP() + } + source := rule.Scope.Provider + if item.SourceKind != constant.FirewallRuleSourceImported || source == "" || source == provider { + return filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(rule, provider)) + } + if rule.NativeKind == filter.NativeKindOpaque || rule.NativeKind == filter.NativeKindZoneService || rule.NativeKind == filter.NativeKindUFWApplication { + return nil, fmt.Errorf("%w: native rule cannot be converted to %s", filter.ErrUnsupportedScope, provider) + } + if provider == filter.ProviderFirewalld { + priority := 0 + rule.Priority = &priority + } + families := []filter.Family{rule.Scope.Family} + if families[0] == filter.FamilyInet && provider != filter.ProviderFirewalld { + switch { + case strings.Contains(rule.SourceAddress, ":") || strings.Contains(rule.DestinationAddress, ":") || rule.Protocol == "icmpv6": + families = []filter.Family{filter.FamilyIPv6} + case rule.SourceAddress != "" || rule.DestinationAddress != "": + families = []filter.Family{filter.FamilyIPv4} + default: + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } + } + var result []filter.FirewallRule + for _, family := range families { + converted := rule + converted.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput} + if (source == filter.ProviderIptables || source == filter.ProviderNftables) && (provider == filter.ProviderIptables || provider == filter.ProviderNftables) { + converted.Scope.Chain = rule.Scope.Chain + } + converted.NativeKind, converted.OrderBucket = "", "" + converted.UUID, converted.OrderIndex = "", nil + if provider != filter.ProviderFirewalld { + converted.Priority = nil + } + expanded, err := filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(converted, provider)) if err != nil { return nil, err } - var converted []filter.FirewallRule - for _, sourceRule := range rules { - policy, err := firewallRuleFromDomain(sourceRule) - if err != nil { - return nil, err - } - targetRules, err := expandStoredFirewallRule(policy, provider) - if err != nil { - return nil, err - } - converted = append(converted, targetRules...) - } - return converted, nil + result = append(result, expanded...) } - rule := applySelectedProviderScopeDefaults(item.Rule, provider) - if provider == filter.ProviderUFW && strings.TrimSpace(rule.DestinationPort) != "" { - protocol := strings.ToLower(strings.TrimSpace(rule.Protocol)) - if protocol == "" || protocol == "all" || protocol == "any" { - return filter.ExpandAtomicRules(rule) - } - } - return []filter.FirewallRule{rule}, nil + return result, nil } func applySelectedProviderScopeDefaults(rule filter.FirewallRule, selected filter.Provider) filter.FirewallRule { @@ -2010,7 +935,27 @@ func applySelectedProviderScopeDefaults(rule filter.FirewallRule, selected filte } func prepareFirewallCreateRule(ctx context.Context, runtime filter.Adapter, request dto.FirewallRuleCreateItem) (dto.FirewallRuleCreateItem, error) { + request.Rule.Description = strings.TrimSpace(request.Rule.Description) selected := runtime.Provider() + if (selected == filter.ProviderIptables || selected == filter.ProviderNftables) && request.Rule.Scope.Chain != "" && request.Rule.Scope.Chain != filter.IptablesInputChain && request.Rule.Description != "" { + return dto.FirewallRuleCreateItem{}, filter.ErrRuleOperation + } + if request.Raw != "" && request.ParseStatus != "" && request.ParseStatus != filter.ParseStatusSupported { + if request.Rule.Scope.Provider != selected { + return dto.FirewallRuleCreateItem{}, filter.ErrUnsupportedScope + } + plan, err := runtime.BuildCommands(filter.RuleSet{Scope: request.Rule.Scope}, []filter.RuleChange{{Operation: filter.ChangeCreate, After: &request.Rule, Raw: request.Raw, CommandOnly: true, Append: true}}) + if err != nil { + return dto.FirewallRuleCreateItem{}, err + } + description := request.Rule.Description + request.Rule = plan.Rules[0].Expected.Rule + request.Rule.Description = description + if plan.Rules[0].Expected.ParseStatus == filter.ParseStatusSupported { + request.ParseStatus, request.Raw = filter.ParseStatusSupported, "" + } + return request, nil + } rule, err := filter.NormalizeRule(applySelectedProviderScopeDefaults(request.Rule, selected)) if err != nil { return dto.FirewallRuleCreateItem{}, err @@ -2024,6 +969,7 @@ func prepareFirewallCreateRule(ctx context.Context, runtime filter.Adapter, requ } rule.UUID = "" request.Rule = rule + request.ParseStatus = filter.ParseStatusSupported if request.SourceKind == "" { request.SourceKind = constant.FirewallRuleSourceUser } @@ -2048,82 +994,6 @@ func readFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes [ return snapshots, nil } -func observedFirewallRuleCollisionIndex(snapshot filter.RuleSet) (firewallRuleCollisionIndex, error) { - index := make(firewallRuleCollisionIndex, len(snapshot.Rules)) - for _, observed := range snapshot.Rules { - if observed.ParseStatus != filter.ParseStatusSupported { - continue - } - if err := index.Add(observed.Rule); err != nil { - return nil, err - } - } - return index, nil -} - -func firewallRuleFromDomain(rule filter.FirewallRule) (model.FirewallRule, error) { - normalized, err := filter.NormalizeRule(rule) - if err != nil { - return model.FirewallRule{}, err - } - switch normalized.NativeKind { - case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule: - default: - return model.FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind) - } - record := model.FirewallRule{ - Family: string(normalized.Scope.Family), - Protocol: normalized.Protocol, - SourceAddress: normalized.SourceAddress, - SourcePort: normalized.SourcePort, - DestinationAddress: normalized.DestinationAddress, - DestinationPort: normalized.DestinationPort, - Interface: normalized.Interface, - ConnectionStates: strings.Join(normalized.ConnectionStates, ","), - Action: string(normalized.Action), - Description: normalized.Description, - } - return record, nil -} - -func sortFirewallRules(rules []model.FirewallRule, provider filter.Provider) { - sort.SliceStable(rules, func(i, j int) bool { - left, right := rules[i], rules[j] - if provider == filter.ProviderFirewalld { - switch { - case left.Priority == nil && right.Priority != nil: - return false - case left.Priority != nil && right.Priority == nil: - return true - case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority: - return *left.Priority < *right.Priority - } - } else { - switch { - case left.Sequence == nil && right.Sequence != nil: - return false - case left.Sequence != nil && right.Sequence == nil: - return true - case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence: - return *left.Sequence < *right.Sequence - } - } - return left.UUID < right.UUID - }) -} -func firewallRuleModelForCreate(rule filter.FirewallRule, request dto.FirewallRuleCreateItem, origin string) (model.FirewallRule, error) { - record, err := firewallRuleFromDomain(rule) - if err != nil { - return model.FirewallRule{}, err - } - record.Origin = origin - record.Owner = strings.TrimSpace(request.SourceKind) - if sourceID := strings.TrimSpace(request.SourceID); sourceID != "" { - record.Owner += ":" + sourceID - } - return record, nil -} - func firewallTaskName(operation, subsystem, backend string) string { name := i18n.GetMsgByKey(subsystem) if backend != "" { @@ -2146,6 +1016,143 @@ func closeUnstartedFirewallTask(t *task.Task) { } } +func loadFirewallRuleState(ctx context.Context, runtime filter.Adapter, state map[string]filter.RuleSet) error { + for _, group := range firewallScopeReadGroups(filter.ManagedInputScopes(runtime.Provider())) { + snapshots, err := listFirewallRuleScopes(runtime, ctx, group) + if errors.Is(err, filter.ErrFamilyUnavailable) { + continue + } + if err != nil { + return err + } + for _, snapshot := range snapshots { + missing := false + for _, notice := range snapshot.Notices { + missing = missing || notice.Code == filter.ScopeNoticeManagedScopeMissing || notice.Code == filter.ScopeNoticeManagedScopeInactive + } + if !missing { + state[snapshot.Scope.Key()] = snapshot + } + } + } + return nil +} + +func firewallRuleDuplicateKey(observed filter.ObservedRule, converted bool) (string, error) { + if converted && observed.ParseStatus == filter.ParseStatusSupported && observed.Rule.Scope.Provider == filter.ProviderFirewalld { + priority := 0 + observed.Rule.Priority = &priority + observed.Rule.NativeKind, observed.Rule.OrderBucket = filter.NativeKindRichRule, "" + } + key, err := filter.DescriptionID(observed) + if converted { + key = "converted:" + key + } + return key, err +} + +func (s *FirewallService) syncPortWhitelist(ctx context.Context, ruleState map[string]filter.RuleSet) error { + firewallWhitelistMu.Lock() + defer firewallWhitelistMu.Unlock() + ports, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + return s.applyPortWhitelist(ctx, ports, ruleState) +} + +func (s *FirewallService) applyPortWhitelist(ctx context.Context, ports []firewall.PortWhitelist, ruleState map[string]filter.RuleSet, families ...filter.Family) error { + required, err := firewall.RequiredPortWhitelist(ports) + if err != nil { + return err + } + provider, err := s.selectedProvider(ctx) + if err != nil { + return err + } + direct := provider == filter.ProviderIptables || provider == filter.ProviderNftables + if !direct { + client, err := s.baseClient() + if err != nil { + return err + } + active, err := client.Status() + if err != nil || !active { + return err + } + } + runtime, err := s.firewallAdapter(provider) + if err != nil { + return err + } + firewallRuleMutationMu.Lock() + defer firewallRuleMutationMu.Unlock() + if ruleState == nil { + ruleState = make(map[string]filter.RuleSet) + if err := loadFirewallRuleState(ctx, runtime, ruleState); err != nil { + return err + } + } + keys := make(map[string]bool) + for _, snapshot := range ruleState { + for _, observed := range snapshot.Rules { + if observed.ParseStatus != filter.ParseStatusSupported { + continue + } + families := []filter.Family{observed.Rule.Scope.Family} + if families[0] == filter.FamilyInet { + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } + for _, family := range families { + rule := observed.Rule + rule.Scope.Family = family + key, err := filter.RuleMatchKey(rule) + if err == nil { + keys[string(rule.Action)+key] = true + } + } + } + } + rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required)) + var changes []firewallRuleBatchItem + for _, rule := range rules { + if len(families) > 0 && !slices.Contains(families, rule.Scope.Family) { + continue + } + item, err := prepareFirewallCreateRule(ctx, runtime, dto.FirewallRuleCreateItem{Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity}) + if err != nil { + return err + } + rule = item.Rule + snapshot, available := ruleState[rule.Scope.Key()] + if !available { + continue + } + key, err := filter.RuleMatchKey(rule) + if err != nil { + return err + } + key = string(rule.Action) + key + if keys[key] { + continue + } + keys[key] = true + rule.UUID = uuid.NewString() + if provider != filter.ProviderFirewalld { + position := int64(1) + rule.OrderIndex = &position + } + changes = append(changes, firewallRuleBatchItem{snapshot: snapshot, change: filter.RuleChange{Operation: filter.ChangeCreate, After: &rule}}) + } + var failures []error + for _, err := range executeFirewallRuleBatches(ctx, runtime, changes) { + if err != nil && !errors.Is(err, filterfirewalld.ErrAlreadyEnabled) && !errors.Is(err, filterufw.ErrAlreadyEnabled) { + failures = append(failures, err) + } + } + return errors.Join(failures...) +} + func whitelistRules(provider filter.Provider, ports, required []firewall.SystemPort) []filter.FirewallRule { rules := make([]filter.FirewallRule, 0, len(ports)+len(required)) for _, port := range required { @@ -2171,615 +1178,12 @@ func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist return result } -func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (filter.Adapter, []*firewallSyncRule, []filter.RuleSet, error) { - if request.SourceProvider != "" || request.ResetSource { - return nil, nil, nil, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule) +func loadForwardingFirewallOverview(manager forwarding.Adapter, families []string) (dto.FirewallSubsystemStatus, error) { + ipv4, ipv4Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv4) + if len(families) == 1 { + return dto.FirewallSubsystemStatus{IPv6Enabled: false, IsInit: ipv4.Initialized, IsBind: ipv4.Bound, IPv4: ipv4}, ipv4Err } - if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil { - return nil, nil, nil, err - } - runtime, err := s.firewallAdapter(request.TargetProvider) - if err != nil { - return nil, nil, nil, err - } - stored, err := s.rules.List(ctx) - if err != nil { - return nil, nil, nil, err - } - sortFirewallRules(stored, request.TargetProvider) - ports, err := loadFirewallPortWhiteList() - if err != nil { - return nil, nil, nil, err - } - required, err := firewall.RequiredPortWhitelist(ports) - if err != nil { - return nil, nil, nil, err - } - whitelistDesired := whitelistRules(request.TargetProvider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required)) - rules := make([]*firewallSyncRule, 0, len(stored)) - preservedMarkers := make(map[string]bool) - compileFailed := false - whitelist := filter.NewPortWhitelistIndex(ports) - for _, record := range stored { - desired, preserved, err := s.compileRestorableFirewallRules(ctx, record, runtime, required) - if err != nil { - compileFailed = true - rules = append(rules, &firewallSyncRule{FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Status: firewallsync.StatusBlocked, Reason: err.Error()}}) - continue - } - for _, rule := range preserved { - preservedMarkers[rule.Marker] = true - } - for _, native := range desired { - rule := native.Rule - native.Protected = whitelist.Matches(rule) - rules = append(rules, &firewallSyncRule{desired: native, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Rule: &rule}}) - } - } - identities := make(firewallRuleCollisionIndex, len(rules)) - for _, existing := range rules { - if existing.Rule != nil { - if err := identities.Add(*existing.Rule); err != nil { - return nil, nil, nil, err - } - } - } - for _, candidate := range whitelistDesired { - prepared, err := prepareFirewallCreateRule(ctx, runtime, dto.FirewallRuleCreateItem{Rule: candidate}) - if err != nil { - return nil, nil, nil, err - } - rule := prepared.Rule - if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) { - continue - } else if err != nil { - return nil, nil, nil, err - } - if err := identities.Add(rule); err != nil { - return nil, nil, nil, err - } - key, err := filter.RuleKey(rule) - if err != nil { - return nil, nil, nil, err - } - rule.UUID = uuid.NewSHA1(uuid.NameSpaceOID, []byte(key)).String() - rules = append(rules, &firewallSyncRule{ - desired: filter.DesiredRule{UUID: rule.UUID, Rule: rule, RuleKey: key, Origin: filter.RuleOriginCreated, Protected: true}, - FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: rule.UUID, Rule: &rule}, - }) - } - scopes := filter.ManagedInputScopes(request.TargetProvider) - if compileFailed { - scopes = slices.DeleteFunc(scopes, func(scope filter.Scope) bool { - return !slices.ContainsFunc(rules, func(rule *firewallSyncRule) bool { - return rule.Rule != nil && rule.Rule.Scope.Key() == scope.Key() - }) - }) - } - snapshots := make([]filter.RuleSet, 0, len(scopes)) - for _, group := range firewallScopeReadGroups(scopes) { - current, err := readMutableFirewallRuleScopes(runtime, ctx, group) - if errors.Is(err, filter.ErrFamilyUnavailable) { - for _, rule := range rules { - if rule.Rule != nil && slices.ContainsFunc(group, func(scope filter.Scope) bool { return scope.Key() == rule.Rule.Scope.Key() }) { - rule.Status, rule.Reason = firewallsync.StatusBlocked, err.Error() - } - } - continue - } - if err != nil { - return nil, nil, nil, err - } - snapshots = append(snapshots, current...) - } - for _, snapshot := range snapshots { - scope := snapshot.Scope - desired := make([]filter.DesiredRule, 0) - byUUID := make(map[string]*firewallSyncRule) - for _, rule := range rules { - if rule.Rule != nil && rule.Rule.Scope.Key() == scope.Key() { - desired = append(desired, rule.desired) - byUUID[rule.desired.Rule.UUID] = rule - } - } - inventory, err := mergeFirewallInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) - if err != nil { - return nil, nil, nil, err - } - for _, item := range inventory { - if item.Desired == nil { - if compileFailed || item.Observed == nil || !strings.HasPrefix(item.Observed.Marker, "1panel-rule:") || preservedMarkers[item.Observed.Marker] { - continue - } - observed := item.Observed - if observed.Rule.Scope.Chain == filter.BasicBeforeChain { - continue - } - rule := &firewallSyncRule{observed: observed, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{ - SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &observed.Rule, Status: firewallsync.StatusRemove, - ReasonCode: firewallsync.ReasonManagedOnlyInTarget, Reason: firewallSyncReasonMessage(firewallsync.ReasonManagedOnlyInTarget), - }} - if observed.Protected || observed.ParseStatus == filter.ParseStatusOpaque { - rule.Status, rule.ReasonCode = firewallsync.StatusBlocked, firewallsync.ReasonUnsafeRemoval - rule.Reason = firewallSyncReasonMessage(rule.ReasonCode) - } - rules = append(rules, rule) - continue - } - rule := byUUID[item.Desired.Rule.UUID] - rule.observed = item.Observed - if item.Match == filter.InventoryMatchExact && item.Observed != nil && scope.Provider == filter.ProviderFirewalld { - rule.Rule.Priority = item.Observed.Rule.Priority - rule.Rule.NativeKind = item.Observed.Rule.NativeKind - rule.Rule.OrderBucket = item.Observed.Rule.OrderBucket - rule.desired.Rule = *rule.Rule - rule.desired.RuleKey, err = filter.RuleKey(*rule.Rule) - if err != nil { - return nil, nil, nil, err - } - } - divergent := item.Observed != nil && item.Observed.Persistence != "" && item.Observed.Persistence != filter.PersistenceStatusConverged - switch { - case item.Match == filter.InventoryMatchExact && !divergent: - rule.Status, rule.Reason = firewallsync.StatusExisting, "rule already matches database policy" - case item.Match == filter.InventoryMatchMissing || item.Match == filter.InventoryMatchChanged || item.Match == filter.InventoryMatchExact: - rule.Status, rule.Reason = firewallsync.StatusReady, "target rule differs from database policy" - if item.Observed != nil && item.Observed.Protected { - rule.Status, rule.Reason = firewallsync.StatusBlocked, filter.ErrProtectedRule.Error() - } - default: - rule.Status, rule.Reason = firewallsync.StatusBlocked, fmt.Sprintf("target rule cannot be synchronized: %s", item.Match) - } - } - } - return runtime, rules, snapshots, nil -} - -func (s *FirewallService) compileRestorableFirewallRules(ctx context.Context, stored model.FirewallRule, client filter.Adapter, required []firewall.PortWhitelist) (restorable, preserved []filter.DesiredRule, err error) { - provider := client.Provider() - compiled, err := compileStoredFirewallRules(ctx, stored, client) - if err != nil { - return nil, nil, err - } - if (provider != filter.ProviderIptables && provider != filter.ProviderNftables) || !strings.HasPrefix(stored.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) { - return compiled, nil, nil - } - requiredPorts := firewall.ExpandPortWhitelist(required) - for _, desired := range compiled { - covered := false - for _, port := range requiredPorts { - covered, err = filter.SameRuleContent(desired.Rule, firewall.RuleForSystemPort(provider, firewall.SystemPort(port))) - if err != nil { - return nil, nil, err - } - if covered { - break - } - } - if covered { - preserved = append(preserved, desired) - } else { - restorable = append(restorable, desired) - } - } - return restorable, preserved, nil -} - -func (s *FirewallService) removeTransferredSystemPortRules(ctx context.Context, provider filter.Provider, ports []firewall.PortWhitelist) error { - if provider != filter.ProviderIptables && provider != filter.ProviderNftables { - return nil - } - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - required, err := firewall.RequiredPortWhitelist(ports) - if err != nil { - return err - } - runtime, err := s.firewallAdapter(provider) - if err != nil { - return err - } - stored, err := s.rules.List(ctx) - if err != nil { - return err - } - custom := filter.NewPortWhitelistIndex(customWhitelist(ports)) - candidates := make([]model.FirewallRule, 0) - keysByUUID := make(map[string][]string) - scopes := make([]filter.Scope, 0) - for _, record := range stored { - if record.Origin != constant.FirewallRuleOriginCreated || !strings.HasPrefix(record.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) { - continue - } - restorable, preserved, err := s.compileRestorableFirewallRules(ctx, record, runtime, required) - if isFirewallPolicyIncompatible(err) { - continue - } - if err != nil { - return err - } - if len(restorable) != 0 || len(preserved) == 0 || slices.ContainsFunc(preserved, func(rule filter.DesiredRule) bool { return custom.Matches(rule.Rule) }) { - continue - } - for _, desired := range preserved { - rule := desired.Rule - rule.Scope.Chain = filter.BasicBeforeChain - key, err := filter.RuleMatchKey(rule) - if err != nil { - return err - } - keysByUUID[record.UUID] = append(keysByUUID[record.UUID], key) - scopes = append(scopes, rule.Scope) - } - candidates = append(candidates, record) - } - present := make(map[string]bool) - for _, group := range firewallScopeReadGroups(scopes) { - snapshots, err := readMutableFirewallRuleScopes(runtime, ctx, group) - if errors.Is(err, filter.ErrFamilyUnavailable) { - continue - } - if err != nil { - return err - } - for _, snapshot := range snapshots { - for _, observed := range snapshot.Rules { - if observed.ParseStatus != filter.ParseStatusSupported || observed.Rule.Action != filter.ActionAccept || (observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged) { - continue - } - key, err := filter.RuleMatchKey(observed.Rule) - if err != nil { - return err - } - present[key] = true - } - } - } - candidates = slices.DeleteFunc(candidates, func(record model.FirewallRule) bool { - return slices.ContainsFunc(keysByUUID[record.UUID], func(key string) bool { return !present[key] }) - }) - var failures []error - for ruleUUID, err := range s.rules.DeleteBatchWithRevision(ctx, candidates) { - failures = append(failures, fmt.Errorf("remove transferred system port rule %s: %w", ruleUUID, err)) - } - return errors.Join(failures...) -} - -func firewallInventoryRuleKey(rule filter.FirewallRule) (string, error) { - if rule.Scope.Provider == filter.ProviderFirewalld { - key, err := filter.RuleMatchKey(rule) - return key + ":" + string(rule.Action), err - } - return filter.RuleKey(rule) -} - -func mergeFirewallInventory(input filter.InventoryMergeInput) ([]filter.InventoryItem, error) { - candidates := make([]observedInventoryCandidate, len(input.Observed)) - byRuleKey := make(map[string][]int) - byInstanceKey := make(map[string][]int) - byMarker := make(map[string][]int) - bySemanticKey := make(map[string][]int) - byPartialRuleKey := make(map[string][]int) - for index, observed := range input.Observed { - candidate := observedInventoryCandidate{rule: observed} - if marker := strings.TrimSpace(candidate.rule.Marker); marker != "" { - markerKey := candidate.rule.Rule.Scope.Key() + "\x00" + marker - byMarker[markerKey] = append(byMarker[markerKey], index) - } - if observed.ParseStatus == filter.ParseStatusSupported { - normalized, err := filter.NormalizeRule(observed.Rule) - if err != nil { - return nil, fmt.Errorf("normalize observed firewall rule %d: %w", index, err) - } - candidate.rule.Rule = normalized - candidate.ruleKey, err = filter.RuleKey(normalized) - if err != nil { - return nil, err - } - key, err := firewallInventoryRuleKey(normalized) - if err != nil { - return nil, err - } - byRuleKey[key] = append(byRuleKey[key], index) - if instanceKey, err := filter.InstanceKey(candidate.rule); err == nil { - candidate.instanceKey = instanceKey - candidate.rule.InstanceKey = instanceKey - byInstanceKey[instanceKey] = append(byInstanceKey[instanceKey], index) - } - } - if observed.ParseStatus != filter.ParseStatusOpaque { - rule := candidate.rule.Rule - protocolUnknown, supportedFields := false, true - for _, field := range observed.UncertainFields { - if field != filter.ObservedFieldProtocol { - supportedFields = false - break - } - protocolUnknown = true - } - if supportedFields { - key := candidate.ruleKey - var err error - if protocolUnknown { - rule.Protocol = "tcp" - key, err = filter.RuleKey(rule) - } else if key == "" { - key, err = filter.RuleKey(rule) - } - if err == nil { - key += "\x00" + strings.TrimSpace(observed.Marker) - if protocolUnknown { - key = "protocol\x00" + key - byPartialRuleKey[key] = append(byPartialRuleKey[key], index) - } else if observed.ParseStatus != filter.ParseStatusSupported { - key = "exact\x00" + key - byPartialRuleKey[key] = append(byPartialRuleKey[key], index) - } else { - bySemanticKey[key] = append(bySemanticKey[key], index) - } - } - } - } - candidates[index] = candidate - } - - normalizedDesired := make([]filter.DesiredRule, 0, len(input.Desired)) - desiredMatches := make(map[int]int) - desiredMatchStates := make([]filter.InventoryMatch, 0, len(input.Desired)) - for _, desired := range input.Desired { - normalized, err := filter.NormalizeRule(desired.Rule) - if err != nil { - return nil, fmt.Errorf("normalize desired firewall rule %q: %w", desired.UUID, err) - } - desired.Rule = normalized - calculatedKey, err := filter.RuleKey(normalized) - if err != nil { - return nil, err - } - if desired.RuleKey != "" && desired.RuleKey != calculatedKey { - return nil, fmt.Errorf("%w: desired rule %q key does not match its semantics", filter.ErrInvalidRule, desired.UUID) - } - desired.RuleKey = calculatedKey - - match, matchState := findObservedInventoryMatch(desired, candidates, byRuleKey, byInstanceKey, byMarker, bySemanticKey, byPartialRuleKey) - normalizedIndex := len(normalizedDesired) - normalizedDesired = append(normalizedDesired, desired) - desiredMatchStates = append(desiredMatchStates, matchState) - if match >= 0 { - candidates[match].claimed = true - desiredMatches[match] = normalizedIndex - } - } - - items := make([]filter.InventoryItem, 0, len(candidates)+len(normalizedDesired)) - matchedDesired := make(map[int]struct{}, len(desiredMatches)) - for index := range candidates { - candidate := &candidates[index] - if desiredIndex, exists := desiredMatches[index]; exists { - desired := normalizedDesired[desiredIndex] - observed := candidate.rule - match := desiredMatchStates[desiredIndex] - if match == filter.InventoryMatchExact && observed.ParseStatus != filter.ParseStatusSupported { - orderIndex := observed.Rule.OrderIndex - observed.Rule = desired.Rule - observed.Rule.OrderIndex = orderIndex - observed.ParseStatus = filter.ParseStatusSupported - observed.UncertainFields = nil - } - displayRule := observed.Rule - displayRule.Description = desired.Rule.Description - state := inventoryStateForDesired(desired, match) - if observed.Protected { - state = filter.InventoryStateProtected - } else if observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged { - state = filter.InventoryStateDrifted - } - items = append(items, filter.InventoryItem{ - Rule: displayRule, - Observed: &observed, - Desired: &desired, - State: state, - Match: match, - }) - matchedDesired[desiredIndex] = struct{}{} - continue - } - observed := candidate.rule - state := filter.InventoryStateExternal - if observed.Protected { - state = filter.InventoryStateProtected - } else if _, protected := input.ProtectedObservedKeys[candidate.ruleKey]; protected { - state = filter.InventoryStateProtected - } - match := filter.InventoryMatchNone - if observed.ParseStatus != filter.ParseStatusSupported { - match = filter.InventoryMatchOpaque - } - items = append(items, filter.InventoryItem{Rule: observed.Rule, Observed: &observed, State: state, Match: match}) - } - for index, desired := range normalizedDesired { - if _, matched := matchedDesired[index]; matched { - continue - } - desiredCopy := desired - match := desiredMatchStates[index] - items = append(items, filter.InventoryItem{ - Rule: desired.Rule, - Desired: &desiredCopy, - State: inventoryStateForDesired(desired, match), - Match: match, - }) - } - return items, nil -} - -func findObservedInventoryMatch(desired filter.DesiredRule, candidates []observedInventoryCandidate, byRuleKey, byInstanceKey, byMarker, bySemanticKey, byPartialRuleKey map[string][]int) (int, filter.InventoryMatch) { - if marker := strings.TrimSpace(desired.Marker); marker != "" { - match, status := matchUnclaimedSemanticCandidate(desired, marker, candidates, bySemanticKey, byPartialRuleKey) - if status != filter.InventoryMatchMissing { - return match, status - } - markerKey := desired.Rule.Scope.Key() + "\x00" + marker - match, status = uniqueUnclaimedCandidate(byMarker, markerKey, candidates) - if match >= 0 && candidates[match].rule.ParseStatus != filter.ParseStatusOpaque && - !filter.ObservedRuleMatchesExpected(candidates[match].rule, desired.Rule) { - return match, filter.InventoryMatchChanged - } - if status != filter.InventoryMatchMissing { - return match, status - } - if desired.Origin == filter.RuleOriginAdopted { - match, status = matchUnclaimedSemanticCandidate(desired, "", candidates, bySemanticKey, byPartialRuleKey) - if status != filter.InventoryMatchMissing { - if match >= 0 { - return match, filter.InventoryMatchChanged - } - return match, status - } - } - legacyMarker := "1panel-rule:" + strings.TrimSpace(desired.UUID) - if legacyMarker != "1panel-rule:" && legacyMarker != marker { - match, status = matchUnclaimedSemanticCandidate(desired, legacyMarker, candidates, bySemanticKey, byPartialRuleKey) - if status != filter.InventoryMatchMissing { - if match >= 0 { - return match, filter.InventoryMatchChanged - } - return match, status - } - } - return match, status - } - var match int - if desired.ObservedInstanceKey != "" { - match = firstUnclaimedCandidate(byInstanceKey, desired.ObservedInstanceKey, candidates) - } else { - key, err := firewallInventoryRuleKey(desired.Rule) - if err != nil { - return -1, filter.InventoryMatchMissing - } - match = firstUnclaimedCandidate(byRuleKey, key, candidates) - } - if match < 0 { - return -1, filter.InventoryMatchMissing - } - return match, filter.InventoryMatchExact -} - -func firstUnclaimedCandidate(byKey map[string][]int, key string, candidates []observedInventoryCandidate) int { - indices := byKey[key] - for len(indices) > 0 && candidates[indices[0]].claimed { - indices = indices[1:] - } - if len(indices) == 0 { - delete(byKey, key) - return -1 - } - byKey[key] = indices - return indices[0] -} - -func uniqueUnclaimedCandidate(byKey map[string][]int, key string, candidates []observedInventoryCandidate) (int, filter.InventoryMatch) { - match := firstUnclaimedCandidate(byKey, key, candidates) - if match < 0 { - return -1, filter.InventoryMatchMissing - } - for _, index := range byKey[key][1:] { - if !candidates[index].claimed { - return -1, filter.InventoryMatchAmbiguous - } - } - return match, filter.InventoryMatchExact -} - -func matchUnclaimedSemanticCandidate(desired filter.DesiredRule, marker string, candidates []observedInventoryCandidate, bySemanticKey, byPartialRuleKey map[string][]int) (int, filter.InventoryMatch) { - key := desired.RuleKey + "\x00" + marker - if match := firstUnclaimedCandidate(bySemanticKey, key, candidates); match >= 0 { - return match, filter.InventoryMatchExact - } - if len(byPartialRuleKey) == 0 { - return -1, filter.InventoryMatchMissing - } - match, status := uniqueUnclaimedCandidate(byPartialRuleKey, "exact\x00"+key, candidates) - if status == filter.InventoryMatchAmbiguous { - return match, status - } - protocolIndependent := desired.Rule - protocolIndependent.Protocol = "tcp" - partialKey, err := filter.RuleKey(protocolIndependent) - if err != nil { - return match, status - } - partial, partialStatus := uniqueUnclaimedCandidate(byPartialRuleKey, "protocol\x00"+partialKey+"\x00"+marker, candidates) - if partialStatus == filter.InventoryMatchAmbiguous || (match >= 0 && partial >= 0) { - return -1, filter.InventoryMatchAmbiguous - } - if match >= 0 { - return match, status - } - return partial, partialStatus -} - -func inventoryStateForDesired(desired filter.DesiredRule, match filter.InventoryMatch) filter.InventoryState { - if match != filter.InventoryMatchExact { - return filter.InventoryStateDrifted - } - if desired.Protected { - return filter.InventoryStateProtected - } - switch desired.Origin { - case filter.RuleOriginAdopted: - return filter.InventoryStateAdopted - default: - return filter.InventoryStateManaged - } -} - -func firewallSyncReasonMessage(code firewallsync.ReasonCode) string { - switch code { - case firewallsync.ReasonAlreadyExists: - return "rule already exists in target backend" - case firewallsync.ReasonOnlyExistsInTarget: - return "rule exists only in target backend" - case firewallsync.ReasonManagedOnlyInTarget: - return "managed rule exists only in target backend" - case firewallsync.ReasonUnsafeRemoval: - return "managed runtime rule cannot be safely removed" - case firewallsync.ReasonReadOnlyRule: - return "read-only runtime rule is preserved but cannot be synchronized" - default: - return "" - } -} - -func appendDatabaseSyncFailure(result *dto.FirewallRuleSyncResult, item dto.FirewallRuleSyncItem, err error) { - if err == nil { - err = errors.New("database synchronization failed") - } - result.Failed++ - result.Errors = append(result.Errors, dto.FirewallRuleSyncFailure{ - SourceUUID: item.SourceUUID, - Rule: item.Rule, ForwardRule: item.ForwardRule, DockerRule: item.DockerRule, - Error: err.Error(), - }) -} - -func firewallDeleteChange(current filter.ObservedRule, desired filter.DesiredRule) (filter.RuleChange, error) { - if err := filter.GuardMutation(current); err != nil { - return filter.RuleChange{}, err - } - before := current.Rule - if before.UUID == "" && strings.HasPrefix(current.Marker, "1panel-rule:") { - before.UUID = strings.TrimSpace(strings.TrimPrefix(current.Marker, "1panel-rule:")) - } - if before.UUID == "" { - before.UUID = desired.Rule.UUID - } - return filter.RuleChange{Operation: filter.ChangeDelete, Before: &before, Locator: ¤t.Locator, UnmarkedAdopted: current.Marker == "" && desired.Origin == filter.RuleOriginAdopted}, nil -} - -func loadForwardingFirewallOverview(manager forwarding.Adapter) (dto.FirewallSubsystemStatus, error) { - ipv4, ipv4Err := loadForwardingFamilyInfo(manager, manager.Name(), constant.FirewallFamilyIPv4) - ipv6, ipv6Err := loadForwardingFamilyInfo(manager, manager.Name(), constant.FirewallFamilyIPv6) + ipv6, ipv6Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv6) if ipv6.Available { interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile) var pathError *os.PathError @@ -2796,88 +1200,326 @@ func loadForwardingFirewallOverview(manager forwarding.Adapter) (dto.FirewallSub ipv6.Reason = "ipv6_forwarding_not_enabled" } } - return dto.FirewallSubsystemStatus{IsInit: ipv4.Initialized || ipv6.Initialized, IsBind: ipv4.Bound || ipv6.Bound, IPv4: ipv4, IPv6: ipv6}, errors.Join(ipv4Err, ipv6Err) + return dto.FirewallSubsystemStatus{IPv6Enabled: len(families) > 1, IsInit: ipv4.Initialized || ipv6.Initialized, IsBind: ipv4.Bound || ipv6.Bound, IPv4: ipv4, IPv6: ipv6}, errors.Join(ipv4Err, ipv6Err) } -func loadForwardingFamilyInfo(manager forwarding.Adapter, backend, family string) (dto.FirewallBackendFamilyStatus, error) { - initialized, bound, err := manager.FamilyStatus(family) +func loadForwardingFamilyInfo(manager forwarding.Adapter, family string) (dto.FirewallBackendFamilyStatus, error) { + initialized, bound, partial, err := manager.FamilyState(family) + if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { + return dto.FirewallBackendFamilyStatus{}, nil + } available := err == nil - if backend == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 { + if manager.Name() == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 { commands, commandErr := lifecycle.ResolveIptablesCommands() available = available && commandErr == nil && commands.IPv6Available() } - return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound}, err + return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound, Partial: partial}, err } -func (s *ForwardingService) forwardingEnabled() (bool, error) { - if s.enabled != nil { - return s.enabled() - } - status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey) - return status == constant.StatusEnable, err -} - -func (s *ForwardingService) initializeForwarding(manager forwarding.Adapter) error { - if err := s.saveForwardingBackend(manager.Name()); err != nil { +func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate, t *task.Task, initialize bool, families ...string) (resultErr error) { + defer func() { resultErr = errors.Join(resultErr, ctx.Err()) }() + forwardingMutationMu.Lock() + defer forwardingMutationMu.Unlock() + if err := ctx.Err(); err != nil { return err } - return manager.Enable() -} - -func (s *ForwardingService) saveForwardingBackend(backend string) error { - if s.persistBackend != nil { - return s.persistBackend(backend) + var allowed []string + if initialize || request.Import { + var err error + allowed, err = loadFirewallFamilies() + if err != nil { + return err + } } - return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend) + skippedIPv6 := 0 + type operationBatch struct { + operation forwarding.OperationType + rules []forwarding.Rule + } + groups := make([]operationBatch, 0) + for _, operation := range request.Rules { + kind := forwarding.OperationType(operation.Operation) + if kind != forwarding.OperationAdd && kind != forwarding.OperationRemove { + return fmt.Errorf("unsupported forwarding operation %q", operation.Operation) + } + if len(groups) == 0 || groups[len(groups)-1].operation != kind { + groups = append(groups, operationBatch{operation: kind}) + } + for _, protocol := range strings.Split(operation.Protocol, "/") { + rule, err := forwarding.NormalizeRule(forwarding.Rule{ + Family: operation.Family, Protocol: protocol, Port: operation.Port, + TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface, + }) + if err != nil { + return err + } + if len(allowed) > 0 && rule.Family == forwarding.FamilyIPv6 && !slices.Contains(allowed, forwarding.FamilyIPv6) { + skippedIPv6++ + continue + } + groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule) + } + } + logFirewallIPv6Skipped(t, "forwarding", skippedIPv6) + groups = slices.DeleteFunc(groups, func(group operationBatch) bool { return len(group.rules) == 0 }) + if !initialize && len(groups) == 0 && skippedIPv6 > 0 { + return nil + } + manager, err := s.clientFactory(ctx) + if err != nil { + return err + } + if initialize { + if err := initializeForwarding(manager, allowed); err != nil { + return err + } + if !slices.Contains(allowed, forwarding.FamilyIPv6) && t != nil { + t.Logf("IPv6 firewall support is disabled; skipping IPv6 forwarding chain initialization") + } + for _, family := range families { + if !slices.Contains(allowed, family) { + continue + } + if err := manager.OperateFamily(family, true); err != nil { + return err + } + } + } + initialized, _, err := manager.InitStatus() + if err != nil { + return err + } + if !initialized { + return filter.ErrProviderUnavailable + } + checkedFamilies := make(map[string]bool) + for _, group := range groups { + for _, rule := range group.rules { + if checkedFamilies[rule.Family] { + continue + } + checkedFamilies[rule.Family] = true + ready, _, err := manager.FamilyStatus(rule.Family) + if err != nil { + return err + } + if !ready { + if !initialize { + return fmt.Errorf("%s forwarding chains are not initialized", rule.Family) + } + if err := manager.OperateFamily(rule.Family, true); err != nil { + return err + } + } + } + } + stored, err := manager.List() + if err != nil { + return err + } + stored = slices.Clone(stored) + counts := make(map[string]int, len(stored)) + for index, rule := range stored { + normalized, err := forwarding.NormalizeRule(rule) + if err != nil { + return err + } + stored[index] = normalized + counts[normalized.Identity()]++ + } + succeeded, failed, skipped := 0, 0, 0 + defer func() { + if t != nil { + t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{"succeeded": succeeded, "failed": failed})) + if skipped > 0 { + t.Logf("%s: %d", i18n.GetMsgByKey("FirewallCreateRuleSkipped"), skipped) + } + } + }() + record := func(operation forwarding.OperationType, rule forwarding.Rule, status string, cause error) { + label := fmt.Sprintf("%s %s %s %s -> %s:%s", operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort) + switch status { + case "skipped": + skipped++ + if t != nil { + t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause) + } + case "failed": + failed++ + if t != nil { + t.LogFailedWithErr(label, cause) + } + default: + succeeded++ + if t != nil { + t.LogSuccess(label) + } + } + } + var failures []error + var interfaces map[string]bool + for _, group := range groups { + if group.operation != forwarding.OperationAdd { + continue + } + for _, rule := range group.rules { + if rule.Interface == "" { + continue + } + if counts[rule.Identity()] > 0 { + continue + } + if interfaces == nil { + available, err := net.Interfaces() + if err != nil { + return fmt.Errorf("load forwarding interfaces: %w", err) + } + interfaces = make(map[string]bool, len(available)) + for _, item := range available { + interfaces[item.Name] = true + } + } + if !interfaces[rule.Interface] { + err := buserr.WithMap("ErrForwardInterfaceNotFound", map[string]interface{}{"name": rule.Interface}, nil) + failures = append(failures, err) + record(group.operation, rule, "failed", err) + } + } + } + if len(failures) > 0 { + return errors.Join(failures...) + } + isEdit := len(request.Rules) == 2 && len(groups) == 2 && groups[0].operation == forwarding.OperationRemove && groups[1].operation == forwarding.OperationAdd + if isEdit { + old := make(map[string]bool, len(groups[0].rules)) + for _, rule := range groups[0].rules { + old[rule.Identity()] = true + } + unchanged := len(old) == len(groups[1].rules) + duplicate := false + next := make(map[string]bool, len(groups[1].rules)) + for _, rule := range groups[1].rules { + key := rule.Identity() + next[key] = true + unchanged = unchanged && old[key] + if counts[key] > 0 && !old[key] { + duplicate = true + } + } + if unchanged || duplicate { + for _, group := range groups { + for _, rule := range group.rules { + record(group.operation, rule, "skipped", buserr.New("ErrRecordExist")) + } + } + return nil + } + for _, rule := range groups[0].rules { + if counts[rule.Identity()] == 0 { + record(forwarding.OperationRemove, rule, "failed", filter.ErrRuleStale) + return filter.ErrRuleStale + } + } + removals := make([]forwarding.Rule, 0, len(groups[0].rules)) + for _, rule := range groups[0].rules { + if !next[rule.Identity()] { + removals = append(removals, rule) + } + } + groups[0], groups[1] = groups[1], operationBatch{operation: forwarding.OperationRemove, rules: removals} + } + for _, group := range groups { + byFamily := make(map[string][]forwarding.Rule, 2) + seen := make(map[string]bool, len(group.rules)) + for _, rule := range group.rules { + key := rule.Identity() + exists := counts[key] > 0 + if seen[key] || (group.operation == forwarding.OperationAdd && exists) { + record(group.operation, rule, "skipped", buserr.New("ErrRecordExist")) + continue + } + if group.operation == forwarding.OperationRemove && !exists { + failures = append(failures, filter.ErrRuleStale) + record(group.operation, rule, "failed", filter.ErrRuleStale) + continue + } + seen[key] = true + copies := 1 + if group.operation == forwarding.OperationRemove && manager.Name() == constant.FirewallProviderIptables { + copies = counts[key] + } + for i := 0; i < copies; i++ { + byFamily[rule.Family] = append(byFamily[rule.Family], rule) + } + } + for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { + rules := byFamily[family] + if len(rules) == 0 { + continue + } + err := ctx.Err() + + if err == nil { + if group.operation == forwarding.OperationAdd { + err = manager.CreateRules(ctx, rules) + } else { + err = manager.DeleteRules(ctx, rules) + } + } + if err != nil { + failures = append(failures, err) + for _, rule := range rules { + record(group.operation, rule, "failed", err) + } + continue + } + for _, rule := range rules { + if group.operation == forwarding.OperationAdd { + counts[rule.Identity()]++ + stored = append(stored, rule) + } else { + delete(counts, rule.Identity()) + stored = slices.DeleteFunc(stored, func(item forwarding.Rule) bool { return item.Identity() == rule.Identity() }) + } + record(group.operation, rule, "succeeded", nil) + } + } + if len(failures) > 0 && (isEdit || group.operation == forwarding.OperationRemove) { + break + } + } + return errors.Join(errors.Join(failures...), persistForwardingRules(manager, stored)) } -func (s *ForwardingService) persistForwardingEnabled() error { - if s.markEnabled != nil { - return s.markEnabled() +func initializeForwarding(manager forwarding.Adapter, families []string) error { + if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, manager.Name()); err != nil { + return err + } + if !slices.Contains(families, forwarding.FamilyIPv6) { + global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 forwarding chain initialization") + } + if err := manager.Enable(families...); err != nil { + return err } return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable) } -func recordForwardingSyncError(err error) { - forwardingSyncStateMu.Lock() - forwardingLastSyncErr = err - forwardingSyncStateMu.Unlock() -} - -func forwardingRulesFromModels(stored []model.ForwardingRule) []forwarding.Rule { - rules := make([]forwarding.Rule, 0, len(stored)) - for _, rule := range stored { - rules = append(rules, forwarding.Rule{ - Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, - TargetPort: rule.TargetPort, Interface: rule.Interface, - }) - } - return rules -} - func newForwardingService() *ForwardingService { return &ForwardingService{ clientFactory: newForwardingAdapter, - rules: forwardingRuleRepo, - markEnabled: func() error { - return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable) - }, - persistBackend: func(backend string) error { - return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend) - }, } } -func newForwardingAdapter() (forwarding.Adapter, error) { +func newForwardingAdapter(ctx context.Context) (forwarding.Adapter, error) { selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey) selected = strings.TrimSpace(selected) if selected == "" { selected = constant.FirewallProviderIptables } - return newForwardingAdapterFor(selected) + return newForwardingAdapterFor(ctx, selected) } -func newForwardingAdapterFor(backend string) (forwarding.Adapter, error) { +func newForwardingAdapterFor(ctx context.Context, backend string) (forwarding.Adapter, error) { client, err := lifecycle.NewClient(backend) if err != nil { return nil, fmt.Errorf( @@ -2887,75 +1529,14 @@ func newForwardingAdapterFor(backend string) (forwarding.Adapter, error) { } switch client.Name() { case constant.FirewallProviderIptables: - return forwarding.NewIptables(client.Name()), nil + return forwarding.NewIptables(ctx, client.Name()), nil case constant.FirewallProviderNftables: - return forwarding.NewNftables(), nil + return forwarding.NewNftables(ctx), nil default: return nil, errForwardingBackendUnavailable } } -func ReconcileDockerPortGuard(ctx context.Context) error { - return NewIDockerPortGuardService().Reconcile(ctx) -} - -func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) error { - persistedEnabled, err := dockerPortGuardPersistedEnabled() - if err != nil { - return fmt.Errorf("load Docker port guard persisted status: %w", err) - } - backends := []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} - if s.runtime != nil { - backends = []string{selectedDockerFirewallBackend(constant.FirewallProviderIptables)} - } - initializedByBackend := make(map[string]bool, len(backends)) - initialized := false - for _, backend := range backends { - initialized, err = s.guardRuntime(backend).Initialized(dockerfirewall.FamilyIPv4) - if err != nil { - return &dockerfirewall.FamilyError{Family: dockerfirewall.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)} - } - initializedByBackend[backend] = initialized - if initialized { - break - } - } - if !initialized && !persistedEnabled { - return nil - } - runtime, backend, err := s.runtimeForDocker(ctx) - if err != nil { - return err - } - initialized, inspected := initializedByBackend[backend] - if !inspected { - initialized, err = runtime.Initialized(dockerfirewall.FamilyIPv4) - if err != nil { - return &dockerfirewall.FamilyError{Family: dockerfirewall.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)} - } - } - if !initialized && !persistedEnabled { - return nil - } - policies, err := s.runtimePolicies(ctx) - if err != nil { - return err - } - inventory, err := runtime.ListPolicies() - if err != nil { - return err - } - if !initialized { - err = runtime.Initialize(policies, inventory) - } else { - err = runtime.ReplacePolicies(policies, inventory) - } - if err != nil { - return err - } - return verifyDockerFirewall(runtime, policies, inventory.ReadOnly) -} - func (s *DockerPortGuardService) runtimeForDocker(ctx context.Context) (dockerfirewall.Runtime, string, error) { if s.runtime != nil { return s.runtime, selectedDockerFirewallBackend(constant.FirewallProviderIptables), nil @@ -2973,24 +1554,24 @@ func (s *DockerPortGuardService) runtimeForDocker(ctx context.Context) (dockerfi if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables { return nil, backend, fmt.Errorf("Docker firewall backend %q is not supported", backend) } - return s.guardRuntime(backend), backend, nil + return s.guardRuntime(ctx, backend), backend, nil } -func (s *DockerPortGuardService) guardRuntime(backend string) dockerfirewall.Runtime { +func (s *DockerPortGuardService) guardRuntime(ctx context.Context, backend string) dockerfirewall.Runtime { if s.runtimeForBackend != nil { - return s.runtimeForBackend(backend) + return s.runtimeForBackend(ctx, backend) } if s.runtime != nil { return s.runtime } - return newDockerFirewallRuntime(backend) + return newDockerFirewallRuntime(ctx, backend) } -func newDockerFirewallRuntime(backend string) dockerfirewall.Runtime { +func newDockerFirewallRuntime(ctx context.Context, backend string) dockerfirewall.Runtime { if backend == constant.FirewallProviderNftables { - return dockerfirewall.NewNftables() + return dockerfirewall.NewNftables(ctx) } - return dockerfirewall.NewIptables() + return dockerfirewall.NewIptables(ctx) } func selectedDockerFirewallBackend(fallback string) string { @@ -3013,29 +1594,6 @@ func dockerFirewallBackend(info system.Info) string { return strings.ToLower(info.FirewallBackend.Driver) } -func (s *DockerPortGuardService) runtimePolicies(ctx context.Context) ([]dockerfirewall.Policy, error) { - stored, err := s.policies.ListManaged(ctx) - if err != nil { - return nil, err - } - policies := make([]dockerfirewall.Policy, 0, len(stored)) - for _, policy := range stored { - sources := []string{} - _ = json.Unmarshal([]byte(policy.Sources), &sources) - policies = append(policies, dockerfirewall.Policy{UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources}) - } - return policies, nil -} - -func dockerGuardReadOnlyPolicyUUID(policy dockerfirewall.ReadOnlyPolicy) string { - nativeRules, _ := json.Marshal(policy.NativeRules) - fingerprint := strings.Join([]string{ - policy.Policy.Family, policy.Policy.HostIP, strconv.Itoa(int(policy.Policy.HostPort)), - policy.Policy.Protocol, policy.Action, string(nativeRules), - }, "\x00") - return uuid.NewSHA1(uuid.NameSpaceOID, []byte(fingerprint)).String() -} - func dockerPortGuardPersistedEnabled() (bool, error) { status, err := settingRepo.GetValueByKey(constant.FirewallDockerPortGuardStatusKey) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -3044,110 +1602,10 @@ func dockerPortGuardPersistedEnabled() (bool, error) { return status == constant.StatusEnable, err } -func verifyDockerFirewall(runtime dockerfirewall.Runtime, desired []dockerfirewall.Policy, preserved []dockerfirewall.ReadOnlyPolicy) error { - inventory, err := runtime.ListPolicies() - if err != nil { - return fmt.Errorf("verify synchronized Docker firewall policies: %w", err) - } - if !dockerFirewallPoliciesEqual(inventory.Policies, desired) { - return fmt.Errorf("verify synchronized Docker firewall policies: target policies do not match the database") - } - if !readOnlyStatesEqual(inventory.ReadOnly, preserved) { - return fmt.Errorf("verify synchronized Docker firewall policies: read-only runtime rules changed") - } - return nil -} - -func dockerFirewallPoliciesEqual(left, right []dockerfirewall.Policy) bool { - if len(left) != len(right) { - return false - } - counts := make(map[string]int, len(left)) - for _, policy := range left { - counts[dockerFirewallPolicyKey(policy)]++ - } - for _, policy := range right { - key := dockerFirewallPolicyKey(policy) - if counts[key] == 0 { - return false - } - counts[key]-- - } - return true -} - -func dockerFirewallPolicyKey(policy dockerfirewall.Policy) string { - mode := policy.Mode - if mode == dockerfirewall.ModeAllow && len(policy.Sources) == 0 { - mode = dockerfirewall.ModeAll - } - sources := make([]string, 0, len(policy.Sources)) - for _, source := range policy.Sources { - source = strings.TrimSpace(source) - if prefix, err := netip.ParsePrefix(source); err == nil { - source = prefix.Masked().String() - } else if address, err := netip.ParseAddr(source); err == nil { - address = address.Unmap() - source = netip.PrefixFrom(address, address.BitLen()).String() - } - sources = append(sources, source) - } - sort.Strings(sources) - host := policy.HostIP - if address, err := netip.ParseAddr(host); err == nil { - host = address.String() - } - return strings.Join([]string{ - policy.UUID, policy.Family, host, strconv.Itoa(int(policy.HostPort)), - policy.Protocol, mode, strings.Join(sources, ","), - }, "\x00") -} - -func readOnlyStatesEqual(left, right []dockerfirewall.ReadOnlyPolicy) bool { - if len(left) != len(right) { - return false - } - leftRules := flattenNativeRules(left) - rightRules := flattenNativeRules(right) - if len(leftRules) != len(rightRules) { - return false - } - for index := range leftRules { - if leftRules[index].Family != rightRules[index].Family || !slices.Equal(leftRules[index].Tokens, rightRules[index].Tokens) { - return false - } - } - return true -} - -func flattenNativeRules(policies []dockerfirewall.ReadOnlyPolicy) []dockerfirewall.NativeRule { - rules := make([]dockerfirewall.NativeRule, 0) - for _, policy := range policies { - rules = append(rules, policy.NativeRules...) - } - slices.SortStableFunc(rules, func(left, right dockerfirewall.NativeRule) int { - if left.Family < right.Family { - return -1 - } - if left.Family > right.Family { - return 1 - } - if left.Order < right.Order { - return -1 - } - if left.Order > right.Order { - return 1 - } - return 0 - }) - return rules -} - func newDockerPortGuardService() *DockerPortGuardService { return &DockerPortGuardService{ - policies: repo.NewIDockerPortGuardRepo(), - client: docker.NewDockerClient, - version: dockerFirewallVersion, + client: docker.NewDockerClient, + version: dockerFirewallVersion, } } @@ -3331,28 +1789,6 @@ func restoreFail2BanAfterFirewallStart() error { return nil } -func currentFirewallRuleSyncTaskLocked() (dto.FirewallRuleSyncTask, error) { - if firewallRuleSyncTaskID != "" { - return dto.FirewallRuleSyncTask{TaskID: firewallRuleSyncTaskID, Executing: true}, nil - } - if global.TaskDB == nil { - return dto.FirewallRuleSyncTask{}, nil - } - taskRepo := repo.NewITaskRepo() - record, err := taskRepo.GetFirst( - repo.WithByStatus(constant.StatusExecuting), - repo.WithByType(task.TaskScopeFirewall), - taskRepo.WithOperate(task.TaskSync), - ) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return dto.FirewallRuleSyncTask{}, nil - } - return dto.FirewallRuleSyncTask{}, err - } - return dto.FirewallRuleSyncTask{TaskID: record.ID, Executing: true}, nil -} - func (s *FirewallService) operateFilterChainBase(provider string, request dto.FilterChainOperation) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() @@ -3368,14 +1804,54 @@ func (s *FirewallService) operateFilterChainBaseLocked(provider string, request } operation := firewall.BaseOperation(request.Operate) var ports []firewall.PortWhitelist - if operation == firewall.BaseOperationInit || operation == firewall.BaseOperationBind { + if operation == firewall.BaseOperationInit { var err error ports, err = LoadRequiredFirewallPortWhiteList() if err != nil { return err } } - if provider == constant.FirewallProviderNftables { + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if operation == firewall.BaseOperationInit && !slices.Contains(families, constant.FirewallFamilyIPv6) { + global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 host chain initialization") + } + if operation == firewall.BaseOperationBind { + bound := false + for _, family := range families { + status, err := loadSystemFirewallFamilyInfo(provider, family) + if err != nil { + return err + } + if !status.Initialized { + continue + } + if provider == constant.FirewallProviderNftables { + err = nftables_helper.OperateFamily(filter.Family(family), false, nil) + } else { + err = iptables_helper.OperateFamily(family, false, nil) + } + if err != nil { + return err + } + bound = true + } + if !bound { + return filter.ErrProviderUnavailable + } + } else if len(families) == 1 && operation != firewall.BaseOperationUnbind { + initialize := operation == firewall.BaseOperationInit + if provider == constant.FirewallProviderNftables { + err = nftables_helper.OperateFamily(filter.FamilyIPv4, initialize, ports) + } else { + err = iptables_helper.OperateFamily(constant.FirewallFamilyIPv4, initialize, ports) + } + if err != nil { + return err + } + } else if provider == constant.FirewallProviderNftables { if err := nftables_helper.Operate(operation, ports); err != nil { return err } @@ -3463,54 +1939,7 @@ func resetServiceFirewallClient(client lifecycle.Client, withDockerRestart bool, return resetter.Reset() } -func (s *FirewallService) desiredFirewallRulesByScope(ctx context.Context, stored []model.FirewallRule, runtime filter.Adapter) (map[string][]filter.DesiredRule, []filter.InventoryItem) { - provider := runtime.Provider() - desired := make(map[string][]filter.DesiredRule) - var failures []filter.InventoryItem - ports, protectionErr := loadFirewallPortWhiteList() - var required []firewall.PortWhitelist - if protectionErr == nil { - loadRequired := s.requiredPorts - if loadRequired != nil { - required, protectionErr = loadRequired() - } else { - required, protectionErr = firewall.RequiredPortWhitelist(ports) - } - } - whitelist := filter.NewPortWhitelistIndex(ports) - for _, record := range stored { - compiled, _, err := s.compileRestorableFirewallRules(ctx, record, runtime, required) - if err == nil { - err = protectionErr - } - if err != nil { - rule := filter.FirewallRule{ - UUID: record.UUID, - Scope: filter.Scope{Provider: provider, Family: filter.Family(record.Family), Direction: filter.DirectionInput}.Normalize(), - Protocol: record.Protocol, SourceAddress: record.SourceAddress, SourcePort: record.SourcePort, - DestinationAddress: record.DestinationAddress, DestinationPort: record.DestinationPort, - Interface: record.Interface, ConnectionStates: strings.FieldsFunc(record.ConnectionStates, func(r rune) bool { return r == ',' }), - Action: filter.Action(record.Action), Description: record.Description, Priority: record.Priority, - } - failures = append(failures, filter.InventoryItem{ - Incompatible: isFirewallPolicyIncompatible(err), - Rule: rule, State: filter.InventoryStateDrifted, Match: filter.InventoryMatchNone, - Desired: &filter.DesiredRule{UUID: record.UUID, Rule: rule, Origin: filter.RuleOrigin(record.Origin), Protected: protectionErr != nil || whitelist.Matches(rule)}, - Error: fmt.Sprintf("policy %s: %v", record.UUID, err), - }) - continue - } - for _, rule := range compiled { - rule.Protected = whitelist.Matches(rule.Rule) - rule.Expanded = len(compiled) > 1 - key := rule.Rule.Scope.Key() - desired[key] = append(desired[key], rule) - } - } - return desired, failures -} - -func firewallInventoryPositionRanges(provider filter.Provider, items []filter.InventoryItem) (ipv4, ipv6 filter.PositionRange) { +func firewallInventoryPositionRanges(provider filter.Provider, chain string, items []filter.InventoryItem) (ipv4, ipv6 filter.PositionRange) { if provider == filter.ProviderFirewalld { return filter.PositionRange{Min: -32768, Max: 32767}, filter.PositionRange{Min: -32768, Max: 32767} } @@ -3522,8 +1951,12 @@ func firewallInventoryPositionRanges(provider filter.Provider, items []filter.In if scope.Provider != provider || scope.Direction != filter.DirectionInput { continue } + position := *item.Observed.Locator.Position + if position < 1 { + continue + } if (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && - (scope.Table != "filter" || scope.Chain != filter.IptablesInputChain) { + (scope.Table != "filter" || scope.Chain != chain) { continue } bounds := &ipv4 @@ -3532,10 +1965,6 @@ func firewallInventoryPositionRanges(provider filter.Provider, items []filter.In } else if scope.Family != filter.FamilyIPv4 { continue } - position := *item.Observed.Locator.Position - if position < 1 { - continue - } if bounds.Min == 0 || position < bounds.Min { bounds.Min = position } @@ -3547,139 +1976,6 @@ func firewallInventoryPositionRanges(provider filter.Provider, items []filter.In return } -func (s *FirewallService) adoptRule(ctx context.Context, runtime filter.Adapter, snapshot filter.RuleSet, observed filter.ObservedRule, source dto.FirewallRuleCreateItem) error { - if (observed.Rule.Scope.Provider == filter.ProviderIptables || observed.Rule.Scope.Provider == filter.ProviderNftables) && - (observed.Rule.Scope.Chain == filter.BasicBeforeChain || observed.Rule.Scope.Chain == filter.BasicAfterChain) { - return fmt.Errorf("%w: system preset chains cannot be adopted", filter.ErrUnsupportedScope) - } - if observed.Protected { - return filter.ErrProtectedRule - } - if observed.ParseStatus != filter.ParseStatusSupported || - (observed.Persistence != "" && observed.Persistence != filter.PersistenceStatusConverged) { - return fmt.Errorf("%w: rule cannot be managed", filter.ErrRuleOperation) - } - rule, err := prepareFirewallBackendRule(ctx, runtime, observed.Rule) - if err != nil { - return err - } - if rule.Scope.Provider != filter.ProviderFirewalld && observed.Locator.Position != nil { - position := int64(*observed.Locator.Position) - rule.OrderIndex = &position - } - record, err := firewallRuleModelForCreate(rule, source, constant.FirewallRuleOriginAdopted) - if err != nil { - return err - } - stored, err := s.rules.List(ctx) - if err != nil { - return err - } - if runtime.Provider() == filter.ProviderNftables { - duplicates := 0 - for _, candidate := range snapshot.Rules { - if candidate.ParseStatus != filter.ParseStatusSupported { - continue - } - same, err := filter.SameRuleContent(candidate.Rule, rule) - if err != nil { - return err - } - if same { - duplicates++ - if duplicates > 1 { - return buserr.WithDetail("ErrInvalidParams", "duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", nil) - } - } - } - } - identities, err := firewallRuleCollisions(stored, rule.Scope.Provider) - if err != nil { - return err - } - for _, existing := range stored { - marker := "1panel-rule:" + existing.UUID - if existing.UUID != "" && (observed.Marker == marker || strings.HasPrefix(observed.Marker, marker+"-")) { - return fmt.Errorf("%w: rule is already managed", filter.ErrRuleOperation) - } - } - if err := identities.CheckDuplicate(rule); err != nil { - if errors.Is(err, filter.ErrRuleOperation) { - return buserr.WithDetail("ErrInvalidParams", "duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", nil) - } - return err - } - record.UUID = uuid.NewString() - rule.UUID = record.UUID - if runtime.Provider() != filter.ProviderNftables { - ports, err := loadFirewallPortWhiteList() - if err != nil { - return err - } - if filter.RuleMatchesPortWhitelist(rule, ports) { - return filter.ErrProtectedRule - } - before := observed.Rule - before.UUID = record.UUID - scope := filter.RuleSet{Scope: rule.Scope} - remove, err := runtime.BuildCommands(scope, []filter.RuleChange{{ - Operation: filter.ChangeDelete, Before: &before, CommandOnly: true, - UnmarkedAdopted: observed.Marker == "", PreviousMarker: observed.Marker, - }}) - if err != nil { - return err - } - create, err := runtime.BuildCommands(scope, []filter.RuleChange{{ - Operation: filter.ChangeCreate, After: &rule, CommandOnly: true, Append: rule.OrderIndex == nil, - }}) - if err != nil { - return err - } - remove.CommandOnly, create.CommandOnly = true, true - if err := runtime.RunCommands(ctx, remove); err != nil { - return err - } - record.Priority = rule.Priority - saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - err = s.saveFirewallRule(saveCtx, &record) - cancel() - if err != nil { - return err - } - runErr := runtime.RunCommands(ctx, create) - if err := errors.Join(runErr, persistFirewallRules(ctx, runtime, create)); err != nil { - return buserr.WithDetail("ErrFirewallRuleSavedApplyFailed", err.Error(), err) - } - return nil - } - plan, verification, err := applyFirewallChanges(runtime, ctx, snapshot, []filter.RuleChange{{ - Operation: filter.ChangeAdopt, After: &rule, Locator: &observed.Locator, PreviousMarker: observed.Marker, - }}) - if err != nil { - return err - } - if !verification.Matched { - return filter.ErrVerificationFailed - } - if _, err := filter.FindCommittedObserved(verification.RuleSet, rule, plan); err != nil { - return rollbackFirewallPlan(ctx, runtime, plan, err) - } - return s.saveFirewallRule(ctx, &record) -} - -func (index firewallRuleCollisionIndex) CheckDuplicate(rule filter.FirewallRule) error { - key, err := filter.RuleMatchKey(rule) - if err != nil { - return err - } - for _, action := range index[key] { - if action == rule.Action { - return checkCollisionActions(rule.Action, action) - } - } - return nil -} - func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) { index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool { return samePortWhitelistRule(rule, target) @@ -3720,10 +2016,7 @@ func loadSSHWhitelistPortFrom(path string) (string, error) { func newFirewallService() *FirewallService { return &FirewallService{ - rules: repo.NewIFirewallRuleRepo(), adapters: nil, - forwardingSync: newForwardingService(), - dockerSync: newDockerPortGuardService(), selectedProvider: firewallRuleSelectedProvider, requiredPorts: LoadRequiredFirewallPortWhiteList, cleanupBackend: cleanupSystemBackend, @@ -3733,7 +2026,7 @@ func newFirewallService() *FirewallService { restoreForwarding: func(ctx context.Context) error { return newForwardingService().Restore(ctx) }, - restoreDockerGuard: ReconcileDockerPortGuard, + restoreDockerGuard: RestoreDockerPortGuard, baseClient: NewSelectedSystemFirewallClient, } } @@ -3746,272 +2039,6 @@ func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) { return filter.Provider(client.Name()), nil } -func (s *ForwardingService) loadRuleSyncCandidates(ctx context.Context, targetProvider filter.Provider) (forwarding.Adapter, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) { - target, err := s.clientFactory() - if err != nil { - return nil, nil, nil, false, err - } - if target.Name() != string(targetProvider) { - return nil, nil, nil, false, fmt.Errorf( - "%w: selected forwarding backend is %s, requested target is %s", - filter.ErrProviderUnavailable, target.Name(), targetProvider, - ) - } - stored, err := s.rules.List(ctx) - if err != nil { - return nil, nil, nil, false, err - } - candidates := make([]forwardingRuleSyncCandidate, 0, len(stored)) - for _, record := range stored { - rule := forwarding.Rule{ - Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP, - TargetPort: record.TargetPort, Interface: record.Interface, - } - normalized, normalizeErr := forwarding.NormalizeRule(rule) - candidates = append(candidates, forwardingRuleSyncCandidate{rule: normalized, err: normalizeErr}) - } - initialized, _, err := target.InitStatus() - if err != nil { - return nil, nil, nil, false, err - } - targetRules := make([]forwarding.Rule, 0) - if initialized { - targetRules, err = target.List() - if err != nil { - return nil, nil, nil, false, err - } - targetRules, err = normalizeForwardingRuntimeRules(targetRules) - if err != nil { - return nil, nil, nil, false, err - } - } - return target, candidates, targetRules, initialized, nil -} - -func normalizeForwardingRuntimeRules(rules []forwarding.Rule) ([]forwarding.Rule, error) { - normalized := make([]forwarding.Rule, 0, len(rules)) - for _, rule := range rules { - item, err := forwarding.NormalizeRule(rule) - if err != nil { - return nil, fmt.Errorf("normalize target forwarding rule %s: %w", rule.Identity(), err) - } - normalized = append(normalized, item) - } - return normalized, nil -} - -func databaseRuleSyncTarget(request dto.FirewallRuleSyncRequest, subsystem string) (filter.Provider, error) { - if request.SourceProvider != "" { - return "", fmt.Errorf("%w: %s synchronization reads rules from the database and does not accept a source provider", filter.ErrInvalidRule, subsystem) - } - if request.ResetSource { - return "", fmt.Errorf("%w: %s synchronization does not have a source firewall to reset", filter.ErrInvalidRule, subsystem) - } - if request.TargetProvider != filter.ProviderIptables && request.TargetProvider != filter.ProviderNftables { - return "", fmt.Errorf("%w: %s synchronization only supports iptables and nftables targets", filter.ErrInvalidRule, subsystem) - } - return request.TargetProvider, nil -} - -func forwardingSyncPreview(target filter.Provider, candidates []forwardingRuleSyncCandidate, actual []forwarding.Rule) dto.FirewallRuleSyncPreview { - desired := make([]firewallSyncDesired[forwarding.Rule], 0, len(candidates)) - for _, candidate := range candidates { - desired = append(desired, firewallSyncDesired[forwarding.Rule]{ - Value: candidate.rule, - Payload: dto.FirewallRuleSyncItem{ - SourceUUID: candidate.rule.Identity(), ForwardRule: &dto.ForwardRule{Family: candidate.rule.Family, Protocol: candidate.rule.Protocol, Port: candidate.rule.Port, TargetIP: candidate.rule.TargetIP, TargetPort: candidate.rule.TargetPort, Interface: candidate.rule.Interface}, - }, - Err: candidate.err, - }) - } - return firewallDiffPreview( - "forwarding", target, desired, actual, - func(rule forwarding.Rule) string { return rule.Identity() }, - func(rule forwarding.Rule) dto.FirewallRuleSyncItem { - return dto.FirewallRuleSyncItem{SourceUUID: rule.Identity(), ForwardRule: &dto.ForwardRule{Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface}} - }, - ) -} - -func firewallDiffPreview[T any](subsystem string, target filter.Provider, desired []firewallSyncDesired[T], actual []T, key func(T) string, actualItem func(T) dto.FirewallRuleSyncItem) dto.FirewallRuleSyncPreview { - preview := dto.FirewallRuleSyncPreview{Subsystem: subsystem, TargetProvider: target, Items: make([]dto.FirewallRuleSyncItem, 0, len(desired)+len(actual))} - actualByKey := make(map[string][]int, len(actual)) - for index, value := range actual { - actualByKey[key(value)] = append(actualByKey[key(value)], index) - } - matched := make([]bool, len(actual)) - for _, candidate := range desired { - item := candidate.Payload - item.Status, item.ReasonCode, item.Reason = "", "", "" - switch { - case candidate.Err != nil: - item.Status, item.ReasonCode, item.Reason = firewallsync.StatusBlocked, firewallsync.ReasonInvalidPolicy, candidate.Err.Error() - default: - match := -1 - for _, index := range actualByKey[key(candidate.Value)] { - if !matched[index] { - match = index - break - } - } - if match >= 0 { - matched[match] = true - item.Status, item.ReasonCode = firewallsync.StatusExisting, firewallsync.ReasonAlreadyExists - item.Reason = firewallSyncReasonMessage(item.ReasonCode) - } else { - item.Status = firewallsync.StatusReady - } - } - preview.Add(item) - } - for index, value := range actual { - if matched[index] { - continue - } - item := actualItem(value) - item.Status, item.ReasonCode = firewallsync.StatusRemove, firewallsync.ReasonOnlyExistsInTarget - item.Reason = firewallSyncReasonMessage(item.ReasonCode) - preview.Add(item) - } - return preview -} - -func firewallSyncResult(preview dto.FirewallRuleSyncPreview, cause error, executed bool) dto.FirewallRuleSyncResult { - result := dto.FirewallRuleSyncResult{Subsystem: preview.Subsystem, TargetProvider: preview.TargetProvider} - for _, item := range preview.Items { - if item.Status != firewallsync.StatusRemove { - result.Total++ - } - switch item.Status { - case firewallsync.StatusExisting: - result.Skipped++ - case firewallsync.StatusBlocked: - appendDatabaseSyncFailure(&result, item, errors.New(item.Reason)) - case firewallsync.StatusReady: - if executed { - if cause != nil { - appendDatabaseSyncFailure(&result, item, cause) - } else { - result.Succeeded++ - } - } - case firewallsync.StatusRemove: - if executed && cause == nil { - result.Removed++ - } - } - } - return result -} - -func firewallRuleStatesEqual[T any](left, right []T, key func(T) string) bool { - if len(left) != len(right) { - return false - } - counts := make(map[string]int, len(left)) - for _, value := range left { - counts[key(value)]++ - } - for _, value := range right { - valueKey := key(value) - if counts[valueKey] == 0 { - return false - } - counts[valueKey]-- - } - return true -} - -func (s *DockerPortGuardService) loadRuleSyncCandidates(ctx context.Context, request dto.FirewallRuleSyncRequest) (string, []model.DockerPortGuardPolicy, dockerfirewall.Runtime, error) { - targetProvider, err := databaseRuleSyncTarget(request, "Docker") - if err != nil { - return "", nil, nil, err - } - target := string(targetProvider) - selected, err := s.selectedRuleSyncBackend(ctx) - if err != nil { - return "", nil, nil, err - } - if target != selected { - return "", nil, nil, fmt.Errorf( - "%w: selected Docker firewall backend is %s, requested target is %s", - filter.ErrProviderUnavailable, selected, target, - ) - } - policies, err := s.policies.ListManaged(ctx) - if err != nil { - return "", nil, nil, err - } - return target, policies, s.guardRuntime(target), nil -} - -func (s *DockerPortGuardService) selectedRuleSyncBackend(ctx context.Context) (string, error) { - selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey) - selected = strings.ToLower(strings.TrimSpace(selected)) - if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables { - return selected, nil - } - if s.client == nil { - return "", buserr.New("ErrDockerFailed") - } - cli, err := s.client() - if err != nil { - return "", buserr.WithDetail("ErrDockerFailed", err.Error(), err) - } - defer cli.Close() - info, err := cli.Info(ctx) - if err != nil { - return "", buserr.WithDetail("ErrDockerFailed", err.Error(), err) - } - return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil -} - -func dockerSyncPreview(target filter.Provider, policies []model.DockerPortGuardPolicy, inventory dockerfirewall.PolicyInventory) dto.FirewallRuleSyncPreview { - desired := make([]firewallSyncDesired[dockerfirewall.Policy], 0, len(policies)) - for _, policy := range policies { - sources := []string{} - _ = json.Unmarshal([]byte(policy.Sources), &sources) - desired = append(desired, firewallSyncDesired[dockerfirewall.Policy]{ - Value: dockerfirewall.Policy{ - UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, - Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources, - }, - Payload: dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: &dto.DockerPortGuardEndpoint{ - Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, - PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources, Description: policy.Description, - TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, - ManagementReason: dockerReasonNoMatchingPath, - }}, - }) - } - preview := firewallDiffPreview( - "docker", target, desired, inventory.Policies, dockerFirewallPolicyKey, - func(policy dockerfirewall.Policy) dto.FirewallRuleSyncItem { - return dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: &dto.DockerPortGuardEndpoint{ - Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, - PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...), - TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, - ManagementReason: dockerReasonNoMatchingPath, - }} - }, - ) - for _, policy := range inventory.ReadOnly { - preview.Add(dto.FirewallRuleSyncItem{ - SourceUUID: dockerGuardReadOnlyPolicyUUID(policy), - DockerRule: &dto.DockerPortGuardEndpoint{ - Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort, - Protocol: policy.Policy.Protocol, PolicyUUID: dockerGuardReadOnlyPolicyUUID(policy), Sources: append([]string(nil), policy.Policy.Sources...), - NativeAction: policy.Action, ReadOnly: true, TrafficPath: dockerTrafficPathUnknown, - ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath, - }, - Status: firewallsync.StatusBlocked, - ReasonCode: firewallsync.ReasonReadOnlyRule, - Reason: firewallSyncReasonMessage(firewallsync.ReasonReadOnlyRule), - }) - } - return preview -} - func discoverDockerEndpoints(ctx context.Context, cli *client.Client, all bool) ([]dto.DockerPortGuardEndpoint, error) { containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: all}) if err != nil { @@ -4045,12 +2072,12 @@ func discoverDockerEndpoints(ctx context.Context, cli *client.Client, all bool) return endpoints, nil } -func annotateDockerEndpointManagement(endpoints []dto.DockerPortGuardEndpoint, backend string) { +func annotateDockerEndpointManagement(ctx context.Context, endpoints []dto.DockerPortGuardEndpoint, backend string) { rules := map[string]dockerfirewall.DNATRules{ - constant.FirewallFamilyIPv4: dockerfirewall.ReadDNATRules(backend, constant.FirewallFamilyIPv4), - constant.FirewallFamilyIPv6: dockerfirewall.ReadDNATRules(backend, constant.FirewallFamilyIPv6), + constant.FirewallFamilyIPv4: dockerfirewall.ReadDNATRules(ctx, backend, constant.FirewallFamilyIPv4), + constant.FirewallFamilyIPv6: dockerfirewall.ReadDNATRules(ctx, backend, constant.FirewallFamilyIPv6), } - proxies := dockerfirewall.ReadProxyEndpoints() + proxies := dockerfirewall.ReadProxyEndpoints(ctx) inspections := make(map[string]dockerfirewall.EndpointInspection, len(rules)) for family, familyRules := range rules { inspections[family] = dockerfirewall.InspectEndpoints(backend, family, familyRules, proxies) @@ -4111,7 +2138,7 @@ func groupDockerGuardContainers(endpoints []dto.DockerPortGuardEndpoint) []dto.D for i, endpoint := range container.Endpoints { sources := append([]string(nil), endpoint.Sources...) sort.Strings(sources) - policyKey := fmt.Sprintf("%t|%s|%s|%t|%s|%s|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.Description, endpoint.ManagementTarget, endpoint.ManagementReason) + policyKey := fmt.Sprintf("%t|%s|%s|%t|%s|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.ManagementTarget, endpoint.ManagementReason) items = append(items, docker.PortRangeItem{ Key: endpoint.Family + "|" + endpoint.HostIP + "|" + endpoint.Protocol + "|" + policyKey, PublicPort: endpoint.HostPort, PrivatePort: endpoint.ContainerPort, @@ -4166,19 +2193,29 @@ func normalizeDockerFirewallUUIDs(values []string) ([]string, error) { return uuids, nil } -func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) { - taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0) +func queueFirewallRuleTask(subsystem, operation, taskID string, labels []string, apply func(*task.Task) error) (dto.FilterChainOperationResponse, error) { + taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, taskID, 0) if err != nil { return dto.FilterChainOperationResponse{}, err } taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { - t.Logf("rules=%d", len(labels)) + if labels != nil { + t.Logf("rules=%d", len(labels)) + } err := t.TaskCtx.Err() if err == nil { - err = apply(t.TaskCtx) + err = apply(t) } - succeeded, failed := 0, 0 + err = errors.Join(err, t.TaskCtx.Err()) + if labels == nil { + return err + } + succeeded, failed, skipped := 0, 0, 0 for _, label := range labels { + if label == "" { + skipped++ + continue + } if err != nil { failed++ t.LogFailedWithErr(label, err) @@ -4187,12 +2224,12 @@ func queueFirewallRuleTask(subsystem, operation string, labels []string, apply f t.LogSuccess(label) } } - t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{ - "succeeded": succeeded, "failed": failed, + t.Log(i18n.GetMsgWithMap("FirewallCreateRulesResult", map[string]interface{}{ + "succeeded": succeeded, "failed": failed, "skipped": skipped, })) return err }, nil, 0, 0) - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { + if err := taskRepo.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) @@ -4209,13 +2246,14 @@ func normalizeDockerFirewallPolicy(policy dockerfirewall.Policy) (dockerfirewall if policy.HostPort == 0 || (policy.Protocol != "tcp" && policy.Protocol != "udp") || (policy.Family != dockerfirewall.FamilyIPv4 && policy.Family != dockerfirewall.FamilyIPv6) || - (policy.Mode != dockerfirewall.ModeAll && policy.Mode != dockerfirewall.ModeSources && policy.Mode != dockerfirewall.ModeAllow) { + (policy.Mode != dockerfirewall.ModeAll && policy.Mode != dockerfirewall.ModeSources && policy.Mode != dockerfirewall.ModeAllow && policy.Mode != dockerfirewall.ModeAcceptSources && policy.Mode != dockerfirewall.ModeAcceptAll) { return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "invalid policy fields", nil) } address, err := netip.ParseAddr(policy.HostIP) if err != nil || (policy.Family == dockerfirewall.FamilyIPv4) != address.Is4() { return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "host IP does not match address family", nil) } + policy.HostIP = address.String() normalizedSources := make([]string, 0, len(policy.Sources)) seen := make(map[string]struct{}, len(policy.Sources)) for _, source := range policy.Sources { @@ -4244,10 +2282,10 @@ func normalizeDockerFirewallPolicy(policy dockerfirewall.Policy) (dockerfirewall normalizedSources = append(normalizedSources, canonical) } } - if policy.Mode != dockerfirewall.ModeAll && len(normalizedSources) == 0 { + if policy.Mode != dockerfirewall.ModeAll && policy.Mode != dockerfirewall.ModeAcceptAll && len(normalizedSources) == 0 { return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "source-based modes require at least one source", nil) } - if policy.Mode == dockerfirewall.ModeAll { + if policy.Mode == dockerfirewall.ModeAll || policy.Mode == dockerfirewall.ModeAcceptAll { normalizedSources = []string{} } sort.Strings(normalizedSources) @@ -4255,17 +2293,6 @@ func normalizeDockerFirewallPolicy(policy dockerfirewall.Policy) (dockerfirewall return policy, nil } -func (i forwardingInventoryItem) SyncStatus() string { - switch { - case i.IsDesired && i.IsRuntime: - return forwardingSyncConverged - case i.IsDesired: - return forwardingSyncMissing - default: - return forwardingSyncRuntimeOnly - } -} - func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool { if len(operations) == 0 { return false @@ -4318,3 +2345,654 @@ func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firew } return firewall.ValidatePortWhitelist(entries) } +func (s *FirewallService) readFirewallInventory(ctx context.Context, provider filter.Provider, scopes []filter.Scope) (dto.FirewallRuleInventoryResponse, error) { + response := dto.FirewallRuleInventoryResponse{Items: make([]filter.InventoryItem, 0)} + runtime, err := s.firewallAdapter(provider) + if err != nil { + return response, err + } + for _, scope := range scopes { + if scope.Provider != provider { + return response, filter.ErrInvalidScope + } + } + for _, group := range firewallScopeReadGroups(scopes) { + snapshots, err := listFirewallRuleScopes(runtime, ctx, group) + if errors.Is(err, filter.ErrFamilyUnavailable) { + response.Notices = append(response.Notices, filter.ScopeNotice{Code: filter.ScopeNoticeFamilyUnavailable, Values: []string{string(group[0].Family), err.Error()}}) + continue + } + if err != nil { + return response, err + } + for _, snapshot := range snapshots { + response.Notices = append(response.Notices, snapshot.Notices...) + for _, observed := range snapshot.Rules { + observed.InstanceKey, err = filter.InstanceKey(observed) + if err != nil { + return response, err + } + response.Items = append(response.Items, filter.InventoryItem{Rule: observed.Rule, Observed: &observed}) + } + } + } + if provider == filter.ProviderUFW { + sort.SliceStable(response.Items, func(i, j int) bool { + left, right := response.Items[i].Observed.Locator.Position, response.Items[j].Observed.Locator.Position + return left != nil && right != nil && *left < *right + }) + } + return response, nil +} + +func saveFirewallDescription(ctx context.Context, observed filter.ObservedRule, description string) error { + if err := ctx.Err(); err != nil { + return err + } + id, err := filter.DescriptionID(observed) + if err != nil { + return err + } + description = strings.TrimSpace(description) + if description == "" { + return settingRepo.DelDescription(id) + } + existing, err := settingRepo.GetDescription(settingRepo.WithByDescriptionID(id), repo.WithByType("firewall")) + if errors.Is(err, gorm.ErrRecordNotFound) { + return settingRepo.CreateDescription(&model.CommonDescription{ID: id, Type: "firewall", Description: description}) + } + if err != nil { + return err + } + return settingRepo.UpdateDescription(existing.ID, map[string]interface{}{"description": description}) +} + +func (s *FirewallService) updateObservedRule(ctx context.Context, request dto.FirewallRuleUpdate) error { + metadata := request.Description != nil || request.OrderIndex != nil || request.Priority != nil + if (request.Rule != nil) == metadata || request.OrderIndex != nil && request.Priority != nil { + return filter.ErrInvalidRule + } + runtime, err := s.resolveRuntime(ctx, request.Scope.Provider) + if err != nil { + return err + } + snapshot, err := readMutableFirewallRules(runtime, ctx, request.Scope) + if err != nil { + return err + } + observed, err := filter.FindCandidate(snapshot.Rules, request.InstanceKey) + if err != nil { + return filter.ErrRuleStale + } + if err := filter.GuardMutation(observed); err != nil { + return err + } + if request.Rule != nil { + ports, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + if filter.NewPortWhitelistIndex(ports).Matches(observed.Rule) { + return filter.ErrProtectedRule + } + } + if request.Rule == nil && request.OrderIndex == nil && request.Priority == nil { + return saveFirewallDescription(ctx, observed, *request.Description) + } + if observed.ParseStatus != filter.ParseStatusSupported { + return filter.ErrUnsupportedScope + } + before := observed.Rule + before.UUID = uuid.NewString() + after := before + operation := filter.ChangeReorder + if runtime.Provider() == filter.ProviderFirewalld { + operation = filter.ChangeUpdate + } + if request.Rule != nil { + after, err = prepareFirewallBackendRule(ctx, runtime, *request.Rule) + if err != nil { + return err + } + operation = filter.ChangeUpdate + } else { + after.OrderIndex, after.Priority = request.OrderIndex, before.Priority + if request.Priority != nil { + after.Priority = request.Priority + } + } + if after.Scope.Normalize().Key() != before.Scope.Key() { + return filter.ErrUnsupportedScope + } + after.UUID = before.UUID + after, err = filter.NormalizeRule(after) + if err != nil { + return err + } + beforeKey, err := firewallRuleDuplicateKey(observed, false) + if err != nil { + return err + } + afterKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: after, ParseStatus: filter.ParseStatusSupported}, false) + if err != nil { + return err + } + if beforeKey == afterKey { + if after.OrderIndex == nil || observed.Locator.Position != nil && *after.OrderIndex == int64(*observed.Locator.Position) { + if request.Rule != nil { + return saveFirewallDescription(ctx, observed, request.Rule.Description) + } + if request.Description != nil { + return saveFirewallDescription(ctx, observed, *request.Description) + } + return nil + } + } else { + var matchKey string + if runtime.Provider() == filter.ProviderUFW { + matchKey, err = filter.RuleMatchKey(after) + if err != nil { + return err + } + } + for _, candidate := range snapshot.Rules { + instanceKey, err := filter.InstanceKey(candidate) + if err != nil { + return err + } + if instanceKey == request.InstanceKey { + continue + } + key, err := firewallRuleDuplicateKey(candidate, false) + if err != nil { + return err + } + if key == afterKey { + return buserr.New("ErrRecordExist") + } + if runtime.Provider() == filter.ProviderUFW && candidate.ParseStatus == filter.ParseStatusSupported { + candidateMatch, err := filter.RuleMatchKey(candidate.Rule) + if err != nil { + return err + } + if candidateMatch == matchKey && candidate.Rule.Action != after.Action { + return buserr.New("ErrFirewallRuleConflict") + } + } + } + } + description := before.Description + id, err := filter.DescriptionID(observed) + if err != nil { + return err + } + stored, err := settingRepo.GetDescription(settingRepo.WithByDescriptionID(id), repo.WithByType("firewall")) + if err == nil { + description = stored.Description + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if request.Rule != nil { + description = request.Rule.Description + } + if request.Description != nil { + description = *request.Description + } + plan, err := applyFirewallChanges(runtime, ctx, snapshot, []filter.RuleChange{{ + Operation: operation, Before: &before, After: &after, Locator: &observed.Locator, PreviousMarker: observed.Marker, + }}) + if err != nil { + return err + } + return saveFirewallDescription(ctx, plan.Rules[0].Expected, description) +} + +func firewallBackupDirectory(subsystem string) (string, error) { + var directory string + switch subsystem { + case "system": + directory = "host" + case "docker": + directory = "docker" + case "forwarding": + directory = "forward" + default: + return "", filter.ErrInvalidRule + } + return filepath.Join(global.Dir.BaseDir, "1panel", "backup", "firewall", directory), nil +} + +func readFirewallBackup(name string) ([]dto.FirewallRuleExportItem, error) { + if name == "" || name != filepath.Base(name) || filepath.Ext(name) != ".json" { + return nil, filter.ErrInvalidRule + } + directory, err := firewallBackupDirectory("system") + if err != nil { + return nil, err + } + file, err := os.OpenInRoot(directory, name) + if err != nil { + return nil, err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() { + return nil, filter.ErrInvalidRule + } + data, err := io.ReadAll(file) + if err != nil { + return nil, err + } + var items []dto.FirewallRuleExportItem + if err := json.Unmarshal(data, &items); err != nil { + return nil, err + } + if items == nil { + return nil, filter.ErrInvalidRule + } + for _, item := range items { + if err := item.Scope.ValidateMVP(); err != nil { + return nil, err + } + switch item.ParseStatus { + case "", filter.ParseStatusSupported: + if _, err := filter.NormalizeRule(item.FirewallRule); err != nil { + return nil, err + } + case filter.ParseStatusPartial, filter.ParseStatusOpaque: + if item.Raw == "" { + return nil, filter.ErrInvalidRule + } + default: + return nil, filter.ErrInvalidRule + } + } + return items, nil +} + +func writeFirewallJSON(directory, name string, value any) (string, error) { + data, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + if err := os.MkdirAll(directory, 0700); err != nil { + return "", err + } + file, err := os.CreateTemp(directory, ".firewall-*.tmp") + if err != nil { + return "", err + } + temporary := file.Name() + defer os.Remove(temporary) + _, writeErr := file.Write(data) + syncErr := file.Sync() + closeErr := file.Close() + if err := errors.Join(writeErr, syncErr, closeErr); err != nil { + return "", err + } + target := filepath.Join(directory, name) + if err := os.Rename(temporary, target); err != nil { + return "", err + } + return target, nil +} + +func readFirewallSubsystemBackup(name, subsystem string) (dto.FirewallSubsystemBackup, error) { + var backup dto.FirewallSubsystemBackup + if name == "" || name != filepath.Base(name) || (filepath.Ext(name) != ".json" && name != subsystem+"-iptables.rules" && name != subsystem+"-nftables.rules") { + return backup, filter.ErrInvalidRule + } + directory, err := firewallBackupDirectory(subsystem) + if err != nil { + return backup, err + } + if filepath.Ext(name) == ".rules" { + directory = global.Dir.FirewallDir + } + file, err := os.OpenInRoot(directory, name) + if err != nil { + return backup, err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return backup, err + } + if !info.Mode().IsRegular() { + return backup, filter.ErrInvalidRule + } + data, err := io.ReadAll(file) + if err != nil { + return backup, err + } + if err := json.Unmarshal(data, &backup); err != nil { + return backup, err + } + if backup.Subsystem != subsystem || (backup.Provider != filter.ProviderIptables && backup.Provider != filter.ProviderNftables) { + return backup, filter.ErrInvalidRule + } + for _, family := range backup.Families { + if family != forwarding.FamilyIPv4 && family != forwarding.FamilyIPv6 { + return backup, filter.ErrInvalidRule + } + } + switch subsystem { + case "forwarding": + if backup.Forwarding == nil || backup.Docker != nil { + return backup, filter.ErrInvalidRule + } + for i, rule := range backup.Forwarding { + normalized, err := forwarding.NormalizeRule(rule) + if err != nil { + return backup, err + } + backup.Forwarding[i] = normalized + } + case "docker": + if backup.Docker == nil || len(backup.Forwarding) > 0 { + return backup, filter.ErrInvalidRule + } + seen := make(map[string]bool) + for i, policy := range backup.Docker.Policies { + if _, err := uuid.Parse(policy.UUID); err != nil { + return backup, filter.ErrInvalidRule + } + normalized := policy + if len(policy.NativeRules) == 0 { + var err error + normalized, err = normalizeDockerFirewallPolicy(policy) + if err != nil { + return backup, err + } + } + if seen[normalized.UUID] { + return backup, filter.ErrInvalidRule + } + seen[normalized.UUID] = true + backup.Docker.Policies[i] = normalized + } + for _, policy := range backup.Docker.Policies { + for _, rule := range policy.NativeRules { + if rule.Family != dockerfirewall.FamilyIPv4 && rule.Family != dockerfirewall.FamilyIPv6 { + return backup, filter.ErrInvalidRule + } + if len(rule.Tokens) == 0 { + return backup, filter.ErrInvalidRule + } + for _, token := range rule.Tokens { + if strings.ContainsAny(token, "\r\n\x00") { + return backup, filter.ErrInvalidRule + } + } + if backup.Provider == filter.ProviderIptables && (len(rule.Tokens) < 3 || rule.Tokens[0] != "-A" || rule.Tokens[1] != dockerfirewall.Chain) { + return backup, filter.ErrInvalidRule + } + } + } + default: + return backup, filter.ErrInvalidRule + } + return backup, nil +} + +func forwardingBackup(manager forwarding.Adapter, rules []forwarding.Rule) (dto.FirewallSubsystemBackup, error) { + if rules == nil { + rules = []forwarding.Rule{} + } + backup := dto.FirewallSubsystemBackup{Subsystem: "forwarding", Provider: filter.Provider(manager.Name()), Forwarding: rules} + for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { + initialized, _, err := manager.FamilyStatus(family) + if family == forwarding.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { + continue + } + if err != nil { + return backup, err + } + if initialized { + backup.Families = append(backup.Families, family) + } + } + return backup, nil +} + +func persistForwardingRules(manager forwarding.Adapter, rules []forwarding.Rule) error { + backup, err := forwardingBackup(manager, rules) + if err != nil { + return err + } + _, err = writeFirewallJSON(global.Dir.FirewallDir, "forwarding-"+manager.Name()+".rules", backup) + return err +} + +func persistDockerRules(backend string, inventory dockerfirewall.PolicyInventory) error { + _, err := writeFirewallJSON(global.Dir.FirewallDir, "docker-"+backend+".rules", dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}) + return err +} + +func dockerPolicyEndpointKey(policy dockerfirewall.Policy) string { + return strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00") +} + +func dockerGuardInventoryEndpoints(inventory dockerfirewall.PolicyInventory) []dto.DockerPortGuardEndpoint { + result := make([]dto.DockerPortGuardEndpoint, 0, len(inventory.Policies)) + for _, policy := range inventory.Policies { + if _, err := normalizeDockerFirewallPolicy(policy); err != nil { + policy.Mode = "" + } + result = append(result, dto.DockerPortGuardEndpoint{Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: policy.Sources, TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath}) + } + return result +} + +func applyDockerPolicies(ctx context.Context, runtime dockerfirewall.Runtime, backend string, inventory dockerfirewall.PolicyInventory, desired []dockerfirewall.Policy) error { + families := make(map[string]bool) + for _, policy := range desired { + families[policy.Family] = true + } + for family := range families { + initialized, err := runtime.Initialized(family) + if err != nil { + return err + } + if !initialized { + return filter.ErrProviderUnavailable + } + } + if err := ctx.Err(); err != nil { + return err + } + if err := runtime.ReplacePolicies(desired, inventory); err != nil { + return err + } + if err := ctx.Err(); err != nil { + return err + } + inventory.Policies = desired + return persistDockerRules(backend, inventory) +} + +func (s *DockerPortGuardService) initialize(ctx context.Context, request dto.DockerPortGuardOperation, t *task.Task) error { + runtime, backend, err := s.runtimeForDocker(ctx) + if err != nil { + return err + } + inventory, err := runtime.ListPolicies() + if err != nil { + return err + } + if request.BackupFile != "" { + backup, err := readFirewallSubsystemBackup(request.BackupFile, "docker") + if err != nil { + return err + } + inventory, err = mergeDockerBackup(inventory, backup, backend, t) + if err != nil { + return err + } + } + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if !slices.Contains(families, constant.FirewallFamilyIPv6) { + if t != nil { + t.Logf("IPv6 firewall support is disabled; skipping IPv6 Docker chain initialization") + } else { + global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 Docker chain initialization") + } + } + if err := runtime.Initialize(inventory.Policies, inventory, families...); err != nil { + return err + } + if err := ctx.Err(); err != nil { + return err + } + if err := persistDockerRules(backend, inventory); err != nil { + return err + } + if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil { + return err + } + if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil { + return err + } + return nil +} + +func mergeDockerBackup(current dockerfirewall.PolicyInventory, backup dto.FirewallSubsystemBackup, backend string, t *task.Task) (dockerfirewall.PolicyInventory, error) { + families, err := loadFirewallFamilies() + if err != nil { + return current, err + } + skippedIPv6 := 0 + defer func() { logFirewallIPv6Skipped(t, "Docker", skippedIPv6) }() + empty := len(current.Policies) == 0 + byEndpoint := make(map[string]bool, len(current.Policies)) + byUUID := make(map[string]bool, len(current.Policies)) + for _, policy := range current.Policies { + byEndpoint[dockerPolicyEndpointKey(policy)] = true + byUUID[policy.UUID] = true + } + if current.RuleOrders == nil { + current.RuleOrders = make(map[string][]int64) + } + var maxOrder int64 + for _, existing := range current.Policies { + for _, rule := range existing.NativeRules { + maxOrder = max(maxOrder, rule.Order) + } + for _, order := range current.RuleOrders[existing.Family+"\x00"+existing.UUID] { + maxOrder = max(maxOrder, order) + } + } + for _, policy := range backup.Docker.Policies { + if policy.Family == dockerfirewall.FamilyIPv6 && !slices.Contains(families, dockerfirewall.FamilyIPv6) { + skippedIPv6++ + continue + } + if byEndpoint[dockerPolicyEndpointKey(policy)] { + if t != nil { + t.Logf("%s %s %s:%d %s: %v", policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), buserr.New("ErrRecordExist")) + } + continue + } + if byUUID[policy.UUID] { + return current, filter.ErrRuleStale + } + if string(backup.Provider) != backend { + policy, err = normalizeDockerFirewallPolicy(policy) + if err != nil { + return current, err + } + policy, err = dockerfirewall.ConvertPolicyBackend(policy, string(backup.Provider), backend) + if err != nil { + return current, err + } + } + if !empty { + policy.NativeRules = append([]dockerfirewall.NativeRule(nil), policy.NativeRules...) + for i := range policy.NativeRules { + maxOrder++ + policy.NativeRules[i].Order = maxOrder + } + } + current.Policies = append(current.Policies, policy) + byEndpoint[dockerPolicyEndpointKey(policy)], byUUID[policy.UUID] = true, true + key := policy.Family + "\x00" + policy.UUID + if empty { + current.RuleOrders[key] = backup.Docker.RuleOrders[key] + } + } + return current, nil +} + +func RestoreDockerPortGuard(ctx context.Context) error { + return newDockerPortGuardService().Restore(ctx) +} + +func RestoreDockerPortGuardBestEffort(ctx context.Context) { + if err := RestoreDockerPortGuard(ctx); err != nil { + global.LOG.Warnf("restore Docker port guard failed: %v", err) + } +} + +func loadFirewallFamilies() ([]string, error) { + value, err := settingRepo.GetValueByKey(constant.FirewallIPv6SupportKey) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + if value == constant.StatusDisable { + return []string{constant.FirewallFamilyIPv4}, nil + } + return []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}, nil +} + +func ipv4PortWhitelist(rules []firewall.PortWhitelist) []firewall.PortWhitelist { + result := make([]firewall.PortWhitelist, 0, len(rules)) + for _, rule := range rules { + sources := make([]string, 0, len(rule.Sources)) + for _, source := range rule.Sources { + if !strings.Contains(source, ":") { + sources = append(sources, source) + } + } + if len(sources) == 0 { + continue + } + rule.Sources = sources + result = append(result, rule) + } + return result +} + +func validateFirewallWhitelistFamilies(rules []firewall.PortWhitelist) error { + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if !slices.Contains(families, constant.FirewallFamilyIPv6) { + for _, rule := range rules { + for _, source := range rule.Sources { + if strings.Contains(source, ":") { + return fmt.Errorf("IPv6 firewall support is disabled") + } + } + } + } + return nil +} + +func logFirewallIPv6Skipped(t *task.Task, subsystem string, count int) { + if count == 0 { + return + } + message := fmt.Sprintf("IPv6 firewall support is disabled; skipped %d IPv6 %s rule(s)", count, subsystem) + if t != nil { + t.Log(message) + } else { + global.LOG.Info(message) + } +} diff --git a/agent/app/service/forward.go b/agent/app/service/forward.go index 23dcfba57..bfb68d5d0 100644 --- a/agent/app/service/forward.go +++ b/agent/app/service/forward.go @@ -5,19 +5,16 @@ import ( "errors" "fmt" "os" + "slices" "strconv" "strings" "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" - "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" @@ -25,51 +22,41 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" ) -const ( - forwardingSyncConverged = "converged" - forwardingSyncMissing = "missing" - forwardingSyncRuntimeOnly = "runtime_only" -) - type IForwardingService interface { - LoadBaseInfo() (dto.FirewallSubsystemStatus, error) - SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) + LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) + ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error) + SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) - Enable() error + Enable(ctx context.Context) error QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) Restore(context.Context) error } type ForwardingService struct { - clientFactory func() (forwarding.Adapter, error) - rules repo.IForwardingRuleRepo - enabled func() (bool, error) - persistBackend func(string) error - markEnabled func() error + clientFactory func(context.Context) (forwarding.Adapter, error) } var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected") var forwardingMutationMu sync.Mutex -var ( - forwardingSyncStateMu sync.RWMutex - forwardingLastSyncErr error -) - -func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) { +func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) { + families, err := loadFirewallFamilies() + if err != nil { + return dto.FirewallSubsystemStatus{}, err + } selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey) selected = strings.TrimSpace(selected) if selected == "" { selected = constant.FirewallProviderIptables } baseInfo := dto.FirewallSubsystemStatus{ - Version: "-", Name: selected, Backend: selected, SyncError: lastForwardingSyncError(), + Version: "-", Name: selected, Backend: selected, IPv6Enabled: len(families) > 1, } if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables { baseInfo.Name += "-forward" } - manager, err := s.clientFactory() + manager, err := s.clientFactory(ctx) if err != nil { if errors.Is(err, errForwardingBackendUnavailable) { baseInfo.Reason = constant.FirewallBackendNotInstalled @@ -82,7 +69,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) return baseInfo, err } version, versionErr := client.Version() - status, statusErr := loadForwardingFirewallOverview(manager) + status, statusErr := loadForwardingFirewallOverview(manager, families) if err := errors.Join(versionErr, statusErr); err != nil { return baseInfo, err } @@ -102,7 +89,10 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {"iptables", &baseInfo.IPv4}, {"ip6tables", &baseInfo.IPv6}, } { - policy, err := loadForwardPolicy(family.command) + if family.command == "ip6tables" && !baseInfo.IPv6Enabled { + continue + } + policy, err := loadForwardPolicy(ctx, family.command) if err != nil { global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err) continue @@ -112,66 +102,48 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) return baseInfo, nil } -func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) { - if request.Strategy != "" { - return 0, nil, nil +func (s *ForwardingService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) { + var manager forwarding.Adapter + var err error + if provider == "" { + manager, err = s.clientFactory(ctx) + } else { + manager, err = newForwardingAdapterFor(ctx, string(provider)) } - stored, err := s.rules.List(context.Background()) + if err != nil { + return dto.FirewallSubsystemBackup{}, err + } + rules, err := manager.List() + if err != nil { + return dto.FirewallSubsystemBackup{}, err + } + return forwardingBackup(manager, rules) +} + +func (s *ForwardingService) SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) { + manager, err := s.clientFactory(ctx) if err != nil { return 0, nil, err } - manager, err := s.clientFactory() - if err != nil { - return 0, nil, err - } - runtime, err := manager.List() - if err != nil { - return 0, nil, err - } - inventory, err := mergeForwardingInventory(stored, runtime) + rules, err := manager.List() if err != nil { return 0, nil, err } + items := make([]dto.ForwardRule, 0, len(rules)) keyword := strings.ToLower(strings.TrimSpace(request.Info)) - filtered := inventory[:0] - for _, item := range inventory { - if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) { - filtered = append(filtered, item) + for _, rule := range rules { + if keyword != "" && !strings.Contains(strings.ToLower(strings.Join([]string{rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface}, " ")), keyword) { + continue } + items = append(items, dto.ForwardRule{Num: strconv.Itoa(len(items) + 1), Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface}) } - inventory = filtered - total := len(inventory) - start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize + total := len(items) if request.All { - start, end = 0, total + return int64(total), items, nil } - if start > total { - return int64(total), make([]dto.ForwardRule, 0), nil - } - if end > total { - end = total - } - pageRules := inventory[start:end] - var items []dto.ForwardRule - if pageRules != nil { - items = make([]dto.ForwardRule, 0, len(pageRules)) - } - for index, item := range pageRules { - items = append(items, dto.ForwardRule{ - ID: item.ID, - Num: strconv.Itoa(start + index + 1), - Family: item.Rule.Family, - Protocol: item.Rule.Protocol, - Port: item.Rule.Port, - TargetIP: item.Rule.TargetIP, - TargetPort: item.Rule.TargetPort, - Interface: item.Rule.Interface, - IsDesired: item.IsDesired, - IsRuntime: item.IsRuntime, - SyncStatus: item.SyncStatus(), - }) - } - return int64(total), items, nil + start := min(max(request.Page-1, 0)*max(request.PageSize, 1), total) + end := min(start+max(request.PageSize, 1), total) + return int64(total), items[start:end], nil } func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) { @@ -198,7 +170,7 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi return dto.FilterChainOperationResponse{}, err } taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { - return s.operateRules(t.TaskCtx, request, t) + return s.operateRules(t.TaskCtx, request, t, false) }, nil, 0, 0) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { taskItem.LogFailedWithErr(taskItem.Name, err) @@ -209,94 +181,142 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil } -func (s *ForwardingService) Enable() error { +func (s *ForwardingService) Enable(ctx context.Context) error { forwardingMutationMu.Lock() defer forwardingMutationMu.Unlock() - manager, err := s.clientFactory() + manager, err := s.clientFactory(ctx) if err != nil { - recordForwardingSyncError(err) return err } - if err := s.persistForwardingEnabled(); err != nil { - recordForwardingSyncError(err) - return err - } - if err := s.initializeForwarding(manager); err != nil { - recordForwardingSyncError(err) - return err - } - rules, err := s.rules.List(context.Background()) + rules, err := manager.List() if err != nil { - recordForwardingSyncError(err) return err } - err = manager.ReplaceRules(forwardingRulesFromModels(rules)) - recordForwardingSyncError(err) - return err + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if err := initializeForwarding(manager, families); err != nil { + return err + } + return persistForwardingRules(manager, rules) } func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { return dto.FilterChainOperationResponse{}, err } - taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0) - if err != nil { - return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err) - } - var manager forwarding.Adapter - var backend string - taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error { - forwardingMutationMu.Lock() - defer forwardingMutationMu.Unlock() + var backup dto.FirewallSubsystemBackup + if request.BackupFile != "" { var err error - manager, err = s.clientFactory() + backup, err = readFirewallSubsystemBackup(request.BackupFile, "forwarding") if err != nil { - recordForwardingSyncError(err) - return err + return dto.FilterChainOperationResponse{}, err } - backend = manager.Name() - t.Logf("backend=%s", backend) - if err := s.persistForwardingEnabled(); err != nil { - recordForwardingSyncError(err) - return err - } - if err := s.initializeForwarding(manager); err != nil { - recordForwardingSyncError(err) - return err - } - return nil - }, nil) - taskItem.AddSubTask(i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func(t *task.Task) error { - forwardingMutationMu.Lock() - defer forwardingMutationMu.Unlock() - rules, err := s.rules.List(t.TaskCtx) - if err != nil { - recordForwardingSyncError(err) - return err - } - err = manager.ReplaceRules(forwardingRulesFromModels(rules)) - recordForwardingSyncError(err) - return err - }, nil) - if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { - return dto.FilterChainOperationResponse{}, fmt.Errorf("save forwarding initialization task: %w", err) } - go func() { _ = taskItem.Execute() }() - return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil + operations := dto.ForwardRuleOperate{Rules: make([]dto.ForwardRuleOperation, 0, len(backup.Forwarding))} + for _, rule := range backup.Forwarding { + operations.Rules = append(operations.Rules, dto.ForwardRuleOperation{Operation: "add", Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface}) + } + return queueFirewallRuleTask(firewallTaskForwarding, task.TaskExec, request.TaskID, nil, func(t *task.Task) error { + return s.operateRules(t.TaskCtx, operations, t, true, backup.Families...) + }) +} + +func (s *ForwardingService) Restore(ctx context.Context) error { + forwardingMutationMu.Lock() + defer forwardingMutationMu.Unlock() + status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey) + if err != nil || status != constant.StatusEnable { + return err + } + manager, err := s.clientFactory(ctx) + if err != nil { + return err + } + backup, err := readFirewallSubsystemBackup("forwarding-"+manager.Name()+".rules", "forwarding") + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + families, err := loadFirewallFamilies() + if err != nil { + return err + } + if !slices.Contains(families, constant.FirewallFamilyIPv6) { + before := len(backup.Forwarding) + backup.Forwarding = slices.DeleteFunc(backup.Forwarding, func(rule forwarding.Rule) bool { return rule.Family == constant.FirewallFamilyIPv6 }) + logFirewallIPv6Skipped(nil, "forwarding startup", before-len(backup.Forwarding)) + } + missing := make(map[string]bool) + needsBind := false + for _, family := range families { + initialized, bound, err := manager.FamilyStatus(family) + if err != nil { + return err + } + missing[family] = !initialized + needsBind = needsBind || (initialized && !bound) + } + restoreIPv6 := slices.Contains(backup.Families, forwarding.FamilyIPv6) + for _, rule := range backup.Forwarding { + restoreIPv6 = restoreIPv6 || rule.Family == forwarding.FamilyIPv6 + } + if !missing[forwarding.FamilyIPv4] && (!missing[forwarding.FamilyIPv6] || !restoreIPv6) && !needsBind { + return nil + } + current, err := manager.List() + if err != nil { + return err + } + seen := make(map[string]int, len(current)) + for _, rule := range current { + seen[rule.Identity()]++ + } + for _, family := range families { + if family == forwarding.FamilyIPv6 && missing[family] && !restoreIPv6 { + continue + } + if err := manager.OperateFamily(family, missing[family]); err != nil { + return err + } + } + for _, family := range families { + if !missing[family] { + continue + } + rules := make([]forwarding.Rule, 0) + for _, rule := range backup.Forwarding { + if rule.Family != family { + continue + } + if seen[rule.Identity()] > 0 { + seen[rule.Identity()]-- + continue + } + rules = append(rules, rule) + } + if err := manager.CreateRules(ctx, rules); err != nil { + return err + } + } + return nil } func NewIForwardingService() IForwardingService { return newForwardingService() } -func loadForwardPolicy(command string) (string, error) { +func loadForwardPolicy(ctx context.Context, command string) (string, error) { if !cmd.Which(command) { command += "-nft" if !cmd.Which(command) { return "", nil } } - output, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD") + output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD") if err != nil { return "", err } @@ -311,256 +331,3 @@ func loadForwardPolicy(command string) (string, error) { } return "", errors.New("FORWARD default policy was not found") } - -func lastForwardingSyncError() string { - forwardingSyncStateMu.RLock() - defer forwardingSyncStateMu.RUnlock() - if forwardingLastSyncErr == nil { - return "" - } - return forwardingLastSyncErr.Error() -} - -func mergeForwardingInventory(stored []model.ForwardingRule, runtime []forwarding.Rule) ([]forwardingInventoryItem, error) { - items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime)) - byIdentity := make(map[string]int, len(stored)+len(runtime)) - for _, record := range stored { - rule, err := forwarding.NormalizeRule(forwarding.Rule{ - Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP, - TargetPort: record.TargetPort, Interface: record.Interface, - }) - if err != nil { - return nil, fmt.Errorf("normalize desired forwarding rule: %w", err) - } - key := rule.Identity() - byIdentity[key] = len(items) - items = append(items, forwardingInventoryItem{ID: record.ID, Rule: rule, IsDesired: true}) - } - for _, observed := range runtime { - rule, err := forwarding.NormalizeRule(observed) - if err != nil { - return nil, fmt.Errorf("normalize runtime forwarding rule: %w", err) - } - key := rule.Identity() - if index, exists := byIdentity[key]; exists { - items[index].IsRuntime = true - continue - } - byIdentity[key] = len(items) - items = append(items, forwardingInventoryItem{Rule: rule, IsRuntime: true}) - } - return items, nil -} - -func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool { - values := []string{ - item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP, - item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(), - } - for _, value := range values { - if strings.Contains(strings.ToLower(value), keyword) { - return true - } - } - return false -} - -func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate, t *task.Task) (resultErr error) { - forwardingMutationMu.Lock() - defer forwardingMutationMu.Unlock() - if err := ctx.Err(); err != nil { - return err - } - type operationBatch struct { - operation forwarding.OperationType - rules []forwarding.Rule - } - groups := make([]operationBatch, 0) - for _, operation := range request.Rules { - kind := forwarding.OperationType(operation.Operation) - if kind != forwarding.OperationAdd && kind != forwarding.OperationRemove { - return fmt.Errorf("unsupported forwarding operation %q", operation.Operation) - } - if len(groups) == 0 || groups[len(groups)-1].operation != kind { - groups = append(groups, operationBatch{operation: kind}) - } - for _, protocol := range strings.Split(operation.Protocol, "/") { - rule, err := forwarding.NormalizeRule(forwarding.Rule{ - Family: operation.Family, Protocol: protocol, Port: operation.Port, - TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface, - }) - if err != nil { - return err - } - groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule) - } - } - stored, err := s.rules.List(ctx) - if err != nil { - return err - } - byIdentity := make(map[string]model.ForwardingRule, len(stored)) - for index, rule := range forwardingRulesFromModels(stored) { - normalized, err := forwarding.NormalizeRule(rule) - if err != nil { - return err - } - byIdentity[normalized.Identity()] = stored[index] - } - succeeded, failed, skipped := 0, 0, 0 - var nativeFailure error - defer func() { - recordForwardingSyncError(errors.Join(resultErr, nativeFailure)) - if t != nil { - t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{"succeeded": succeeded, "failed": failed})) - if skipped > 0 { - t.Logf("%s: %d", i18n.GetMsgByKey("FirewallCreateRuleSkipped"), skipped) - } - } - }() - record := func(operation forwarding.OperationType, rule forwarding.Rule, status string, cause error) { - label := fmt.Sprintf("%s %s %s %s -> %s:%s", operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort) - switch status { - case "skipped": - skipped++ - if t != nil { - t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause) - } - case "failed": - failed++ - if t != nil { - t.LogFailedWithErr(label, cause) - } - default: - succeeded++ - if t != nil { - t.LogSuccess(label) - } - } - } - if len(request.Rules) == 2 && len(groups) == 2 && groups[0].operation == forwarding.OperationRemove && groups[1].operation == forwarding.OperationAdd { - old := make(map[string]bool, len(groups[0].rules)) - for _, rule := range groups[0].rules { - old[rule.Identity()] = true - } - unchanged := len(old) == len(groups[1].rules) - duplicate := false - for _, rule := range groups[1].rules { - key := rule.Identity() - unchanged = unchanged && old[key] - if _, exists := byIdentity[key]; exists && !old[key] { - duplicate = true - } - } - if unchanged || duplicate { - for _, group := range groups { - for _, rule := range group.rules { - record(group.operation, rule, "skipped", buserr.New("ErrRecordExist")) - } - } - return nil - } - for _, rule := range groups[1].rules { - if rule.Family != forwarding.FamilyIPv6 { - continue - } - interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile) - if err != nil { - return fmt.Errorf("check IPv6 Router Advertisement: %w", err) - } - if len(interfaces) > 0 { - return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", ")) - } - break - } - } - var client forwarding.Adapter - var failures []error - for _, group := range groups { - byFamily := make(map[string][]forwarding.Rule, 2) - seen := make(map[string]bool, len(group.rules)) - for _, rule := range group.rules { - key := rule.Identity() - _, exists := byIdentity[key] - if seen[key] || (group.operation == forwarding.OperationAdd && exists) { - record(group.operation, rule, "skipped", buserr.New("ErrRecordExist")) - continue - } - seen[key] = true - byFamily[rule.Family] = append(byFamily[rule.Family], rule) - } - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - rules := byFamily[family] - if len(rules) == 0 { - continue - } - err := ctx.Err() - if err == nil && client == nil { - var enabled bool - enabled, err = s.forwardingEnabled() - if err == nil && !enabled { - err = fmt.Errorf("%w: forwarding is not initialized", filter.ErrProviderUnavailable) - } - if err == nil { - client, err = s.clientFactory() - } - } - if err == nil { - if group.operation == forwarding.OperationAdd { - err = client.CreateRules(ctx, rules) - } else { - err = client.DeleteRules(ctx, rules) - } - } - if err != nil { - nativeFailure = errors.Join(nativeFailure, err) - if !request.ForceDelete || !forwardingOperationsOnlyRemove(request.Rules) || ctx.Err() != nil { - failures = append(failures, err) - for _, rule := range rules { - record(group.operation, rule, "failed", err) - } - continue - } - if t != nil { - t.Logf("force delete database records: %v", err) - } - } - for start := 0; start < len(rules); start += 500 { - batch := rules[start:min(start+500, len(rules))] - records := make([]model.ForwardingRule, 0, len(batch)) - ids := make([]uint, 0, len(batch)) - for _, rule := range batch { - if group.operation == forwarding.OperationAdd { - records = append(records, model.ForwardingRule{Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface}) - } else if stored, exists := byIdentity[rule.Identity()]; exists { - ids = append(ids, stored.ID) - } - } - if group.operation == forwarding.OperationAdd { - err = s.rules.CreateBatch(context.WithoutCancel(ctx), records) - } else { - err = s.rules.DeleteBatch(context.WithoutCancel(ctx), ids) - } - if err != nil { - failures = append(failures, err) - } - for index, rule := range batch { - if err != nil { - record(group.operation, rule, "failed", err) - continue - } - if group.operation == forwarding.OperationAdd { - byIdentity[rule.Identity()] = records[index] - } else { - delete(byIdentity, rule.Identity()) - } - record(group.operation, rule, "succeeded", nil) - } - } - } - if group.operation == forwarding.OperationRemove && len(failures) > 0 { - return errors.Join(failures...) - } - } - return errors.Join(failures...) -} diff --git a/agent/app/service/setting.go b/agent/app/service/setting.go index 9051b2263..41b76c4b6 100644 --- a/agent/app/service/setting.go +++ b/agent/app/service/setting.go @@ -1,6 +1,7 @@ package service import ( + "context" "encoding/base64" "encoding/json" "errors" @@ -44,6 +45,7 @@ type ISettingService interface { GetLocalConnForSSH() (dto.SSHConnData, error) SaveDescription(req dto.CommonDescription) error + CleanupDescriptions(context.Context) (int64, error) } func NewISettingService() ISettingService { diff --git a/agent/app/service/setting_description.go b/agent/app/service/setting_description.go new file mode 100644 index 000000000..ecbf1d949 --- /dev/null +++ b/agent/app/service/setting_description.go @@ -0,0 +1,114 @@ +package service + +import ( + "context" + "errors" + "fmt" + + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" + "github.com/docker/docker/api/types/container" +) + +func (u *SettingService) CleanupDescriptions(ctx context.Context) (int64, error) { + var deleted int64 + var failures []error + for _, kind := range []string{"container", "firewall", "firewall-docker"} { + count, err := func() (int64, error) { + switch kind { + case "container": + return cleanupUnusedDescriptions(ctx, kind, loadContainerDescriptionIDs) + case "firewall": + firewallRuleMutationMu.Lock() + defer firewallRuleMutationMu.Unlock() + return cleanupUnusedDescriptions(ctx, kind, loadHostFirewallDescriptionIDs) + default: + return cleanupUnusedDescriptions(ctx, kind, nil) + } + }() + deleted += count + if err != nil { + failures = append(failures, fmt.Errorf("%s: %w", kind, err)) + } + } + return deleted, errors.Join(failures...) +} + +func cleanupUnusedDescriptions(ctx context.Context, kind string, loadIDs func(context.Context) (map[string]bool, error)) (int64, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + descriptions, err := settingRepo.GetDescriptionList(repo.WithByType(kind)) + if err != nil || len(descriptions) == 0 { + return 0, err + } + var active map[string]bool + if loadIDs != nil { + active, err = loadIDs(ctx) + if err != nil { + return 0, err + } + } + ids, empty := make([]string, 0), make([]string, 0) + for _, description := range descriptions { + if !active[description.ID] { + ids = append(ids, description.ID) + } else if description.Description == "" && !description.IsPinned { + empty = append(empty, description.ID) + } + } + deleted, err := settingRepo.DeleteDescriptions(ctx, kind, ids, false) + if err != nil { + return deleted, err + } + count, err := settingRepo.DeleteDescriptions(ctx, kind, empty, true) + return deleted + count, err +} + +func loadContainerDescriptionIDs(ctx context.Context) (map[string]bool, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + containers, err := client.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + return nil, err + } + ids := make(map[string]bool, len(containers)) + for _, item := range containers { + ids[item.ID] = true + } + return ids, nil +} + +func loadHostFirewallDescriptionIDs(ctx context.Context) (map[string]bool, error) { + providers := lifecycle.InstalledProviders() + if len(providers) == 0 { + return nil, filter.ErrProviderUnavailable + } + service := newFirewallService() + ids := make(map[string]bool) + for _, name := range providers { + provider := filter.Provider(name) + inventory, err := service.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider)) + if err != nil { + return nil, err + } + for _, notice := range inventory.Notices { + if notice.Code == filter.ScopeNoticeFamilyUnavailable || notice.Code == filter.ScopeNoticeManagedScopeInactive { + return nil, fmt.Errorf("%w: %s %s", filter.ErrInventoryUnavailable, name, notice.Code) + } + } + for _, item := range inventory.Items { + id, err := filter.DescriptionID(*item.Observed) + if err != nil { + return nil, err + } + ids[id] = true + } + } + return ids, nil +} diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index a62cd236f..7974f4b8a 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -265,8 +265,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "应用端口修改 [key]-[name] =\u003e [port]", - "formatEN": "Application port update [key]-[name] =\u003e [port]" + "formatZH": "应用端口修改 [key]-[name] => [port]", + "formatEN": "Application port update [key]-[name] => [port]" }, "/apps/installed/sort/update": { "bodyKeys": [], @@ -679,8 +679,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "容器重命名 [name] =\u003e [newName]", - "formatEN": "rename container [name] =\u003e [newName]" + "formatZH": "容器重命名 [name] => [newName]", + "formatEN": "rename container [name] => [newName]" }, "/containers/repo": { "bodyKeys": [ @@ -831,8 +831,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", - "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" + "formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/auth/expired/reset": { "bodyKeys": [], @@ -1716,8 +1716,8 @@ "output_value": "name" } ], - "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", - "formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" + "formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/enterprise/users/del": { "bodyKeys": [ @@ -1918,8 +1918,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", - "formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]" + "formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]", + "formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]" }, "/core/settings/memo": { "bodyKeys": [], @@ -1948,8 +1948,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统端口 =\u003e [serverPort]", - "formatEN": "update system port =\u003e [serverPort]" + "formatZH": "修改系统端口 => [serverPort]", + "formatEN": "update system port => [serverPort]" }, "/core/settings/proxy/update": { "bodyKeys": [ @@ -1974,8 +1974,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统 ssl =\u003e [ssl]", - "formatEN": "update system ssl =\u003e [ssl]" + "formatZH": "修改系统 ssl => [ssl]", + "formatEN": "update system ssl => [ssl]" }, "/core/settings/terminal/update": { "bodyKeys": [], @@ -1991,8 +1991,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统配置 [key] =\u003e [value]", - "formatEN": "update system setting [key] =\u003e [value]" + "formatZH": "修改系统配置 [key] => [value]", + "formatEN": "update system setting [key] => [value]" }, "/core/settings/upgrade": { "bodyKeys": [ @@ -2000,8 +2000,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新系统 =\u003e [version]", - "formatEN": "upgrade system =\u003e [version]" + "formatZH": "更新系统 => [version]", + "formatEN": "upgrade system => [version]" }, "/core/xapp/appQRCode": { "bodyKeys": [ @@ -2888,8 +2888,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "首页应用 [key] =\u003e 显示:[value]", - "formatEN": "app launcher [key] =\u003e show: [value]" + "formatZH": "首页应用 [key] => 显示:[value]", + "formatEN": "app launcher [key] => show: [value]" }, "/dashboard/quick/change": { "bodyKeys": [], @@ -3017,7 +3017,7 @@ } ], "formatZH": "mysql 数据库 [name] 描述信息修改 [description]", - "formatEN": "The description of the mysql database [name] is modified =\u003e [description]" + "formatEN": "The description of the mysql database [name] is modified => [description]" }, "/databases/grants": { "bodyKeys": [ @@ -3098,7 +3098,7 @@ } ], "formatZH": "mongodb 数据库 [name] 描述信息修改 [description]", - "formatEN": "The description of the mongodb database [name] is modified =\u003e [description]" + "formatEN": "The description of the mongodb database [name] is modified => [description]" }, "/databases/mongodb/password": { "bodyKeys": [ @@ -3183,7 +3183,7 @@ } ], "formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", - "formatEN": "The description of the postgresql database [name] is modified =\u003e [description]" + "formatEN": "The description of the postgresql database [name] is modified => [description]" }, "/databases/pg/password": { "bodyKeys": [ @@ -3288,8 +3288,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]", - "formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]" + "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]", + "formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]" }, "/databases/variables/update": { "bodyKeys": [], @@ -3325,8 +3325,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]", - "formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]" + "formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]", + "formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]" }, "/files/chunkdownload": { "bodyKeys": [ @@ -3416,8 +3416,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改权限 [path] =\u003e [mode]", - "formatEN": "Change mode [path] =\u003e [mode]" + "formatZH": "修改权限 [path] => [mode]", + "formatEN": "Change mode [path] => [mode]" }, "/files/move": { "bodyKeys": [ @@ -3426,8 +3426,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "移动文件 [oldPaths] =\u003e [newPath]", - "formatEN": "Move [oldPaths] =\u003e [newPath]" + "formatZH": "移动文件 [oldPaths] => [newPath]", + "formatEN": "Move [oldPaths] => [newPath]" }, "/files/owner": { "bodyKeys": [ @@ -3437,8 +3437,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改用户/组 [path] =\u003e [user]/[group]", - "formatEN": "Change owner [path] =\u003e [user]/[group]" + "formatZH": "修改用户/组 [path] => [user]/[group]", + "formatEN": "Change owner [path] => [user]/[group]" }, "/files/preview": { "bodyKeys": [ @@ -3472,8 +3472,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "重命名 [oldName] =\u003e [newName]", - "formatEN": "Rename [oldName] =\u003e [newName]" + "formatZH": "重命名 [oldName] => [newName]", + "formatEN": "Rename [oldName] => [newName]" }, "/files/save": { "bodyKeys": [ @@ -3530,8 +3530,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "下载 url =\u003e [path]/[name]", - "formatEN": "Download url =\u003e [path]/[name]" + "formatZH": "下载 url => [path]/[name]", + "formatEN": "Download url => [path]/[name]" }, "/files/wget/process/remove": { "bodyKeys": [ @@ -3681,12 +3681,16 @@ "formatZH": "删除 Docker 端口防护策略 [uuids]", "formatEN": "delete Docker port guard policies [uuids]" }, - "/hosts/firewall/docker/sync": { - "bodyKeys": [], + "/hosts/firewall/family/operate": { + "bodyKeys": [ + "subsystem", + "family", + "operation" + ], + "formatEN": "[operation] [subsystem] [family] firewall chains", + "formatZH": "[operation] [subsystem] [family] 防火墙链", "paramKeys": [], - "beforeFunctions": [], - "formatZH": "同步 Docker 端口防护规则", - "formatEN": "sync Docker port guard rules" + "beforeFunctions": [] }, "/hosts/firewall/filter/operate": { "bodyKeys": [ @@ -3727,13 +3731,6 @@ "formatZH": "添加防火墙规则", "formatEN": "create firewall rules" }, - "/hosts/firewall/rules/adopt": { - "bodyKeys": [], - "paramKeys": [], - "beforeFunctions": [], - "formatZH": "纳管防火墙规则", - "formatEN": "adopt firewall rule" - }, "/hosts/firewall/rules/delete": { "bodyKeys": [], "paramKeys": [], @@ -3743,12 +3740,12 @@ }, "/hosts/firewall/rules/reorder": { "bodyKeys": [ - "uuid" + "instanceKey" ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "调整防火墙规则顺序 [uuid]", - "formatEN": "reorder firewall rule [uuid]" + "formatZH": "调整防火墙规则顺序 [instanceKey]", + "formatEN": "reorder firewall rule [instanceKey]" }, "/hosts/firewall/rules/reset": { "bodyKeys": [], @@ -3757,25 +3754,23 @@ "formatZH": "重置防火墙规则", "formatEN": "reset firewall rules" }, - "/hosts/firewall/rules/sync": { - "bodyKeys": [ - "subsystem", - "sourceProvider", - "targetProvider" - ], - "paramKeys": [], - "beforeFunctions": [], - "formatZH": "同步 [subsystem] 防火墙规则到 [targetProvider]", - "formatEN": "sync [subsystem] firewall rules to [targetProvider]" - }, "/hosts/firewall/rules/update": { "bodyKeys": [ - "uuid" + "instanceKey" ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新防火墙规则 [uuid]", - "formatEN": "update firewall rule [uuid]" + "formatZH": "更新防火墙规则 [instanceKey]", + "formatEN": "update firewall rule [instanceKey]" + }, + "/hosts/firewall/settings/ipv6": { + "bodyKeys": [ + "status" + ], + "formatEN": "Set firewall IPv6 support to [status]", + "formatZH": "设置防火墙 IPv6 支持为 [status]", + "paramKeys": [], + "beforeFunctions": [] }, "/hosts/firewall/settings/operate": { "bodyKeys": [ @@ -3893,8 +3888,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改 SSH 配置 [key] =\u003e [newValue]", - "formatEN": "update SSH setting [key] =\u003e [newValue]" + "formatZH": "修改 SSH 配置 [key] => [newValue]", + "formatEN": "update SSH setting [key] => [newValue]" }, "/hosts/tool/config/set": { "bodyKeys": [ @@ -4155,7 +4150,7 @@ } ], "formatZH": "快照 [name] 描述信息修改 [description]", - "formatEN": "The description of the snapshot [name] is modified =\u003e [description]" + "formatEN": "The description of the snapshot [name] is modified => [description]" }, "/settings/snapshot/import": { "bodyKeys": [ @@ -4247,8 +4242,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统配置 [key] =\u003e [value]", - "formatEN": "update system setting [key] =\u003e [value]" + "formatZH": "修改系统配置 [key] => [value]", + "formatEN": "update system setting [key] => [value]" }, "/toolbox/clam": { "bodyKeys": [ @@ -4366,8 +4361,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改主机参数 [key] =\u003e [value]", - "formatEN": "update device conf [key] =\u003e [value]" + "formatZH": "修改主机参数 [key] => [value]", + "formatEN": "update device conf [key] => [value]" }, "/toolbox/device/update/host": { "bodyKeys": [ @@ -4376,8 +4371,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改主机 Host [key] =\u003e [value]", - "formatEN": "update device host [key] =\u003e [value]" + "formatZH": "修改主机 Host [key] => [value]", + "formatEN": "update device host [key] => [value]" }, "/toolbox/device/update/swap": { "bodyKeys": [ @@ -4405,8 +4400,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]", - "formatEN": "update fail2ban conf [key] =\u003e [value]" + "formatZH": "修改 Fail2ban 配置 [key] => [value]", + "formatEN": "update fail2ban conf [key] => [value]" }, "/toolbox/ftp": { "bodyKeys": [ @@ -4665,8 +4660,8 @@ "output_value": "domain" } ], - "formatZH": "修改默认 server =\u003e [domain]", - "formatEN": "Change default server =\u003e [domain]" + "formatZH": "修改默认 server => [domain]", + "formatEN": "Change default server => [domain]" }, "/websites/del": { "bodyKeys": [ @@ -5429,8 +5424,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新防篡改信息 [website][path] =\u003e [status]", - "formatEN": "update tamper info [website][path] =\u003e [status]" + "formatZH": "更新防篡改信息 [website][path] => [status]", + "formatEN": "update tamper info [website][path] => [status]" }, "/xpack/vllm/command-template/create": { "bodyKeys": [ diff --git a/agent/constant/firewall.go b/agent/constant/firewall.go index dd614667a..b42e97b68 100644 --- a/agent/constant/firewall.go +++ b/agent/constant/firewall.go @@ -17,6 +17,7 @@ const ( ) const ( + FirewallIPv6SupportKey = "FirewallIPv6Support" FirewallSystemBackendKey = "FirewallProvider" FirewallForwardingBackendKey = "ForwardingBackend" FirewallDockerBackendKey = "DockerFirewallBackend" @@ -33,12 +34,7 @@ const ( const ( FirewallSystemAcceptedPortSourcePrefix = "accepted-port:" - FirewallRuleOriginCreated = "created" - FirewallRuleOriginAdopted = "adopted" - FirewallRuleSourceUser = "user" FirewallRuleSourceImported = "imported" - FirewallRuleSourcePanel = "panel" FirewallRuleSourceSecurity = "security" - FirewallRuleSourceApp = "application" ) diff --git a/agent/cron/cron.go b/agent/cron/cron.go index f658cb954..402cba256 100644 --- a/agent/cron/cron.go +++ b/agent/cron/cron.go @@ -1,6 +1,7 @@ package cron import ( + "context" "crypto/rand" "fmt" "math/big" @@ -82,6 +83,17 @@ func Run() { if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil { global.LOG.Errorf("can not add backup token refresh corn job: %s", err.Error()) } + if _, err := global.Cron.AddFunc("@every 240h", func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + deleted, err := service.NewISettingService().CleanupDescriptions(ctx) + if err != nil { + global.LOG.Errorf("clean unused common descriptions: %v", err) + } + global.LOG.Infof("common description cleanup completed, deleted %d records", deleted) + }); err != nil { + global.LOG.Errorf("add common description cleanup job: %v", err) + } var cronJobs []model.Cronjob if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil { diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 0c6dad9ad..54c411f62 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall 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' +ErrDockerForwardPolicyDrop: 'This server restricts network forwarding. Container port protection cannot be enabled at this time.' Firewall: 'Firewall' FirewallTaskHost: 'Host firewall' FirewallTaskForwarding: 'Port forwarding' @@ -698,28 +697,15 @@ 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, {{ .unchanged }} already matching (no changes needed)' -FirewallSyncRuleUnchanged: 'Already matching; no changes needed' -FirewallSyncStep: 'Synchronize rules to {{ .name }}' -FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize' -FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}' -FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted' +FirewallCreateRuleExecutionFailed: 'Rule creation failed; executed commands were not rolled back' FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains' -FirewallRestoreRulesStep: 'Restore database rules to {{ .name }}' FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist' FirewallEnableForwardingStep: 'Enable and bind port forwarding chains' 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' -ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule." -FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list" -FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules" FileTaskCopy: 'Copy files to {{ .dst }}' FileTaskMove: 'Move files to {{ .dst }}' FileTaskCompress: 'Compress files to {{ .dst }}' @@ -727,11 +713,13 @@ FileTaskDecompress: 'Extract files to {{ .dst }}' FileTaskSource: 'Source path: {{ .path }}' FileTaskFormat: 'Archive format: {{ .format }}' FileTaskRename: 'Target filename: {{ .name }}' - ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}." ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend." ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}" +ErrFirewallRuleConflict: "A UFW rule with the same match conditions and a different action already exists. Edit the existing rule instead." +ErrForwardInterfaceNotFound: "Network interface {{ .name }} specified by the forwarding rule does not exist. Check the interface name and retry." ErrSSHPublicKey: "Enter a valid SSH public key" ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase" ErrSSHKeyMismatch: "The SSH public and private keys do not match" + diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 4a4a52204..08da8e837 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall 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' +ErrDockerForwardPolicyDrop: 'Este servidor restringe el reenvío de red. Por ahora no se puede habilitar la protección de puertos de contenedores.' Firewall: 'Firewall' FirewallTaskHost: 'Cortafuegos del host' FirewallTaskForwarding: 'Reenvío de puertos' @@ -698,28 +697,15 @@ 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, {{ .unchanged }} ya coinciden (sin cambios necesarios)' -FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios' -FirewallSyncStep: 'Sincronizar reglas con {{ .name }}' -FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall' -FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}' -FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos' +FirewallCreateRuleExecutionFailed: 'Error al crear la regla. Los comandos ejecutados no se revirtieron' 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' FirewallEnableForwardingStep: 'Habilitar y vincular las cadenas de reenvío de puertos' FirewallRestoreForwardingRulesStep: 'Restaurar las reglas de reenvío de puertos de la base de datos' 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' -ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva." -FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista" -FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema" FileTaskCopy: 'Copiar archivos a {{ .dst }}' FileTaskMove: 'Mover archivos a {{ .dst }}' FileTaskCompress: 'Comprimir archivos en {{ .dst }}' @@ -727,11 +713,13 @@ FileTaskDecompress: 'Extraer archivos a {{ .dst }}' FileTaskSource: 'Ruta de origen: {{ .path }}' FileTaskFormat: 'Formato del archivo: {{ .format }}' FileTaskRename: 'Nombre del archivo de destino: {{ .name }}' - ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}." ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker." ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}" +ErrFirewallRuleConflict: "Ya existe una regla UFW con las mismas condiciones y una acción diferente. Edite la regla existente." +ErrForwardInterfaceNotFound: "La interfaz de red {{ .name }} indicada en la regla de reenvío no existe. Compruebe el nombre e inténtelo de nuevo." ErrSSHPublicKey: "Introduce una clave pública SSH válida" ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original" ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden" + diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index a974edfc3..e5c4f91a4 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -684,8 +684,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید' ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بک‌اند فایروال nftables پشتیبانی کند، سپس داکر را راه‌اندازی مجدد کرده و دوباره تلاش کنید' -ErrDockerForwardPolicyDrop: 'سیاست پیش‌فرض FORWARD برای {{ .family }} برابر DROP است. آن را تغییر دهید و دوباره تلاش کنید' -ErrUFWRuleAdopt: 'مدیریت قانون UFW ناموفق بود: {{ .detail }}. اگر نسخه فعلی UFW قدیمی‌تر از 0.35 است، ابتدا UFW را ارتقا دهید و دوباره تلاش کنید' +ErrDockerForwardPolicyDrop: 'این سرور هدایت ترافیک شبکه را محدود کرده است. در حال حاضر امکان فعال‌سازی محافظت از پورت کانتینرها وجود ندارد.' Firewall: 'فایروال' FirewallTaskHost: 'دیواره آتش میزبان' FirewallTaskForwarding: 'هدایت پورت' @@ -699,28 +698,15 @@ 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 }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)' -FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست' -FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}' -FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود' -FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}' -FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد' +FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. دستورات اجراشده بازگردانی نشدند' FirewallInitializeChainsStep: 'راه‌اندازی و اتصال زنجیره‌های پایه {{ .name }}' -FirewallRestoreRulesStep: 'بازیابی قوانین پایگاه داده در {{ .name }}' FirewallSyncWhitelistStep: 'همگام‌سازی فهرست مجاز پورت‌های فایروال' FirewallEnableForwardingStep: 'فعال‌سازی و اتصال زنجیره‌های هدایت پورت' FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پورت پایگاه داده' FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاست‌ها' FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}' FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker' -ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید." -FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید" -FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود" FileTaskCopy: 'کپی فایل‌ها به {{ .dst }}' FileTaskMove: 'انتقال فایل‌ها به {{ .dst }}' FileTaskCompress: 'فشرده‌سازی فایل‌ها در {{ .dst }}' @@ -728,11 +714,13 @@ FileTaskDecompress: 'استخراج فایل‌ها در {{ .dst }}' FileTaskSource: 'مسیر مبدأ: {{ .path }}' FileTaskFormat: 'قالب بایگانی: {{ .format }}' FileTaskRename: 'نام فایل مقصد: {{ .name }}' - ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید." ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید." ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}" +ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید." +ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخص‌شده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید." ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید" ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند" ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند" + diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 47d72c815..86bba78d7 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください' ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください' -ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD の既定ポリシーが DROP です。変更してから再試行してください' -ErrUFWRuleAdopt: 'UFW ルールの管理に失敗しました:{{ .detail }}。現在の UFW バージョンが 0.35 未満の場合は、UFW をアップグレードしてから再試行してください' +ErrDockerForwardPolicyDrop: 'このサーバーではネットワーク転送が制限されているため、現在コンテナのポート保護を有効にできません。' Firewall: 'ファイアウォール' FirewallTaskHost: 'ホストファイアウォール' FirewallTaskForwarding: 'ポート転送' @@ -698,28 +697,15 @@ 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 }} 件、一致済みで変更不要 {{ .unchanged }} 件' -FirewallSyncRuleUnchanged: '一致済み、変更不要' -FirewallSyncStep: '{{ .name }} にルールを同期' -FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした' -FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化' -FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました' +FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。実行済みのコマンドはロールバックしません' FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド' -FirewallRestoreRulesStep: 'データベースルールを {{ .name }} に復元' FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期' FirewallEnableForwardingStep: 'ポート転送チェーンを有効化してバインド' FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルールを復元' FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認' FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド' FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存' -ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。" -FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください" -FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています" FileTaskCopy: 'ファイルを {{ .dst }} にコピー' FileTaskMove: 'ファイルを {{ .dst }} に移動' FileTaskCompress: 'ファイルを {{ .dst }} に圧縮' @@ -727,11 +713,13 @@ FileTaskDecompress: 'ファイルを {{ .dst }} に展開' FileTaskSource: '元のパス:{{ .path }}' FileTaskFormat: '圧縮形式:{{ .format }}' FileTaskRename: '保存先ファイル名:{{ .name }}' - ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。" ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。" ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}" +ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。" +ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。" ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください" ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です" ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません" + diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 1f72e42e9..45d0ea06d 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요' ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요' -ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD 기본 정책이 DROP입니다. 정책을 조정한 후 다시 시도하세요' -ErrUFWRuleAdopt: 'UFW 규칙 관리에 실패했습니다: {{ .detail }}. 현재 UFW 버전이 0.35 미만이면 UFW를 업그레이드한 후 다시 시도하세요' +ErrDockerForwardPolicyDrop: '현재 서버에서 네트워크 전달이 제한되어 컨테이너 포트 보호를 활성화할 수 없습니다.' Firewall: '방화벽' FirewallTaskHost: '호스트 방화벽' FirewallTaskForwarding: '포트 포워딩' @@ -698,28 +697,15 @@ 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 }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개' -FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음' -FirewallSyncStep: '{{ .name }}에 규칙 동기화' -FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다' -FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화' -FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다' +FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 실행된 명령은 롤백하지 않습니다' FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩' -FirewallRestoreRulesStep: '데이터베이스 규칙을 {{ .name }}에 복원' FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화' FirewallEnableForwardingStep: '포트 전달 체인 활성화 및 바인딩' FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복원' FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인' FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩' FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장' -ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요." -FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요" -FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다" FileTaskCopy: '{{ .dst }}에 파일 복사' FileTaskMove: '{{ .dst }}로 파일 이동' FileTaskCompress: '{{ .dst }}에 파일 압축' @@ -727,11 +713,13 @@ FileTaskDecompress: '{{ .dst }}에 압축 해제' FileTaskSource: '원본 경로: {{ .path }}' FileTaskFormat: '압축 형식: {{ .format }}' FileTaskRename: '대상 파일 이름: {{ .name }}' - ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요." ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요." ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}" +ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요." +ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요." ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요" ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다" ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다" + diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index 77fd89f8b..09781db33 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -674,8 +674,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall 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 ແລ້ວລອງໃໝ່' +ErrDockerForwardPolicyDrop: 'ເຊີບເວີນີ້ຈຳກັດການສົ່ງຕໍ່ເຄືອຂ່າຍ. ຍັງບໍ່ສາມາດເປີດໃຊ້ການປ້ອງກັນພອດຄອນເທນເນີໄດ້ໃນຕອນນີ້.' Firewall: 'ໄຟວອລ' FirewallTaskHost: 'ໄຟວໍໂຮສ' FirewallTaskForwarding: 'ການສົ່ງຕໍ່ພອດ' @@ -689,28 +688,15 @@ 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 }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)' -FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ' -FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}' -FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ' -FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}' -FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ' +FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ຄຳສັ່ງທີ່ດຳເນີນການແລ້ວບໍ່ໄດ້ຖືກຍ້ອນກັບ' FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}' -FirewallRestoreRulesStep: 'ກູ້ຄືນກົດຖານຂໍ້ມູນໄປຫາ {{ .name }}' FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ' FirewallEnableForwardingStep: 'ເປີດໃຊ້ ແລະ ຜູກ chain ສົ່ງຕໍ່ພອດ' FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕໍ່ພອດຈາກຖານຂໍ້ມູນ' FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ' FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}' FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker' -ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່." -FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ" -FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ" FileTaskCopy: 'ສຳເນົາໄຟລ໌ໄປທີ່ {{ .dst }}' FileTaskMove: 'ຍ້າຍໄຟລ໌ໄປທີ່ {{ .dst }}' FileTaskCompress: 'ບີບອັດໄຟລ໌ໄປທີ່ {{ .dst }}' @@ -718,11 +704,13 @@ FileTaskDecompress: 'ແຕກໄຟລ໌ໄປທີ່ {{ .dst }}' FileTaskSource: 'ເສັ້ນທາງຕົ້ນທາງ: {{ .path }}' FileTaskFormat: 'ຮູບແບບໄຟລ໌ບີບອັດ: {{ .format }}' FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}' - ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}." ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker." ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}" +ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່." +ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່." ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ" ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ" ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ" + diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 2e90ec676..07d73d4ba 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall 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' +ErrDockerForwardPolicyDrop: 'Pelayan ini mengehadkan pemajuan rangkaian. Perlindungan port kontena tidak dapat diaktifkan buat masa ini.' Firewall: 'Firewall' FirewallTaskHost: 'Tembok api hos' FirewallTaskForwarding: 'Pemajuan port' @@ -698,28 +697,15 @@ 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, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)' -FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan' -FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}' -FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan' -FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}' -FirewallResetSourceResult: 'Firewall sumber ditetapkan semula; {{ .removed }} peraturan pangkalan data dipadamkan' +FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Arahan yang dilaksanakan tidak dibatalkan' FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}' -FirewallRestoreRulesStep: 'Pulihkan peraturan pangkalan data ke {{ .name }}' FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall' FirewallEnableForwardingStep: 'Aktifkan dan ikat rantai pemajuan port' FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan data' FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi' FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}' FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker' -ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu." -FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan" -FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem" FileTaskCopy: 'Salin fail ke {{ .dst }}' FileTaskMove: 'Pindahkan fail ke {{ .dst }}' FileTaskCompress: 'Mampatkan fail ke {{ .dst }}' @@ -727,11 +713,13 @@ FileTaskDecompress: 'Ekstrak fail ke {{ .dst }}' FileTaskSource: 'Laluan sumber: {{ .path }}' FileTaskFormat: 'Format arkib: {{ .format }}' FileTaskRename: 'Nama fail sasaran: {{ .name }}' - ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}." ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.ip_forward=1 sebelum menggunakan bahagian belakang tembok api Docker." ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}" +ErrFirewallRuleConflict: "Peraturan UFW dengan syarat padanan yang sama tetapi tindakan berbeza sudah wujud. Sila edit peraturan sedia ada." +ErrForwardInterfaceNotFound: "Antara muka rangkaian {{ .name }} yang dinyatakan dalam peraturan pemajuan tidak wujud. Semak namanya dan cuba lagi." ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah" ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal" ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan" + diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 9b9d7da92..5b04cf38d 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall 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' +ErrDockerForwardPolicyDrop: 'Este servidor restringe o encaminhamento de rede. Não é possível ativar a proteção de portas de contêineres no momento.' Firewall: 'Firewall' FirewallTaskHost: 'Firewall do host' FirewallTaskForwarding: 'Encaminhamento de portas' @@ -698,28 +697,15 @@ 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, {{ .unchanged }} já correspondem (sem alterações necessárias)' -FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária' -FirewallSyncStep: 'Sincronizar regras com {{ .name }}' -FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização' -FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}' -FirewallResetSourceResult: 'Firewall de origem redefinido; {{ .removed }} regras do banco de dados foram excluídas' +FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Os comandos executados não foram revertidos' 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' FirewallEnableForwardingStep: 'Habilitar e vincular as cadeias de encaminhamento de portas' FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de portas do banco de dados' 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' -ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra." -FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista" -FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema" FileTaskCopy: 'Copiar arquivos para {{ .dst }}' FileTaskMove: 'Mover arquivos para {{ .dst }}' FileTaskCompress: 'Compactar arquivos em {{ .dst }}' @@ -727,11 +713,13 @@ FileTaskDecompress: 'Extrair arquivos para {{ .dst }}' FileTaskSource: 'Caminho de origem: {{ .path }}' FileTaskFormat: 'Formato do arquivo: {{ .format }}' FileTaskRename: 'Nome do arquivo de destino: {{ .name }}' - ErrFirewallBackendCleanupRequired: "O backend atual {{ .current }} ainda contém regras do 1Panel. Remova-as antes de mudar para {{ .target }}." ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina net.ipv4.ip_forward=1 antes de usar o backend de firewall do Docker." ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}" +ErrFirewallRuleConflict: "Já existe uma regra UFW com as mesmas condições e uma ação diferente. Edite a regra existente." +ErrForwardInterfaceNotFound: "A interface de rede {{ .name }} especificada na regra de encaminhamento não existe. Verifique o nome e tente novamente." ErrSSHPublicKey: "Informe uma chave pública SSH válida" ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original" ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem" + diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index ad6838b31..3f137d901 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку' ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку' -ErrDockerForwardPolicyDrop: 'Политика FORWARD по умолчанию для {{ .family }} — DROP. Измените её и повторите попытку' -ErrUFWRuleAdopt: 'Не удалось принять правило UFW под управление: {{ .detail }}. Если текущая версия UFW ниже 0.35, обновите UFW и повторите попытку' +ErrDockerForwardPolicyDrop: 'На этом сервере ограничена пересылка сетевого трафика. Сейчас невозможно включить защиту портов контейнеров.' Firewall: 'Межсетевой экран' FirewallTaskHost: 'Межсетевой экран хоста' FirewallTaskForwarding: 'Перенаправление портов' @@ -698,28 +697,15 @@ 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 }}, уже совпадают (изменения не нужны): {{ .unchanged }}' -FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны' -FirewallSyncStep: 'Синхронизировать правила с {{ .name }}' -FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}' -FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}' -FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}' +FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Выполненные команды не отменены' FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}' -FirewallRestoreRulesStep: 'Восстановить правила базы данных в {{ .name }}' FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана' FirewallEnableForwardingStep: 'Включить и привязать цепочки перенаправления портов' FirewallRestoreForwardingRulesStep: 'Восстановить правила перенаправления портов из базы данных' FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики' FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}' FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker' -ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило." -FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил" -FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов" FileTaskCopy: 'Копирование файлов в {{ .dst }}' FileTaskMove: 'Перемещение файлов в {{ .dst }}' FileTaskCompress: 'Сжатие файлов в {{ .dst }}' @@ -727,11 +713,13 @@ FileTaskDecompress: 'Распаковка файлов в {{ .dst }}' FileTaskSource: 'Исходный путь: {{ .path }}' FileTaskFormat: 'Формат архива: {{ .format }}' FileTaskRename: 'Имя целевого файла: {{ .name }}' - ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}." ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1." ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}" +ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило." +ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку." ErrSSHPublicKey: "Введите действительный открытый ключ SSH" ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза" ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу" + diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index adf567a5b..d06d52fda 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan' # Firewall 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' +ErrDockerForwardPolicyDrop: 'Bu sunucu ağ yönlendirmesini kısıtlıyor. Konteyner port koruması şu anda etkinleştirilemiyor.' Firewall: 'Güvenlik duvarı' FirewallTaskHost: 'Ana makine güvenlik duvarı' FirewallTaskForwarding: 'Port yönlendirme' @@ -698,28 +697,15 @@ 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, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)' -FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor' -FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle' -FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi' -FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak' -FirewallResetSourceResult: 'Kaynak güvenlik duvarı sıfırlandı; {{ .removed }} veritabanı kuralı silindi' +FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Yürütülen komutlar geri alınmadı' 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' FirewallEnableForwardingStep: 'Bağlantı noktası yönlendirme zincirlerini etkinleştir ve bağla' FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirme kurallarını geri yükle' 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' -ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun." -FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin" -FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor" FileTaskCopy: 'Dosyaları {{ .dst }} konumuna kopyala' FileTaskMove: 'Dosyaları {{ .dst }} konumuna taşı' FileTaskCompress: 'Dosyaları {{ .dst }} konumuna sıkıştır' @@ -727,11 +713,13 @@ FileTaskDecompress: 'Dosyaları {{ .dst }} konumuna çıkar' FileTaskSource: 'Kaynak yol: {{ .path }}' FileTaskFormat: 'Arşiv biçimi: {{ .format }}' FileTaskRename: 'Hedef dosya adı: {{ .name }}' - ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin." ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güvenlik duvarı arka ucunu kullanmadan önce net.ipv4.ip_forward=1 ayarını yapın." ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}" +ErrFirewallRuleConflict: "UFW içinde aynı eşleşme koşullarına sahip, farklı bir eylem uygulayan bir kural zaten var. Mevcut kuralı düzenleyin." +ErrForwardInterfaceNotFound: "Yönlendirme kuralında belirtilen {{ .name }} ağ arayüzü mevcut değil. Arayüz adını kontrol edip yeniden deneyin." ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin" ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir" ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor" + diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 79d190076..5a4aa18ec 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: '火山方舟 Coding Plan' # 防火牆 ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試' ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試' -ErrDockerForwardPolicyDrop: '偵測到 {{ .family }} FORWARD 預設策略為 DROP,請調整後重試' -ErrUFWRuleAdopt: 'UFW 規則納管失敗:{{ .detail }}。如果目前 UFW 版本低於 0.35,請先升級 UFW 後再試' +ErrDockerForwardPolicyDrop: '目前伺服器限制了網路轉送,暫時無法啟用容器連接埠防護。' Firewall: '防火牆' FirewallTaskHost: '主機防火牆' FirewallTaskForwarding: '連接埠轉發' @@ -698,28 +697,15 @@ 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 }} 條,已一致無需變更 {{ .unchanged }} 條' -FirewallSyncRuleUnchanged: '已一致,無需變更' -FirewallSyncStep: '同步規則到 {{ .name }}' -FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗' -FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}' -FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則' +FirewallCreateRuleExecutionFailed: '規則建立失敗,已執行的命令不回復' FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈' -FirewallRestoreRulesStep: '恢復資料庫規則至 {{ .name }}' FirewallSyncWhitelistStep: '同步防火牆連接埠白名單' FirewallEnableForwardingStep: '啟用並綁定連接埠轉發鏈' FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則' FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略' FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈' FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態' -ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。" -FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除" -FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護" FileTaskCopy: '複製檔案至 {{ .dst }}' FileTaskMove: '移動檔案至 {{ .dst }}' FileTaskCompress: '壓縮檔案至 {{ .dst }}' @@ -727,10 +713,11 @@ FileTaskDecompress: '解壓檔案至 {{ .dst }}' FileTaskSource: '來源路徑:{{ .path }}' FileTaskFormat: '壓縮格式:{{ .format }}' FileTaskRename: '目標檔名:{{ .name }}' - ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。" ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。" ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}" +ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。" +ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。" ErrSSHPublicKey: "請輸入有效的 SSH 公鑰" ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 4a7ef3db2..9676e2a31 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -683,8 +683,7 @@ AIProviderArkCodingPlan: "火山方舟 Coding Plan" # 防火墙 ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试" ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试" -ErrDockerForwardPolicyDrop: '检测到 {{ .family }} FORWARD 默认策略为 DROP,请调整后重试' -ErrUFWRuleAdopt: "UFW 规则纳管失败:{{ .detail }}。如果当前 UFW 版本低于 0.35,请先升级 UFW 后重试" +ErrDockerForwardPolicyDrop: '当前服务器限制了网络转发,暂时无法启用容器端口防护。' Firewall: "防火墙" FirewallTaskHost: '主机防火墙' FirewallTaskForwarding: '端口转发' @@ -698,28 +697,15 @@ 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 }} 条,已一致无需变更 {{ .unchanged }} 条' -FirewallSyncRuleUnchanged: '已一致,无需变更' -FirewallSyncStep: "同步规则到 {{ .name }}" -FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败" -FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}" -FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则" +FirewallCreateRuleExecutionFailed: '规则创建失败,已执行的命令不回滚' FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链" -FirewallRestoreRulesStep: "恢复数据库规则到 {{ .name }}" FirewallSyncWhitelistStep: "同步防火墙端口白名单" FirewallEnableForwardingStep: "启用并绑定端口转发链" FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则" FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略" FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链" FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态" -ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。" -FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除" -FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护" FileTaskCopy: '复制文件到 {{ .dst }}' FileTaskMove: '移动文件到 {{ .dst }}' FileTaskCompress: '压缩文件到 {{ .dst }}' @@ -727,11 +713,13 @@ FileTaskDecompress: '解压文件到 {{ .dst }}' FileTaskSource: '源路径:{{ .path }}' FileTaskFormat: '压缩格式:{{ .format }}' FileTaskRename: '目标文件名:{{ .name }}' - ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。" ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。" ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}" +ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。" +ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。" ErrSSHPublicKey: "请输入有效的 SSH 公钥" ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码" ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配" + diff --git a/agent/init/firewall/firewall.go b/agent/init/firewall/firewall.go index b88c22078..98b449605 100644 --- a/agent/init/firewall/firewall.go +++ b/agent/init/firewall/firewall.go @@ -6,20 +6,26 @@ import ( "os" "time" + "errors" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/init/migration/migrations" - migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils" "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/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" + "gorm.io/gorm" ) func Init() { ctx := context.Background() + defer initDockerPortGuard(ctx) + if err := initForwardingRules(ctx); err != nil { + global.LOG.Warnf("restore forwarding rules from file failed, err: %v", err) + } client, err := service.NewSelectedSystemFirewallClient() if err != nil { global.LOG.Errorf("select system firewall provider failed, err: %v", err) @@ -31,38 +37,33 @@ func Init() { if err := migrations.TransferFirewalldSSHService(ctx, client, service.NewIFirewallService().SyncPortWhitelist); err != nil { global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err) } - if initialize { - initDockerPortGuard(ctx) - } }() - if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil { - global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err) + families := []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} + nftFamilies := []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + ipv6Status, err := repo.NewISettingRepo().GetValueByKey(constant.FirewallIPv6SupportKey) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + global.LOG.Errorf("load firewall IPv6 setting failed: %v", err) return } - if err := migrationutils.TransferLegacyHostFirewallRuleOwnership(ctx, clientName, service.AdoptLegacyHostFirewallRuleOwnership); err != nil { - global.LOG.Warnf("transfer legacy host firewall rule ownership failed, err: %v", err) - } - if err := migrationutils.TransferFirewallForwarding(ctx); err != nil { - global.LOG.Errorf("transfer legacy forwarding rules failed, err: %v", err) - return - } - if err := initForwardingRules(ctx); err != nil { - global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err) + if ipv6Status == constant.StatusDisable { + global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 host chains and saved rules during startup") + families = families[:1] + nftFamilies = nftFamilies[:1] } initialize = needInit() if !initialize { - repairIptablesBaseChains(clientName) + repairIptablesBaseChains(clientName, families...) return } InitPingStatus() global.LOG.Info("initializing firewall settings...") if clientName == "nftables" { - if err := nftables_helper.Restore(); err != nil { + if err := nftables_helper.Restore(nftFamilies...); err != nil { global.LOG.Errorf("restore nftables rules failed, err: %v", err) } status, _ := repo.NewISettingRepo().GetValueByKey("IptablesStatus") if status == constant.StatusEnable { - if err := nftables_helper.Bind(); err != nil { + if err := nftables_helper.Bind(nftFamilies...); err != nil { global.LOG.Errorf("bind nftables base chains failed, err: %v", err) } } @@ -78,7 +79,7 @@ func Init() { global.LOG.Errorf("load required firewall ports failed, err: %v", err) return } - if err := iptables_helper.RestoreBaseChains(requiredPorts); err != nil { + if err := iptables_helper.RestoreBaseChains(requiredPorts, families...); err != nil { global.LOG.Errorf("restore iptables base chains failed, err: %v", err) return } @@ -94,7 +95,7 @@ func Init() { } -func repairIptablesBaseChains(clientName string) { +func repairIptablesBaseChains(clientName string, families ...string) { if clientName != constant.FirewallProviderIptables { return } @@ -108,7 +109,7 @@ func repairIptablesBaseChains(clientName string) { global.LOG.Warnf("load required firewall ports for base chain repair failed, err: %v", err) return } - if err := iptables_helper.RepairBaseChains(ports); err != nil { + if err := iptables_helper.RepairBaseChains(ports, families...); err != nil { global.LOG.Warnf("repair iptables base chains failed, err: %v", err) } } @@ -120,7 +121,7 @@ func initDockerPortGuard(ctx context.Context) { ) var restoreErr error for attempt := 1; attempt <= attempts; attempt++ { - restoreErr = service.ReconcileDockerPortGuard(ctx) + restoreErr = service.RestoreDockerPortGuard(ctx) if restoreErr == nil { return } diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index cf03b84fa..017c29b60 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -108,8 +108,7 @@ func agentDBMigrations() []*gormigrate.Migration { migrations.AddFirewallRuleTable, migrations.InitDockerPortGuardStatus, migrations.NormalizeFirewallBackendSelections, - migrations.SimplifyFirewallRulePolicy, - migrations.AddDockerPortGuardReadOnly, + migrations.MigrateFirewallDescriptions, migrations.MigrateFirewallPortWhitelistSources, migrations.AddAcceleratorMetrics, migrations.AddVLLMMonitor, diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index e166659a1..5fe2da537 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -59,7 +59,6 @@ var AddTable = &gormigrate.Migration{ &model.Favorite{}, &model.FileShare{}, &model.Host{}, - &model.FirewallRule{}, &model.Ftp{}, &model.ImageRepo{}, &model.ScriptLibrary{}, @@ -1784,7 +1783,7 @@ var AddComposePinned = &gormigrate.Migration{ var AddFirewallRuleTable = &gormigrate.Migration{ ID: "20260819-add-firewall-v2-tables", Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate(&model.FirewallRule{}, &model.DockerPortGuardPolicy{}, &model.ForwardingRule{}) + return tx.AutoMigrate(&model.CommonDescription{}) }, } @@ -1837,75 +1836,10 @@ var NormalizeFirewallBackendSelections = &gormigrate.Migration{ }, } -var SimplifyFirewallRulePolicy = &gormigrate.Migration{ - ID: "20260826-simplify-firewall-rule-policy", +var MigrateFirewallDescriptions = &gormigrate.Migration{ + ID: "20260921-migrate-firewall-descriptions", Migrate: func(tx *gorm.DB) error { - if !tx.Migrator().HasTable(&model.FirewallRule{}) { - return nil - } - return tx.Transaction(func(tx *gorm.DB) error { - if !tx.Migrator().HasColumn("firewall_rules", "compatibility_error") { - if err := tx.Exec("ALTER TABLE firewall_rules ADD COLUMN compatibility_error text NOT NULL DEFAULT ''").Error; err != nil { - return err - } - } - if !tx.Migrator().HasColumn("firewall_rules", "sequence") { - if err := tx.Exec("ALTER TABLE firewall_rules ADD COLUMN sequence integer").Error; err != nil { - return err - } - } - if tx.Migrator().HasColumn("firewall_rules", "native_kind") { - if err := tx.Exec(`UPDATE firewall_rules - SET compatibility_error = 'legacy native firewall rule requires manual recreation: ' || native_kind - WHERE native_kind IN ('zone_service', 'ufw_application', 'opaque')`).Error; err != nil { - return err - } - } - if tx.Migrator().HasColumn("firewall_rules", "provider") { - if err := tx.Exec(`UPDATE firewall_rules - SET priority = NULL, sequence = NULL`).Error; err != nil { - return err - } - } - if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_firewall_rules_sequence ON firewall_rules(sequence)").Error; err != nil { - return err - } - for _, index := range []string{ - "idx_firewall_rules_scope_key", - "uk_firewall_rules_scope_rule", - "uk_firewall_rules_scope_match", - } { - if err := tx.Exec("DROP INDEX IF EXISTS " + index).Error; err != nil { - return err - } - } - for _, column := range []string{ - "provider", "scope_key", "location", "native_kind", "order_index", "order_bucket", "rule_key", "match_key", - } { - if tx.Migrator().HasColumn("firewall_rules", column) { - _ = tx.Exec("ALTER TABLE firewall_rules DROP COLUMN " + column).Error - } - } - return nil - }) - }, -} - -var AddDockerPortGuardReadOnly = &gormigrate.Migration{ - ID: "20260902-add-docker-port-guard-read-only", - Migrate: func(tx *gorm.DB) error { - if !tx.Migrator().HasTable(&model.DockerPortGuardPolicy{}) { - return nil - } - if err := tx.AutoMigrate(&model.DockerPortGuardPolicy{}); err != nil { - return err - } - if tx.Migrator().HasIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint") { - if err := tx.Migrator().DropIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint"); err != nil { - return err - } - } - return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint") + return tx.Transaction(migrationutils.MigrateHostFirewallDescriptions) }, } diff --git a/agent/init/migration/migrations/utils/firewall_transfer.go b/agent/init/migration/migrations/utils/firewall_transfer.go deleted file mode 100644 index dceb5ebeb..000000000 --- a/agent/init/migration/migrations/utils/firewall_transfer.go +++ /dev/null @@ -1,373 +0,0 @@ -package utils - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/constant" - "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/utils/cmd" - "github.com/1Panel-dev/1Panel/agent/utils/controller" - "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" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -const firewallTransferMigrationID = "firewall-transfer" - -type legacyFirewalldForward struct { - rule forwarding.Rule - spec string -} - -type legacyFirewalldForwardFailure struct { - spec string - err error -} - -type firewallTransferSource struct { - rules []forwarding.Rule - firewalld []legacyFirewalldForward - provider string - cleanupOld func([]legacyFirewalldForward) error -} - -type firewallTransfer struct { - db *gorm.DB - load func() (firewallTransferSource, error) -} - -// TransferFirewallForwarding migrates the legacy system-backed forwarding -// inventory into the forwarding_rules table once. The migrations table is -// also used as the completion ledger so a failed transfer can be retried on -// the next agent start without introducing another persisted setting. -func TransferFirewallForwarding(ctx context.Context) error { - transfer := &firewallTransfer{ - db: global.DB, - load: loadLegacyFirewallForwarding, - } - return transfer.run(ctx) -} - -func (t *firewallTransfer) run(ctx context.Context) error { - if t.db == nil { - return errors.New("firewall transfer database is required") - } - completed, err := firewallTransferCompleted(t.db) - if err != nil { - return err - } - if completed { - return nil - } - if t.load == nil { - return errors.New("legacy firewall forwarding loader is required") - } - source, err := t.load() - if err != nil { - return err - } - if err := importLegacyForwardingRules(ctx, t.db, source.rules, source.provider); err != nil { - return err - } - if len(source.firewalld) > 0 { - if source.cleanupOld == nil { - return errors.New("legacy firewalld cleanup is required") - } - if err := source.cleanupOld(source.firewalld); err != nil { - return err - } - } - return markFirewallTransferCompleted(t.db) -} - -func firewallTransferCompleted(db *gorm.DB) (bool, error) { - return migrationRecordExists(db, firewallTransferMigrationID) -} - -func markFirewallTransferCompleted(db *gorm.DB) error { - return markMigrationRecord(db, firewallTransferMigrationID) -} - -func migrationRecordExists(db *gorm.DB, migrationID string) (bool, error) { - var count int64 - if err := db.Table("migrations").Where("id = ?", migrationID).Count(&count).Error; err != nil { - return false, fmt.Errorf("check migration record %q: %w", migrationID, err) - } - return count > 0, nil -} - -func markMigrationRecord(db *gorm.DB, migrationID string) error { - if err := db.Table("migrations").Clauses(clause.OnConflict{DoNothing: true}). - Create(map[string]interface{}{"id": migrationID}).Error; err != nil { - return fmt.Errorf("record migration %q: %w", migrationID, err) - } - return nil -} - -func importLegacyForwardingRules(ctx context.Context, db *gorm.DB, rules []forwarding.Rule, provider string) error { - models := make([]model.ForwardingRule, 0, len(rules)) - seen := make(map[string]struct{}, len(rules)) - for _, rule := range rules { - normalized, err := forwarding.NormalizeRule(rule) - if err != nil { - return fmt.Errorf("normalize legacy forwarding rule: %w", err) - } - key := normalized.Identity() - if _, exists := seen[key]; exists { - continue - } - seen[key] = struct{}{} - models = append(models, model.ForwardingRule{ - Family: normalized.Family, Protocol: normalized.Protocol, Port: normalized.Port, - TargetIP: normalized.TargetIP, TargetPort: normalized.TargetPort, Interface: normalized.Interface, - }) - } - return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - if len(models) > 0 { - if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&models).Error; err != nil { - return fmt.Errorf("import legacy forwarding rules: %w", err) - } - if err := updateOrCreateSetting(tx, "IptablesForwardStatus", constant.StatusEnable); err != nil { - return err - } - if provider != "" { - if err := updateOrCreateSetting(tx, "ForwardingBackend", provider); err != nil { - return err - } - } - } - return nil - }) -} - -func updateOrCreateSetting(tx *gorm.DB, key, value string) error { - result := tx.Model(&model.Setting{}).Where("key = ?", key).Update("value", value) - if result.Error != nil { - return result.Error - } - if result.RowsAffected > 0 { - return nil - } - return tx.Create(&model.Setting{Key: key, Value: value}).Error -} - -func loadLegacyFirewallForwarding() (firewallTransferSource, error) { - source := firewallTransferSource{cleanupOld: cleanupLegacyFirewalldForwarding} - if cmd.Which("firewall-cmd") { - firewalldRules, err := listLegacyFirewalldForwarding() - if err != nil { - return firewallTransferSource{}, err - } - supportedRules, cleanupRules, failures := selectSupportedLegacyFirewalldForwarding(firewalldRules) - for _, failure := range failures { - if global.LOG != nil { - global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err) - } - } - source.rules = append(source.rules, supportedRules...) - source.firewalld = append(source.firewalld, cleanupRules...) - if len(source.rules) > 0 { - source.provider = "iptables" - } - return source, nil - } - if _, err := lifecycle.ResolveIptablesCommands(); err != nil { - return source, nil - } - rules, err := listLegacyIptablesForwarding() - if err != nil { - return firewallTransferSource{}, err - } - source.rules = append(source.rules, rules...) - return source, nil -} - -func selectSupportedLegacyFirewalldForwarding(items []legacyFirewalldForward) ( - []forwarding.Rule, []legacyFirewalldForward, []legacyFirewalldForwardFailure, -) { - rules := make([]forwarding.Rule, 0, len(items)) - cleanup := make([]legacyFirewalldForward, 0, len(items)) - failures := make([]legacyFirewalldForwardFailure, 0) - for _, item := range items { - if _, err := forwarding.NormalizeRule(item.rule); err != nil { - failures = append(failures, legacyFirewalldForwardFailure{spec: item.spec, err: err}) - continue - } - rules = append(rules, item.rule) - cleanup = append(cleanup, item) - } - return rules, cleanup, failures -} - -func listLegacyIptablesForwarding() ([]forwarding.Rule, error) { - exists, err := iptables_helper.CheckChainExist(iptables_helper.NatTab, forwarding.ChainPreRouting) - if err != nil { - return nil, err - } - if !exists { - return nil, nil - } - stdout, err := iptables_helper.RunWithStd( - iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers", - ) - if err != nil { - return nil, fmt.Errorf("list legacy iptables forwarding rules: %w", err) - } - return parseLegacyIptablesForwarding(stdout), nil -} - -func parseLegacyIptablesForwarding(stdout string) []forwarding.Rule { - rules := make([]forwarding.Rule, 0) - for _, line := range strings.Split(stdout, "\n") { - fields := strings.Fields(line) - if len(fields) < 13 { - continue - } - rule := forwarding.Rule{ - Family: forwarding.FamilyIPv4, Protocol: loadLegacyIptablesProtocol(fields[4]), - Interface: fields[6], Port: loadLegacyIptablesSourcePort(fields[11]), - } - if len(fields) == 15 && fields[13] == "ports" { - rule.TargetPort = fields[14] - } - if len(fields) == 13 && strings.HasPrefix(fields[12], "to:") { - target := strings.TrimPrefix(fields[12], "to:") - separator := strings.LastIndex(target, ":") - if separator > 0 { - rule.TargetIP, rule.TargetPort = target[:separator], target[separator+1:] - } - } - if rule.TargetIP == "" { - rule.TargetIP = "127.0.0.1" - } - rule.TargetPort = strings.TrimPrefix(rule.TargetPort, ":") - rules = append(rules, rule) - } - return rules -} - -func loadLegacyIptablesProtocol(protocol string) string { - switch protocol { - case "6": - return "tcp" - case "17": - return "udp" - default: - return protocol - } -} - -func loadLegacyIptablesSourcePort(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, ":", "-") -} - -func listLegacyFirewalldForwarding() ([]legacyFirewalldForward, error) { - stdout, err := cmd.NewCommandMgr().RunWithStdout( - "firewall-cmd", "--permanent", "--zone=public", "--list-forward-ports", - ) - if err != nil { - return nil, fmt.Errorf("list legacy firewalld forwarding rules: %w", err) - } - rules, failures := parseLegacyFirewalldForwarding(stdout) - for _, failure := range failures { - if global.LOG != nil { - global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err) - } - } - return rules, nil -} - -func parseLegacyFirewalldForwarding(stdout string) ([]legacyFirewalldForward, []legacyFirewalldForwardFailure) { - result := make([]legacyFirewalldForward, 0) - failures := make([]legacyFirewalldForwardFailure, 0) - for _, spec := range strings.Fields(stdout) { - item, err := parseLegacyFirewalldForward(spec) - if err != nil { - failures = append(failures, legacyFirewalldForwardFailure{spec: spec, err: err}) - continue - } - result = append(result, item) - } - return result, failures -} - -func parseLegacyFirewalldForward(spec string) (legacyFirewalldForward, error) { - if !strings.HasPrefix(spec, "port=") { - return legacyFirewalldForward{}, errors.New("missing port field") - } - port, rest, ok := strings.Cut(strings.TrimPrefix(spec, "port="), ":proto=") - if !ok { - return legacyFirewalldForward{}, errors.New("missing protocol field") - } - protocol, rest, ok := strings.Cut(rest, ":toport=") - if !ok { - return legacyFirewalldForward{}, errors.New("missing target port field") - } - targetPort, targetIP, ok := strings.Cut(rest, ":toaddr=") - if !ok { - return legacyFirewalldForward{}, errors.New("missing target address field") - } - if targetIP == "" { - targetIP = "127.0.0.1" - } - return legacyFirewalldForward{ - rule: forwarding.Rule{ - Family: forwarding.FamilyIPv4, Protocol: protocol, Port: port, - TargetIP: targetIP, TargetPort: targetPort, - }, - spec: spec, - }, nil -} - -func cleanupLegacyFirewalldForwarding(rules []legacyFirewalldForward) error { - manager := cmd.NewCommandMgr() - for _, item := range rules { - if err := manager.Run( - "firewall-cmd", "--permanent", "--zone=public", "--remove-forward-port="+item.spec, - ); err != nil { - return fmt.Errorf("remove legacy firewalld forwarding rule %q: %w", item.spec, err) - } - } - if len(rules) == 0 { - return nil - } - if err := manager.Run("firewall-cmd", "--reload"); err != nil { - return fmt.Errorf("reload firewalld after forwarding transfer: %w", err) - } - return restartDockerAfterFirewalldReload(cmd.Which, controller.CheckActive, controller.HandleRestart) -} - -func restartDockerAfterFirewalldReload( - which func(string) bool, - checkActive func(string) (bool, error), - restart func(string) error, -) error { - const service = "docker" - if !which(service) { - return nil - } - active, err := checkActive(service) - if err != nil { - return fmt.Errorf("check Docker status after reloading firewalld: %w", err) - } - if !active { - return nil - } - if err := restart(service); err != nil { - return fmt.Errorf("restart Docker after reloading firewalld: %w", err) - } - return nil -} diff --git a/agent/init/migration/migrations/utils/host_firewall_transfer.go b/agent/init/migration/migrations/utils/host_firewall_transfer.go index 5914dd9e8..feb416760 100644 --- a/agent/init/migration/migrations/utils/host_firewall_transfer.go +++ b/agent/init/migration/migrations/utils/host_firewall_transfer.go @@ -1,25 +1,21 @@ package utils import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" "errors" "fmt" "net/netip" + "slices" + "sort" "strings" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/constant" - "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" - "github.com/google/uuid" + filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld" + filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw" "gorm.io/gorm" ) -const hostFirewallTransferMigrationID = "host-firewall-transfer" - var errUnsupportedLegacyHostFirewallRule = errors.New("unsupported legacy host firewall rule") type legacyHostFirewallRecord struct { @@ -37,126 +33,225 @@ type legacyHostFirewallRecord struct { Description string } -func TransferHostFirewall(ctx context.Context, provider string) error { - if global.DB == nil { - return errors.New("host firewall transfer database is required") - } - return transferHostFirewall(ctx, global.DB, filter.Provider(strings.ToLower(strings.TrimSpace(provider)))) +type legacyFirewallRuleDescription struct { + UUID string + Provider string + ScopeKey string + Location string + Family string + NativeKind string + Protocol string + SourceAddress string + SourcePort string + DestinationAddress string + DestinationPort string + Interface string + ConnectionStates string + CompatibilityError string + Action string + Priority *int + Description string + Owner string } -func TransferLegacyHostFirewallRuleOwnership(ctx context.Context, provider string, transfer func(context.Context) error) error { - if global.DB == nil { - return errors.New("host firewall transfer database is required") +func MigrateHostFirewallDescriptions(db *gorm.DB) error { + providers := []filter.Provider{filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW} + descriptions := make(map[string][]string) + add := func(id, description string) { + if description != "" && !slices.Contains(descriptions[id], description) { + descriptions[id] = append(descriptions[id], description) + } } - return transferLegacyHostFirewallRuleOwnership( - ctx, - global.DB, - filter.Provider(strings.ToLower(strings.TrimSpace(provider))), - transfer, - ) -} - -func transferLegacyHostFirewallRuleOwnership( - ctx context.Context, - db *gorm.DB, - provider filter.Provider, - transfer func(context.Context) error, -) error { - if !legacyHostFirewallOwnershipProvider(provider) { - return nil + addRules := func(rules []filter.FirewallRule, description string) bool { + matched := false + for _, rule := range rules { + var err error + switch rule.Scope.Provider { + case filter.ProviderFirewalld: + rule, err = (&filterfirewalld.Adapter{}).PrepareRule(rule) + case filter.ProviderUFW: + rule, err = (&filterufw.Adapter{}).PrepareRule(rule) + } + if err != nil { + continue + } + id, err := filter.DescriptionID(filter.ObservedRule{Rule: rule, ParseStatus: filter.ParseStatusSupported}) + if err == nil { + add(id, description) + matched = true + } + } + return matched } - if db == nil { - return errors.New("host firewall transfer database is required") - } - completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID) - if err != nil || completed { - return err - } - if transfer == nil { - return errors.New("legacy host firewall ownership transfer is required") - } - if err := transfer(ctx); err != nil { - return fmt.Errorf("transfer legacy host firewall rule ownership: %w", err) - } - return markMigrationRecord(db, hostFirewallTransferMigrationID) -} - -func legacyHostFirewallOwnershipProvider(provider filter.Provider) bool { - return provider == filter.ProviderIptables || provider == filter.ProviderUFW -} - -func transferHostFirewall(ctx context.Context, db *gorm.DB, provider filter.Provider) error { - if db == nil { - return errors.New("host firewall transfer database is required") - } - completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID) - if err != nil || completed { - return err - } - if !isLegacyHostFirewallProvider(provider) { - return fmt.Errorf("unsupported legacy host firewall provider %q", provider) - } - - models := make([]model.FirewallRule, 0) if db.Migrator().HasTable("firewalls") { var records []legacyHostFirewallRecord - if err := db.WithContext(ctx).Table("firewalls").Order("id ASC").Find(&records).Error; err != nil { - return fmt.Errorf("load legacy host firewall records: %w", err) - } - models = convertLegacyHostFirewallRecords(records, provider) - } - - return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - if err := importLegacyHostFirewallRules(tx, models); err != nil { + if err := db.Table("firewalls").Order("id ASC").Find(&records).Error; err != nil { return err } - if legacyHostFirewallOwnershipProvider(provider) { - return nil + for _, record := range records { + if record.Description == "" { + continue + } + matched := false + for _, provider := range providers { + rules, err := legacyHostFirewallRules(record, provider) + if err == nil && addRules(rules, record.Description) { + matched = true + } + } + if !matched { + add(fmt.Sprintf("firewall:legacy:firewalls:%d", record.ID), record.Description) + } } - return markMigrationRecord(tx, hostFirewallTransferMigrationID) - }) -} - -func isLegacyHostFirewallProvider(provider filter.Provider) bool { - switch provider { - case filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW: - return true - default: - return false } + if db.Migrator().HasTable("firewall_rules") { + var records []legacyFirewallRuleDescription + if err := db.Table("firewall_rules").Order("uuid ASC").Find(&records).Error; err != nil { + return err + } + for _, record := range records { + if record.Description == "" { + continue + } + matched := false + for _, provider := range providers { + rules, err := legacyFirewallDescriptionRules(record, provider) + if err == nil && addRules(rules, record.Description) { + matched = true + } + } + if !matched { + add("firewall:legacy:firewall_rules:"+record.UUID, record.Description) + } + } + } + if len(descriptions) == 0 { + return nil + } + var existing []model.CommonDescription + if err := db.Where("type = ?", "firewall").Find(&existing).Error; err != nil { + return err + } + byID := make(map[string]model.CommonDescription, len(existing)) + for _, description := range existing { + byID[description.ID] = description + } + ids := make([]string, 0, len(descriptions)) + for id := range descriptions { + ids = append(ids, id) + } + sort.Strings(ids) + for _, id := range ids { + value := strings.Join(descriptions[id], "\n") + if current, ok := byID[id]; ok { + if current.Description == value { + continue + } + if current.Description == "" { + if err := db.Model(&model.CommonDescription{}).Where("id = ?", id).Update("description", value).Error; err != nil { + return err + } + continue + } + id = "firewall:legacy:description:" + strings.TrimPrefix(id, "firewall:") + if _, ok := byID[id]; ok { + continue + } + } + record := model.CommonDescription{ID: id, Type: "firewall", Description: value} + if strings.HasPrefix(id, "firewall:legacy:") { + record.DetailType = "legacy" + } + if err := db.Create(&record).Error; err != nil { + return err + } + } + return nil } -func convertLegacyHostFirewallRecords(records []legacyHostFirewallRecord, provider filter.Provider) []model.FirewallRule { - converted := make([]model.FirewallRule, 0, len(records)) - byIdentity := make(map[string]int) - for _, record := range records { - rules, err := legacyHostFirewallRules(record, provider) +func legacyFirewallDescriptionRules(record legacyFirewallRuleDescription, provider filter.Provider) ([]filter.FirewallRule, error) { + if record.CompatibilityError != "" { + return nil, errUnsupportedLegacyHostFirewallRule + } + source := filter.Provider(strings.ToLower(strings.TrimSpace(record.Provider))) + scopeParts := strings.Split(record.ScopeKey, ":") + if source == "" && len(scopeParts) > 1 { + source = filter.Provider(scopeParts[0]) + } + if source != "" && source != provider { + return nil, nil + } + base := filter.FirewallRule{ + Protocol: record.Protocol, SourceAddress: record.SourceAddress, SourcePort: record.SourcePort, + DestinationAddress: record.DestinationAddress, DestinationPort: record.DestinationPort, + Interface: record.Interface, Action: filter.Action(record.Action), + } + if record.ConnectionStates != "" { + base.ConnectionStates = strings.Split(record.ConnectionStates, ",") + } + if source != "" { + base.NativeKind = filter.NativeKind(record.NativeKind) + } + switch base.NativeKind { + case filter.NativeKindOpaque, filter.NativeKindZoneService, filter.NativeKindUFWApplication: + return nil, errUnsupportedLegacyHostFirewallRule + } + if provider != filter.ProviderUFW && strings.EqualFold(base.Protocol, "all") && base.SourcePort == "" && base.DestinationPort != "" { + base.Protocol = "tcp/udp" + } + if provider == filter.ProviderFirewalld { + base.Priority = record.Priority + } + families := []filter.Family{filter.Family(record.Family)} + if families[0] == "" { + families[0] = legacyRuleFamily(base.SourceAddress, base.DestinationAddress) + } + if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet { + if base.SourceAddress != "" || base.DestinationAddress != "" || base.Protocol == "icmpv6" { + families = []filter.Family{legacyRuleFamily(base.SourceAddress, base.DestinationAddress)} + if base.Protocol == "icmpv6" { + families[0] = filter.FamilyIPv6 + } + } else { + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } + } + var result []filter.FirewallRule + for _, family := range families { + rule := base + rule.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput} + switch provider { + case filter.ProviderIptables, filter.ProviderNftables: + rule.Scope.Table, rule.Scope.Chain = "filter", filter.IptablesInputChain + if source != "" { + if record.Location != "" { + rule.Scope.Chain = record.Location + } + if len(scopeParts) == 5 { + rule.Scope.Table, rule.Scope.Chain = scopeParts[2], scopeParts[3] + } + } + case filter.ProviderFirewalld: + rule.Scope.Zone = filter.FirewalldInputZone + if source != "" && record.Location != "" { + rule.Scope.Zone = record.Location + } + case filter.ProviderUFW: + rule.Scope.Chain = filter.UFWInputChain + } + expanded, err := filter.ExpandAtomicRules(rule) if err != nil { - if global.LOG != nil { - global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err) - } - continue + return nil, err } - for _, rule := range rules { - item, err := hostFirewallRuleModel(rule) - if err != nil { - if global.LOG != nil { - global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err) - } - continue + result = append(result, expanded...) + if source == "" && (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && strings.HasPrefix(record.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) { + for _, item := range expanded { + item.Scope.Chain = filter.BasicBeforeChain + result = append(result, item) } - identity := hostFirewallPolicyKey(item) - if index, exists := byIdentity[identity]; exists { - if item.Description != "" { - converted[index].Description = item.Description - } - continue - } - byIdentity[identity] = len(converted) - converted = append(converted, item) } } - return converted + return result, nil } func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Provider) ([]filter.FirewallRule, error) { @@ -196,7 +291,7 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr switch provider { case filter.ProviderIptables: rule.Scope = filter.Scope{ - Provider: provider, Family: filter.FamilyIPv4, Table: "filter", + Provider: provider, Family: legacyRuleFamily(rule.SourceAddress, rule.DestinationAddress), Table: "filter", Chain: legacyIptablesChain(record), Direction: filter.DirectionInput, } rule.NativeKind = filter.NativeKindRule @@ -337,84 +432,3 @@ func legacyIPOrPrefix(value string) bool { _, err := netip.ParsePrefix(value) return err == nil } - -func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) { - normalized, err := filter.NormalizeRule(rule) - if err != nil { - return model.FirewallRule{}, err - } - switch normalized.NativeKind { - case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule: - default: - return model.FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind) - } - record := model.FirewallRule{ - Family: string(normalized.Scope.Family), - Protocol: normalized.Protocol, - SourceAddress: normalized.SourceAddress, - SourcePort: normalized.SourcePort, - DestinationAddress: normalized.DestinationAddress, - DestinationPort: normalized.DestinationPort, - Interface: normalized.Interface, - ConnectionStates: strings.Join(normalized.ConnectionStates, ","), - Action: string(normalized.Action), - Description: normalized.Description, - } - if normalized.Scope.Provider == filter.ProviderFirewalld { - record.Priority = normalized.Priority - } - record.UUID = uuid.NewString() - record.Origin = constant.FirewallRuleOriginAdopted - record.Owner = constant.FirewallRuleSourceUser - record.Revision = 1 - return record, nil -} - -func hostFirewallPolicyKey(rule model.FirewallRule) string { - payload, _ := json.Marshal(struct { - Family string `json:"family"` - Protocol string `json:"protocol"` - SourceAddress string `json:"sourceAddress,omitempty"` - SourcePort string `json:"sourcePort,omitempty"` - DestinationAddress string `json:"destinationAddress,omitempty"` - DestinationPort string `json:"destinationPort,omitempty"` - Interface string `json:"interface,omitempty"` - ConnectionStates string `json:"connectionStates,omitempty"` - Action string `json:"action"` - }{ - Family: rule.Family, Protocol: rule.Protocol, - SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort, - DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort, - Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action, - }) - sum := sha256.Sum256(payload) - return hex.EncodeToString(sum[:]) -} - -func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) error { - var existing []model.FirewallRule - if err := tx.Find(&existing).Error; err != nil { - return fmt.Errorf("load current host firewall rules: %w", err) - } - byIdentity := make(map[string]model.FirewallRule, len(existing)) - for _, item := range existing { - byIdentity[hostFirewallPolicyKey(item)] = item - } - for _, item := range rules { - identity := hostFirewallPolicyKey(item) - if current, exists := byIdentity[identity]; exists { - if current.Description == "" && item.Description != "" { - if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID). - Update("description", item.Description).Error; err != nil { - return fmt.Errorf("restore legacy host firewall description: %w", err) - } - } - continue - } - if err := tx.Create(&item).Error; err != nil { - return fmt.Errorf("import legacy host firewall rule: %w", err) - } - byIdentity[identity] = item - } - return nil -} diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index 61aee2760..4d97b1d93 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -28,6 +28,8 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort) hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings) hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend) + hostRouter.POST("/firewall/family/operate", baseApi.OperateFirewallFamily) + hostRouter.POST("/firewall/settings/ipv6", baseApi.OperateFirewallIPv6) hostRouter.POST("/firewall/settings/whitelist", baseApi.CreateFirewallPortWhitelist) hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist) hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist) @@ -36,13 +38,10 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules) hostRouter.POST("/firewall/forward/enable", baseApi.EnableForwarding) hostRouter.POST("/firewall/rules/search", baseApi.SearchFirewallRules) + hostRouter.GET("/firewall/rules/backups", baseApi.ListFirewallRuleBackups) hostRouter.POST("/firewall/rules/reset", baseApi.ResetFirewallRules) hostRouter.POST("/firewall/rules/native/detail", baseApi.LoadFirewallNativeDetail) - 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) - hostRouter.POST("/firewall/rules/sync", baseApi.SyncFirewallRules) hostRouter.POST("/firewall/rules/update", baseApi.UpdateFirewallRule) hostRouter.POST("/firewall/rules/delete", baseApi.DeleteFirewallRules) hostRouter.POST("/firewall/rules/reorder", baseApi.ReorderFirewallRule) @@ -50,7 +49,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/filter/operate", baseApi.OperateFilterChain) hostRouter.GET("/firewall/docker/ports", baseApi.ListDockerPortGuard) hostRouter.GET("/firewall/docker/endpoints", baseApi.ListDockerPublishedPorts) - hostRouter.POST("/firewall/docker/sync", baseApi.SyncDockerPortGuard) hostRouter.POST("/firewall/docker/operate", baseApi.OperateDockerPortGuard) hostRouter.POST("/firewall/docker/policies/batch", baseApi.UpsertDockerPortGuardPolicies) hostRouter.POST("/firewall/docker/policies/delete/batch", baseApi.DeleteDockerPortGuardPolicies) diff --git a/agent/utils/firewall/docker_guard/inspect.go b/agent/utils/firewall/docker_guard/inspect.go index 027a580ed..048c28412 100644 --- a/agent/utils/firewall/docker_guard/inspect.go +++ b/agent/utils/firewall/docker_guard/inspect.go @@ -1,6 +1,7 @@ package docker_guard import ( + "context" "net/netip" "path/filepath" "slices" @@ -13,8 +14,8 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" ) -func ReadDNATRules(backend, family string) DNATRules { - manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C")) +func ReadDNATRules(ctx context.Context, backend, family string) DNATRules { + manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C")) if backend == constant.FirewallProviderNftables { tableFamily := "ip" if family == constant.FirewallFamilyIPv6 { @@ -45,8 +46,8 @@ func ReadDNATRules(backend, family string) DNATRules { return DNATRules{Output: output, Inspected: err == nil} } -func ReadProxyEndpoints() ProxyEndpoints { - manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C")) +func ReadProxyEndpoints(ctx context.Context) ProxyEndpoints { + manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C")) output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=") if err != nil { return ProxyEndpoints{} diff --git a/agent/utils/firewall/docker_guard/manager.go b/agent/utils/firewall/docker_guard/manager.go index d17c3a382..a2c72684e 100644 --- a/agent/utils/firewall/docker_guard/manager.go +++ b/agent/utils/firewall/docker_guard/manager.go @@ -1,10 +1,11 @@ package docker_guard import ( + "context" "errors" "fmt" "github.com/1Panel-dev/1Panel/agent/buserr" - "sort" + "slices" "strconv" "strings" "sync" @@ -12,6 +13,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/cmd" firewallutil "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" ) @@ -22,26 +24,27 @@ type Runner interface { Exists(executable string) bool } -type commandRunner struct{} +type commandRunner struct{ ctx context.Context } -func (commandRunner) Run(executable string, args ...string) (string, error) { +func (r commandRunner) Run(executable string, args ...string) (string, error) { executable = dockerGuardExecutable(executable) - manager := cmd.NewCommandMgr(cmd.WithTimeout(60 * time.Second)) + manager := cmd.NewCommandMgr(cmd.WithContext(r.ctx), cmd.WithTimeout(60*time.Second)) stdout, err := manager.RunWithOptionalSudoAndStdout(executable, args...) + err = errors.Join(err, r.ctx.Err()) if err != nil { return stdout, fmt.Errorf("command=%s %s failed: %w", executable, strings.Join(args, " "), err) } return stdout, nil } -func (commandRunner) RunInput(executable, input string, args ...string) (string, error) { +func (r commandRunner) RunInput(executable, input string, args ...string) (string, error) { executable = dockerGuardExecutable(executable) if executable == "nft" { - return "", nftables_helper.RunScript(input) + return "", errors.Join(nftables_helper.RunScriptContext(r.ctx, input), r.ctx.Err()) } - manager := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input))) + manager := cmd.NewCommandMgr(cmd.WithContext(r.ctx), cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input))) stdout, err := manager.RunWithOptionalSudoAndStdout(executable, args...) - return stdout, firewallutil.WrapBatchCommandError(executable+" "+strings.Join(args, " "), input, err) + return stdout, errors.Join(firewallutil.WrapBatchCommandError(executable+" "+strings.Join(args, " "), input, err), r.ctx.Err()) } func (commandRunner) Exists(executable string) bool { @@ -74,9 +77,12 @@ type Iptables struct { var mutationMu sync.Mutex -func NewIptables() *Iptables { return &Iptables{runner: commandRunner{}} } +func NewIptables(ctx context.Context) *Iptables { return &Iptables{runner: commandRunner{ctx: ctx}} } -func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) error { +func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory, families ...string) error { + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} + } mutationMu.Lock() defer mutationMu.Unlock() if err := CheckIPv4Forwarding(); err != nil { @@ -88,7 +94,7 @@ func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) erro if err := m.ensureFamily("iptables", true); err != nil { return err } - if m.runner.Exists("ip6tables") { + if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") { available, err := m.chainExists("ip6tables", DockerChain) if err != nil { return &FamilyError{Family: FamilyIPv6, Err: fmt.Errorf("inspect %s chain: %w", DockerChain, err)} @@ -102,16 +108,19 @@ func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) erro } } } - return m.rebuildLocked(policies, inventory) + return m.rebuildLocked(policies, inventory, families...) } -func (m *Iptables) Bind() error { +func (m *Iptables) Bind(families ...string) error { + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} + } mutationMu.Lock() defer mutationMu.Unlock() if err := m.bindExistingFamily("iptables", true); err != nil { return err } - if m.runner.Exists("ip6tables") { + if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") { if err := m.bindExistingFamily("ip6tables", false); err != nil { return &FamilyError{Family: FamilyIPv6, Err: err} } @@ -126,13 +135,16 @@ func (m *Iptables) ReplacePolicies(policies []Policy, inventory PolicyInventory) } func (m *Iptables) ListPolicies() (PolicyInventory, error) { - inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)} + inventory := PolicyInventory{Policies: make([]Policy, 0), RuleOrders: make(map[string][]int64)} for _, family := range []string{FamilyIPv4, FamilyIPv6} { executable := executableForFamily(family) if executable == "" || !m.runner.Exists(executable) { continue } exists, err := m.chainExists(executable, Chain) + if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { + continue + } if err != nil { return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)} } @@ -148,22 +160,21 @@ func (m *Iptables) ListPolicies() (PolicyInventory, error) { return PolicyInventory{}, &FamilyError{Family: family, Err: err} } inventory.Policies = append(inventory.Policies, parsed.Policies...) - inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...) - for key, orders := range parsed.ManagedRuleOrders { - inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...) + for key, orders := range parsed.RuleOrders { + inventory.RuleOrders[key] = append([]int64(nil), orders...) } } return inventory, nil } -func (m *Iptables) Unbind() error { +func (m *Iptables) Unbind(families ...string) error { mutationMu.Lock() defer mutationMu.Unlock() - if err := m.unbindFamily("iptables"); err != nil { - return err + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} } - if m.runner.Exists("ip6tables") { - if err := m.unbindFamily("ip6tables"); err != nil { + for _, family := range families { + if err := m.unbindFamily(executableForFamily(family)); err != nil { return err } } @@ -315,13 +326,19 @@ func (m *Iptables) restoreLifecycle(executable string, rules [][]string) error { return nil } -func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) error { - for _, family := range []string{FamilyIPv4, FamilyIPv6} { +func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory, families ...string) error { + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} + } + for _, family := range families { executable := executableForFamily(family) if executable == "" || !m.runner.Exists(executable) { continue } exists, err := m.chainExists(executable, Chain) + if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) && !slices.ContainsFunc(policies, func(policy Policy) bool { return policy.Family == family }) { + continue + } if err != nil { return &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)} } @@ -346,41 +363,34 @@ func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) e return nil } -type orderedIPTablesRule struct { - order int64 - index int - rules [][]string -} - func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInventory) [][]string { - segments := make([]orderedIPTablesRule, 0, len(policies)+len(inventory.ReadOnly)) + segments := make([]orderedPolicyRule, 0, len(policies)) maxOrder := int64(0) - index := 0 - for _, orders := range inventory.ManagedRuleOrders { + for _, orders := range inventory.RuleOrders { for _, order := range orders { - if order > maxOrder { - maxOrder = order - } + maxOrder = max(maxOrder, order) } } - for _, item := range inventory.ReadOnly { - for _, native := range item.NativeRules { - if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain { - continue - } - segments = append(segments, orderedIPTablesRule{order: native.Order, index: index, rules: [][]string{append([]string(nil), native.Tokens...)}}) - index++ - if native.Order > maxOrder { - maxOrder = native.Order - } + for _, policy := range policies { + for _, rule := range policy.NativeRules { + maxOrder = max(maxOrder, rule.Order) } } for _, policy := range policies { if policy.Family != family { continue } + if len(policy.NativeRules) > 0 { + for _, native := range policy.NativeRules { + if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain { + continue + } + segments = append(segments, orderedPolicyRule{order: native.Order, rule: append([]string(nil), native.Tokens...)}) + } + continue + } compiled := compilePolicy(policy) - orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID] + orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID] for ruleIndex, rule := range compiled { order := int64(0) if ruleIndex < len(orders) { @@ -389,21 +399,10 @@ func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInve maxOrder++ order = maxOrder } - segments = append(segments, orderedIPTablesRule{order: order, index: index, rules: [][]string{rule}}) - index++ + segments = append(segments, orderedPolicyRule{order: order, rule: rule}) } } - sort.SliceStable(segments, func(left, right int) bool { - if segments[left].order == segments[right].order { - return segments[left].index < segments[right].index - } - return segments[left].order < segments[right].order - }) - rules := make([][]string, 0) - for _, segment := range segments { - rules = append(rules, segment.rules...) - } - return rules + return sortPolicyRules(segments) } func buildRestoreScript(rules [][]string) (string, error) { @@ -436,14 +435,20 @@ func compilePolicy(policy Policy) [][]string { } base = append(base, "--ctorigdstport", strconv.Itoa(int(policy.HostPort))) comment := "1panel-docker:" + policy.UUID - if policy.Mode == ModeAll { - return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", "DROP")} + if policy.Mode == ModeAll || policy.Mode == ModeAcceptAll { + target := "DROP" + if policy.Mode == ModeAcceptAll { + target = "ACCEPT" + } + return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", target)} } target := "DROP" capacity := len(policy.Sources) if policy.Mode == ModeAllow { target = "RETURN" capacity++ + } else if policy.Mode == ModeAcceptSources { + target = "ACCEPT" } rules := make([][]string, 0, capacity) for _, source := range policy.Sources { @@ -491,7 +496,11 @@ func chainDeclared(output, chain string) bool { func (m *Iptables) run(executable string, args ...string) (string, error) { commandArgs := append([]string{"-w", "-t", "filter"}, args...) - return m.runner.Run(executable, commandArgs...) + output, err := m.runner.Run(executable, commandArgs...) + if executable == "ip6tables" && err != nil && (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 } func executableForFamily(family string) string { @@ -533,3 +542,20 @@ func isWildcardHost(family, hostIP string) bool { return (family == FamilyIPv4 && (hostIP == "" || hostIP == "0.0.0.0")) || (family == FamilyIPv6 && (hostIP == "" || hostIP == "::")) } + +func (m *Iptables) OperateFamily(family string, initialize bool) error { + if family != FamilyIPv4 && family != FamilyIPv6 { + return fmt.Errorf("unsupported Docker firewall family %q", family) + } + if family == FamilyIPv4 { + if err := CheckIPv4Forwarding(); err != nil { + return err + } + } + mutationMu.Lock() + defer mutationMu.Unlock() + if initialize { + return m.ensureFamily(executableForFamily(family), true) + } + return m.bindExistingFamily(executableForFamily(family), true) +} diff --git a/agent/utils/firewall/docker_guard/nftables.go b/agent/utils/firewall/docker_guard/nftables.go index d92418b52..bfe41c127 100644 --- a/agent/utils/firewall/docker_guard/nftables.go +++ b/agent/utils/firewall/docker_guard/nftables.go @@ -1,13 +1,15 @@ package docker_guard import ( + "context" "errors" "fmt" "github.com/1Panel-dev/1Panel/agent/buserr" - "sort" + "slices" "strconv" "strings" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" ) @@ -23,9 +25,12 @@ type Nftables struct { runner Runner } -func NewNftables() *Nftables { return &Nftables{runner: commandRunner{}} } +func NewNftables(ctx context.Context) *Nftables { return &Nftables{runner: commandRunner{ctx: ctx}} } -func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) error { +func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory, families ...string) error { + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} + } mutationMu.Lock() defer mutationMu.Unlock() if !m.runner.Exists("nft") { @@ -34,26 +39,33 @@ func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) erro if err := CheckIPv4Forwarding(); err != nil { return err } - if err := m.checkForwardPolicy(); err != nil { + if err := m.checkForwardPolicy(families...); err != nil { return err } if err := m.ensureFamily(FamilyIPv4, true); err != nil { return err } - if err := m.ensureFamily(FamilyIPv6, false); err != nil { - return &FamilyError{Family: FamilyIPv6, Err: err} + if slices.Contains(families, FamilyIPv6) { + if err := m.ensureFamily(FamilyIPv6, false); err != nil { + return &FamilyError{Family: FamilyIPv6, Err: err} + } } - return m.rebuildLocked(policies, inventory) + return m.rebuildLocked(policies, inventory, families...) } -func (m *Nftables) Bind() error { +func (m *Nftables) Bind(families ...string) error { + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} + } mutationMu.Lock() defer mutationMu.Unlock() if err := m.bindExistingFamily(FamilyIPv4, true); err != nil { return err } - if err := m.bindExistingFamily(FamilyIPv6, false); err != nil { - return &FamilyError{Family: FamilyIPv6, Err: err} + if slices.Contains(families, FamilyIPv6) { + if err := m.bindExistingFamily(FamilyIPv6, false); err != nil { + return &FamilyError{Family: FamilyIPv6, Err: err} + } } return nil } @@ -68,11 +80,11 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) { if !m.runner.Exists("nft") { return PolicyInventory{}, nil } - inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)} + inventory := PolicyInventory{Policies: make([]Policy, 0), RuleOrders: make(map[string][]int64)} for _, family := range []string{FamilyIPv4, FamilyIPv6} { tableFamily := nftTableFamily(family) output, err := nftables_helper.ReadChain(m.run, tableFamily, NftTable, NftChain) - if errors.Is(err, nftables_helper.ErrChainNotFound) { + if errors.Is(err, nftables_helper.ErrChainNotFound) || (family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable)) { continue } if err != nil { @@ -83,18 +95,20 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) { return PolicyInventory{}, &FamilyError{Family: family, Err: err} } inventory.Policies = append(inventory.Policies, parsed.Policies...) - inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...) - for key, orders := range parsed.ManagedRuleOrders { - inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...) + for key, orders := range parsed.RuleOrders { + inventory.RuleOrders[key] = append([]int64(nil), orders...) } } return inventory, nil } -func (m *Nftables) Unbind() error { +func (m *Nftables) Unbind(families ...string) error { + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} + } mutationMu.Lock() defer mutationMu.Unlock() - for _, family := range []string{FamilyIPv4, FamilyIPv6} { + for _, family := range families { if err := m.unbindFamily(family); err != nil { return &FamilyError{Family: family, Err: err} } @@ -106,12 +120,16 @@ func (m *Nftables) Cleanup() error { mutationMu.Lock() defer mutationMu.Unlock() if !m.runner.Exists("nft") { - return nil + return errors.New("nft is not installed") } commands := make([][]string, 0, 2) for _, family := range []string{FamilyIPv4, FamilyIPv6} { tableFamily := nftTableFamily(family) - if !m.objectExists("table", tableFamily, NftTable) { + _, exists, err := nftables_helper.ReadTable(m.run, tableFamily, NftTable) + if err != nil { + return err + } + if !exists { continue } commands = append(commands, []string{"delete", "table", tableFamily, NftTable}) @@ -160,7 +178,7 @@ func (m *Nftables) Status(family string) FamilyStatus { } } if !baseExists || !guardExists { - return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing} + return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing, Partial: baseExists || guardExists} } status := FamilyStatus{State: StatusNotEffective, Initialized: true} rules := baseRules.String() @@ -260,11 +278,14 @@ func (m *Nftables) ensureJump(family string) error { return m.runBatch(commands) } -func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) error { +func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory, families ...string) error { + if len(families) == 0 { + families = []string{FamilyIPv4, FamilyIPv6} + } if !m.runner.Exists("nft") { return nil } - for _, family := range []string{FamilyIPv4, FamilyIPv6} { + for _, family := range families { tableFamily := nftTableFamily(family) if !m.objectExists("chain", tableFamily, NftTable, NftChain) { continue @@ -284,44 +305,37 @@ func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) e return nil } -type orderedNftRule struct { - order int64 - index int - rules [][]string -} - func orderedNftRules(family string, policies []Policy, inventory PolicyInventory) [][]string { tableFamily := nftTableFamily(family) - segments := make([]orderedNftRule, 0, len(policies)+len(inventory.ReadOnly)) + segments := make([]orderedPolicyRule, 0, len(policies)) maxOrder := int64(0) - index := 0 - for _, orders := range inventory.ManagedRuleOrders { + for _, orders := range inventory.RuleOrders { for _, order := range orders { - if order > maxOrder { - maxOrder = order - } + maxOrder = max(maxOrder, order) } } - for _, item := range inventory.ReadOnly { - for _, native := range item.NativeRules { - if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" { - continue - } - command := []string{"add", "rule", tableFamily, NftTable, NftChain} - command = append(command, quoteNftTokens(native.Tokens)...) - segments = append(segments, orderedNftRule{order: native.Order, index: index, rules: [][]string{command}}) - index++ - if native.Order > maxOrder { - maxOrder = native.Order - } + for _, policy := range policies { + for _, rule := range policy.NativeRules { + maxOrder = max(maxOrder, rule.Order) } } for _, policy := range policies { if policy.Family != family { continue } + if len(policy.NativeRules) > 0 { + for _, native := range policy.NativeRules { + if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" { + continue + } + command := []string{"add", "rule", tableFamily, NftTable, NftChain} + command = append(command, quoteNftTokens(native.Tokens)...) + segments = append(segments, orderedPolicyRule{order: native.Order, rule: command}) + } + continue + } compiled := compileNftPolicy(policy) - orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID] + orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID] for ruleIndex, rule := range compiled { order := int64(0) if ruleIndex < len(orders) { @@ -330,27 +344,16 @@ func orderedNftRules(family string, policies []Policy, inventory PolicyInventory maxOrder++ order = maxOrder } - segments = append(segments, orderedNftRule{order: order, index: index, rules: [][]string{rule}}) - index++ + segments = append(segments, orderedPolicyRule{order: order, rule: rule}) } } - sort.SliceStable(segments, func(left, right int) bool { - if segments[left].order == segments[right].order { - return segments[left].index < segments[right].index - } - return segments[left].order < segments[right].order - }) - rules := make([][]string, 0) - for _, segment := range segments { - rules = append(rules, segment.rules...) - } - return rules + return sortPolicyRules(segments) } func quoteNftTokens(tokens []string) []string { quoted := make([]string, 0, len(tokens)) - for _, token := range tokens { - if strings.ContainsAny(token, " \t\\\"'") && !strings.HasPrefix(token, `"`) { + for index, token := range tokens { + if (index > 0 && tokens[index-1] == "comment" || strings.ContainsAny(token, " \t\\\"'")) && !strings.HasPrefix(token, `"`) { quoted = append(quoted, strconv.Quote(token)) continue } @@ -367,15 +370,22 @@ func compileNftPolicy(policy Policy) [][]string { base = append(base, "ct", "original", addressKeyword, "daddr", policy.HostIP) } base = append(base, "ct", "original", "proto-dst", strconv.Itoa(int(policy.HostPort))) - comment := strconv.Quote("1panel-docker:" + policy.UUID) - if policy.Mode == ModeAll { - return [][]string{append(append([]string{}, base...), "drop", "comment", comment)} + marker := "1panel-docker:" + policy.UUID + comment := strconv.Quote(marker) + if policy.Mode == ModeAll || policy.Mode == ModeAcceptAll { + target := "drop" + if policy.Mode == ModeAcceptAll { + target = "accept" + } + return [][]string{append(append([]string{}, base...), target, "comment", comment)} } target := "drop" capacity := len(policy.Sources) if policy.Mode == ModeAllow { target = "return" capacity++ + } else if policy.Mode == ModeAcceptSources { + target = "accept" } rules := make([][]string, 0, capacity) for _, source := range policy.Sources { @@ -506,11 +516,14 @@ func nftHasFirstUniqueJump(output string) bool { return false } -func (m *Nftables) checkForwardPolicy() error { +func (m *Nftables) checkForwardPolicy(families ...string) error { for _, family := range []struct{ command, name string }{ {"iptables", FamilyIPv4}, {"ip6tables", FamilyIPv6}, } { + if len(families) > 0 && !slices.Contains(families, family.name) { + continue + } if !m.runner.Exists(family.command) { continue } @@ -526,11 +539,7 @@ func (m *Nftables) checkForwardPolicy() error { } found = true if fields[2] == "DROP" { - label := "IPv4" - if family.name == FamilyIPv6 { - label = "IPv6" - } - return &FamilyError{Family: family.name, Err: buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": label}, nil)} + return &FamilyError{Family: family.name, Err: buserr.New("ErrDockerForwardPolicyDrop")} } if fields[2] != "ACCEPT" { return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])} @@ -542,3 +551,23 @@ func (m *Nftables) checkForwardPolicy() error { } return nil } + +func (m *Nftables) OperateFamily(family string, initialize bool) error { + if family != FamilyIPv4 && family != FamilyIPv6 { + return fmt.Errorf("unsupported Docker firewall family %q", family) + } + if family == FamilyIPv4 { + if err := CheckIPv4Forwarding(); err != nil { + return err + } + } + mutationMu.Lock() + defer mutationMu.Unlock() + if err := m.checkForwardPolicy(family); err != nil { + return err + } + if initialize { + return m.ensureFamily(family, true) + } + return m.bindExistingFamily(family, true) +} diff --git a/agent/utils/firewall/docker_guard/policy.go b/agent/utils/firewall/docker_guard/policy.go index 287d24667..f47efdd88 100644 --- a/agent/utils/firewall/docker_guard/policy.go +++ b/agent/utils/firewall/docker_guard/policy.go @@ -1,8 +1,11 @@ package docker_guard import ( + "encoding/json" "fmt" + "github.com/google/uuid" "net/netip" + "slices" "sort" "strconv" "strings" @@ -12,9 +15,7 @@ import ( type observedPolicy struct { policy Policy - sequence int64 - nativeRules []NativeRule - managedOrders []int64 + orders []int64 dropAll bool droppedSource []string allowedSource []string @@ -22,6 +23,106 @@ type observedPolicy struct { acceptAll bool } +func ConvertPolicyBackend(policy Policy, sourceBackend, targetBackend string) (Policy, error) { + compiled := make(map[string][][]string, 2) + for _, backend := range []string{sourceBackend, targetBackend} { + switch backend { + case "iptables": + compiled[backend] = compilePolicy(policy) + case "nftables": + compiled[backend] = compileNftPolicy(policy) + for i := range compiled[backend] { + compiled[backend][i] = compiled[backend][i][5:] + } + default: + return Policy{}, fmt.Errorf("unsupported Docker firewall backend %q", backend) + } + } + if len(policy.NativeRules) == 0 { + return policy, nil + } + sourceRules, targetRules := compiled[sourceBackend], compiled[targetBackend] + if len(policy.NativeRules) != len(sourceRules) { + return Policy{}, fmt.Errorf("Docker policy %s contains native rules that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend) + } + byRule := make(map[string]int, len(sourceRules)) + for index, rule := range sourceRules { + byRule[dockerPolicyRuleKey(rule, sourceBackend)] = index + } + converted := make([]NativeRule, 0, len(policy.NativeRules)) + for _, native := range policy.NativeRules { + key := dockerPolicyRuleKey(native.Tokens, sourceBackend) + index, exists := byRule[key] + if !exists || native.Family != policy.Family { + return Policy{}, fmt.Errorf("Docker policy %s contains native conditions that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend) + } + delete(byRule, key) + tokens := append([]string(nil), targetRules[index]...) + for i, token := range tokens { + if unquoted, err := strconv.Unquote(token); err == nil { + tokens[i] = unquoted + } + } + converted = append(converted, NativeRule{Family: native.Family, Order: native.Order, Tokens: tokens}) + } + policy.NativeRules = converted + return policy, nil +} + +func dockerPolicyRuleKey(tokens []string, backend string) string { + tokens = nativeRuleTokens(tokens) + if backend == "iptables" { + if len(tokens)%2 != 0 { + return "" + } + protocol := "" + if index := slices.Index(tokens, "-p"); index >= 0 && index+1 < len(tokens) { + protocol = tokens[index+1] + } + parts := make([]string, 0, len(tokens)/2) + for i := 0; i < len(tokens); i += 2 { + option, value := tokens[i], tokens[i+1] + if option == "-m" && value == protocol && (value == "tcp" || value == "udp") { + continue + } + if option == "--ctorigdst" { + value = normalizeObservedHost(value) + } + if option == "-s" { + if address, err := netip.ParseAddr(value); err == nil { + value = netip.PrefixFrom(address, address.BitLen()).String() + } else if prefix, err := netip.ParsePrefix(value); err == nil { + value = prefix.Masked().String() + } + } + parts = append(parts, option+"\x00"+value) + } + sort.Strings(parts) + return strings.Join(parts, "\x01") + } + parts := make([]string, 0, len(tokens)) + for i, token := range tokens { + if token == "counter" { + continue + } + if unquoted, err := strconv.Unquote(token); err == nil { + token = unquoted + } + if i > 0 && tokens[i-1] == "daddr" { + token = normalizeObservedHost(token) + } + if i > 0 && tokens[i-1] == "saddr" { + if address, err := netip.ParseAddr(token); err == nil { + token = netip.PrefixFrom(address, address.BitLen()).String() + } else if prefix, err := netip.ParsePrefix(token); err == nil { + token = prefix.Masked().String() + } + } + parts = append(parts, token) + } + return strings.Join(parts, "\x00") +} + func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) { groups := make(map[string]*observedPolicy) order := make([]string, 0) @@ -35,33 +136,26 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) { if err != nil { return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err) } - managed := strings.Contains(line, "1panel-docker:") - if !managed && !hasAcceptAction(tokens) { - continue - } sequence++ fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family) if err != nil { return PolicyInventory{}, err } - identity := fragment.UUID - if action == "accept" { - identity = action + if action == "" || (fragment.HostPort == 0 && action == "return") { + continue } - key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|") + key := strings.Join([]string{fragment.UUID, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|") group, exists := groups[key] if !exists { - group = &observedPolicy{policy: fragment, sequence: sequence} + group = &observedPolicy{policy: fragment} groups[key] = group order = append(order, key) } switch { case action == "accept" && source != "": group.acceptedSource = append(group.acceptedSource, source) - group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)}) case action == "accept": group.acceptAll = true - group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)}) case action == "return" && source != "": group.allowedSource = append(group.allowedSource, source) case action == "drop" && source != "": @@ -71,21 +165,19 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) { default: return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action) } - if action != "accept" { - group.managedOrders = append(group.managedOrders, sequence) - } + group.policy.NativeRules = append(group.policy.NativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)}) + group.orders = append(group.orders, sequence) } - inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)} + inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), RuleOrders: make(map[string][]int64)} for _, key := range order { group := groups[key] - if group.acceptAll || len(group.acceptedSource) > 0 { - group.policy.Sources = uniqueSortedStrings(group.acceptedSource) - inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{ - Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules, - }) - continue - } switch { + case group.acceptAll: + group.policy.Mode = ModeAcceptAll + group.policy.Sources = []string{} + case len(group.acceptedSource) > 0: + group.policy.Mode = ModeAcceptSources + group.policy.Sources = uniqueSortedStrings(group.acceptedSource) case len(group.allowedSource) > 0: group.policy.Mode = ModeAllow group.policy.Sources = uniqueSortedStrings(group.allowedSource) @@ -97,8 +189,16 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) { default: return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID) } + if _, err := uuid.Parse(group.policy.UUID); err != nil { + rules := make([][]string, 0, len(group.policy.NativeRules)) + for _, rule := range group.policy.NativeRules { + rules = append(rules, rule.Tokens) + } + fingerprint, _ := json.Marshal(rules) + group.policy.UUID = uuid.NewSHA1(uuid.NameSpaceOID, append([]byte(family+"\x00"), fingerprint...)).String() + } inventory.Policies = append(inventory.Policies, group.policy) - inventory.ManagedRuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.managedOrders...) + inventory.RuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.orders...) } return inventory, nil } @@ -185,7 +285,10 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, action = tokens[index] } } - if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) { + if action != "accept" && action != "drop" && action != "return" { + return policy, "", "", nil + } + if action == "drop" && (policy.Protocol == "" || policy.HostPort == 0) { return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule") } if action == "accept" && policy.Protocol == "" { @@ -194,18 +297,6 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, return policy, source, action, nil } -func hasAcceptAction(tokens []string) bool { - for index, token := range tokens { - if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") { - return true - } - if strings.EqualFold(token, "accept") && !(index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment")) { - return true - } - } - return false -} - func normalizeObservedHost(value string) string { if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() { return prefix.Addr().String() @@ -241,3 +332,17 @@ func uniqueSortedStrings(values []string) []string { sort.Strings(result) return result } + +type orderedPolicyRule struct { + order int64 + rule []string +} + +func sortPolicyRules(segments []orderedPolicyRule) [][]string { + sort.SliceStable(segments, func(i, j int) bool { return segments[i].order < segments[j].order }) + rules := make([][]string, 0, len(segments)) + for _, segment := range segments { + rules = append(rules, segment.rule) + } + return rules +} diff --git a/agent/utils/firewall/docker_guard/runtime.go b/agent/utils/firewall/docker_guard/runtime.go index 54545bac6..ac0b18c94 100644 --- a/agent/utils/firewall/docker_guard/runtime.go +++ b/agent/utils/firewall/docker_guard/runtime.go @@ -18,6 +18,8 @@ const ( ModeSources = "deny_sources" ModeAllow = "allow_sources" ModeAll = "deny_all" + ModeAcceptSources = "accept_sources" + ModeAcceptAll = "accept_all" StatusEffective = "effective" StatusDisabled = "disabled" StatusNotEffective = "not_effective" @@ -35,7 +37,12 @@ type FamilyError struct { Err error } -func (e *FamilyError) Error() string { return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) } +func (e *FamilyError) Error() string { + if err, ok := e.Err.(buserr.BusinessError); ok && err.Msg == "ErrDockerForwardPolicyDrop" { + return err.Error() + } + return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) +} func (e *FamilyError) Unwrap() error { return e.Err } type ProxyEndpoint struct { @@ -55,16 +62,18 @@ type DNATRules struct { } type Policy struct { - UUID string - Family string - HostIP string - HostPort uint16 - Protocol string - Mode string - Sources []string + UUID string + Family string + HostIP string + HostPort uint16 + Protocol string + Mode string + Sources []string + NativeRules []NativeRule `json:",omitempty"` } type FamilyStatus struct { + Partial bool State string Reason string Initialized bool @@ -78,24 +87,17 @@ type NativeRule struct { Tokens []string `json:"tokens"` } -type ReadOnlyPolicy struct { - Policy Policy - Action string - Sequence int64 - NativeRules []NativeRule -} - type PolicyInventory struct { - Policies []Policy - ReadOnly []ReadOnlyPolicy - ManagedRuleOrders map[string][]int64 + Policies []Policy + RuleOrders map[string][]int64 } type Runtime interface { - Initialize([]Policy, PolicyInventory) error - Bind() error + Initialize([]Policy, PolicyInventory, ...string) error + Bind(...string) error + OperateFamily(string, bool) error ReplacePolicies([]Policy, PolicyInventory) error - Unbind() error + Unbind(...string) error Cleanup() error Initialized(string) (bool, error) Status(string) FamilyStatus diff --git a/agent/utils/firewall/filter/adapter.go b/agent/utils/firewall/filter/adapter.go index b40a92731..50a565826 100644 --- a/agent/utils/firewall/filter/adapter.go +++ b/agent/utils/firewall/filter/adapter.go @@ -9,28 +9,29 @@ var ( ErrAdapterUnavailable = errors.New("firewall rule adapter is unavailable") ErrInventoryUnavailable = errors.New("firewall rule inventory is unavailable") ErrFamilyUnavailable = errors.New("firewall address family is unavailable") + ErrRuleNotFound = errors.New("firewall rule does not exist") ) type ChangeOperation string const ( ChangeCreate ChangeOperation = "create" - ChangeAdopt ChangeOperation = "adopt" ChangeUpdate ChangeOperation = "update" ChangeDelete ChangeOperation = "delete" ChangeReorder ChangeOperation = "reorder" ) type RuleChange struct { - 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"` + Target *ObservedRule `json:"-"` + Raw string `json:"raw,omitempty"` + CommandOnly 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 { @@ -69,7 +70,6 @@ func (p CommandBatch) CreatesOnly() bool { type Adapter interface { Provider() Provider - Capabilities(context.Context) (Capabilities, error) ListRules(context.Context, Scope) (RuleSet, error) BuildCommands(RuleSet, []RuleChange) (CommandBatch, error) RunCommands(context.Context, CommandBatch) error diff --git a/agent/utils/firewall/filter/identity.go b/agent/utils/firewall/filter/identity.go index a854e5d2d..41348a553 100644 --- a/agent/utils/firewall/filter/identity.go +++ b/agent/utils/firewall/filter/identity.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "regexp" "strings" ) @@ -53,28 +54,6 @@ func RuleMatchKey(rule FirewallRule) (string, error) { return normalizedRuleKey(normalized) } -func OppositeActions(left, right Action) bool { - return left == ActionAccept && (right == ActionDrop || right == ActionReject) || - right == ActionAccept && (left == ActionDrop || left == ActionReject) -} - -func SameRuleContent(before, after FirewallRule) (bool, error) { - before, err := NormalizeRule(before) - if err != nil { - return false, err - } - after, err = NormalizeRule(after) - if err != nil { - return false, err - } - previous, err := RuleMatchKey(before) - if err != nil { - return false, err - } - requested, err := RuleMatchKey(after) - return err == nil && previous == requested && before.Action == after.Action, err -} - func normalizedRuleKey(normalized FirewallRule) (string, error) { identity := ruleIdentity{ Scope: normalized.Scope.Key(), @@ -231,3 +210,29 @@ func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) b wantKey, wantErr := RuleKey(expected) return gotErr == nil && wantErr == nil && gotKey == wantKey } + +var descriptionCounterPattern = regexp.MustCompile(`\bcounter packets \d+ bytes \d+\b`) + +func DescriptionID(observed ObservedRule) (string, error) { + if observed.ParseStatus == ParseStatusSupported { + key, err := RuleKey(observed.Rule) + return "firewall:" + key, err + } + raw := observed.Raw + if observed.Rule.Scope.Provider == ProviderUFW { + if end := strings.Index(raw, "]"); strings.HasPrefix(strings.TrimSpace(raw), "[") && end >= 0 { + raw = strings.Join(strings.Fields(raw[end+1:]), " ") + } + } + if observed.Rule.Scope.Provider == ProviderNftables { + raw = descriptionCounterPattern.ReplaceAllString(raw, "counter") + } + payload, err := json.Marshal(struct { + Scope Scope + Raw string + }{observed.Rule.Scope.Normalize(), raw}) + if err != nil { + return "", err + } + return fmt.Sprintf("firewall:%x", sha256.Sum256(payload)), nil +} diff --git a/agent/utils/firewall/filter/inventory.go b/agent/utils/firewall/filter/inventory.go index 0df1529c5..320ff10ec 100644 --- a/agent/utils/firewall/filter/inventory.go +++ b/agent/utils/firewall/filter/inventory.go @@ -1,70 +1,13 @@ package filter -import ( - "github.com/1Panel-dev/1Panel/agent/constant" -) - -type RuleOrigin string - -const ( - RuleOriginCreated RuleOrigin = constant.FirewallRuleOriginCreated - RuleOriginAdopted RuleOrigin = constant.FirewallRuleOriginAdopted -) - -type InventoryState string - -const ( - InventoryStateManaged InventoryState = "managed" - InventoryStateAdopted InventoryState = "adopted" - InventoryStateExternal InventoryState = "external" - InventoryStateDrifted InventoryState = "drifted" - InventoryStateProtected InventoryState = "protected" -) - -type InventoryMatch string - -const ( - InventoryMatchNone InventoryMatch = "none" - InventoryMatchExact InventoryMatch = "exact" - InventoryMatchChanged InventoryMatch = "changed" - InventoryMatchMissing InventoryMatch = "missing" - InventoryMatchAmbiguous InventoryMatch = "ambiguous" - InventoryMatchOpaque InventoryMatch = "opaque" -) - -type DesiredRule struct { - UUID string `json:"uuid"` - Rule FirewallRule `json:"rule"` - RuleKey string `json:"ruleKey"` - Origin RuleOrigin `json:"origin"` - Protected bool `json:"protected,omitempty"` - Expanded bool `json:"expanded,omitempty"` - Marker string `json:"marker,omitempty"` - ObservedInstanceKey string `json:"observedInstanceKey,omitempty"` -} - type PositionRange struct { Min int `json:"min"` Max int `json:"max"` } type InventoryItem struct { - Incompatible bool `json:"incompatible,omitempty"` - Error string `json:"error,omitempty"` - Rule FirewallRule `json:"rule"` - Observed *ObservedRule `json:"observed,omitempty"` - Desired *DesiredRule `json:"desired,omitempty"` - State InventoryState `json:"state"` - Match InventoryMatch `json:"match"` -} - -type Inventory struct { - Items []InventoryItem `json:"items"` - Notices []ScopeNotice `json:"notices,omitempty"` -} - -type InventoryMergeInput struct { - Observed []ObservedRule - Desired []DesiredRule - ProtectedObservedKeys map[string]struct{} + Rule FirewallRule `json:"rule"` + Observed *ObservedRule `json:"observed"` + IsWhitelist bool `json:"isWhitelist"` + DescriptionID string `json:"descriptionID"` } diff --git a/agent/utils/firewall/filter/model.go b/agent/utils/firewall/filter/model.go index 1a8033c64..d2975dd1d 100644 --- a/agent/utils/firewall/filter/model.go +++ b/agent/utils/firewall/filter/model.go @@ -88,9 +88,7 @@ const ( type ScopeNoticeCode string const ( - ScopeNoticeDefaultScopeMismatch ScopeNoticeCode = "default_scope_mismatch" ScopeNoticeManagedScopeInactive ScopeNoticeCode = "managed_scope_inactive" - ScopeNoticeUnmanagedActiveScopes ScopeNoticeCode = "unmanaged_active_scopes" ScopeNoticeRuntimePermanentMismatch ScopeNoticeCode = "runtime_permanent_mismatch" ScopeNoticeManagedScopeMissing ScopeNoticeCode = "managed_scope_missing" ScopeNoticeFamilyUnavailable ScopeNoticeCode = "family_unavailable" @@ -303,10 +301,3 @@ type RuleSet struct { Rules []ObservedRule `json:"rules"` Notices []ScopeNotice `json:"notices,omitempty"` } - -type Capabilities struct { - Marker bool - OwnedChains bool - ExplicitPosition bool - ExplicitPriority bool -} diff --git a/agent/utils/firewall/filter/providers/firewalld/adapter.go b/agent/utils/firewall/filter/providers/firewalld/adapter.go index a92def3f8..0f6d56fcc 100644 --- a/agent/utils/firewall/filter/providers/firewalld/adapter.go +++ b/agent/utils/firewall/filter/providers/firewalld/adapter.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "sort" "strconv" "strings" "sync" @@ -47,17 +46,6 @@ func NewAdapterWithBackend(reader CommandReader, writer CommandWriter) *Adapter func (a *Adapter) Provider() filter.Provider { return filter.ProviderFirewalld } -func (a *Adapter) Capabilities(ctx context.Context) (filter.Capabilities, error) { - explicitPriority, err := a.supportsRichRulePriority(ctx) - if err != nil { - return filter.Capabilities{}, err - } - return filter.Capabilities{ - - ExplicitPriority: explicitPriority, - }, nil -} - func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error { if rule.Priority == nil || *rule.Priority == 0 { return nil @@ -215,8 +203,13 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err return errors.New("firewalld writer is required") } executed, alreadyEnabled := 0, 0 + var missing error for index, command := range commands.Commands { err := a.writer.Run(ctx, command) + if errors.Is(err, filter.ErrRuleNotFound) && plan.CommandOnly { + missing = err + continue + } if errors.Is(err, ErrAlreadyEnabled) { alreadyEnabled++ err = nil @@ -232,7 +225,7 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err if commands.Operation == filter.ChangeCreate && alreadyEnabled > 0 && alreadyEnabled == len(commands.Commands) { return ErrAlreadyEnabled } - return nil + return missing } func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error { @@ -299,10 +292,66 @@ func batchCommands(plan filter.CommandBatch) (filter.RuleCommands, error) { } func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) { - rule := change.After - if change.Operation == filter.ChangeDelete { - rule = change.Before + if change.Operation == filter.ChangeCreate && change.Raw != "" && change.After != nil { + raw := strings.TrimSpace(change.Raw) + if strings.ContainsAny(raw, "\r\n\x00") { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + var expected filter.ObservedRule + kind := "rich-rule" + if change.After.NativeKind == filter.NativeKindZoneService { + if !validServiceName(raw) { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + expected = opaqueZoneService(snapshot.Scope, raw) + kind = "service" + } else { + if !strings.HasPrefix(raw, "rule ") { + return filter.RuleCommands{}, filter.ErrUnsupportedScope + } + expected = opaqueRichRule(change.After.Scope, raw) + } + commands, rollback := ruleCommands("--add-"+kind+"="+raw, "--remove-"+kind+"="+raw) + return filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected, Commands: commands, RollbackCommands: rollback}, nil } + if change.Operation == filter.ChangeDelete { + if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + kind, value, ok := strings.Cut(change.Target.Locator.Canonical, ":") + if !ok || value == "" || strings.ContainsAny(value, "\r\n\x00") { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + var target filter.ObservedRule + switch kind { + case "port": + rules := parseZonePorts(snapshot.Scope, value) + if len(rules) != 1 { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + target = rules[0] + case "service": + if !validServiceName(value) { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + target = opaqueZoneService(snapshot.Scope, value) + case "rich": + kind = "rich-rule" + if !strings.HasPrefix(value, "rule ") { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + var parsed bool + target, _, parsed = parseRichRule(snapshot.Scope, value) + if !parsed { + target = opaqueRichRule(snapshot.Scope, value) + } + default: + return filter.RuleCommands{}, filter.ErrUnsupportedScope + } + commands, rollback := ruleCommands("--remove-"+kind+"="+value, "--add-"+kind+"="+value) + return filter.RuleCommands{Operation: filter.ChangeDelete, Previous: &target, Expected: target, Commands: commands, RollbackCommands: rollback}, nil + } + rule := change.After if rule == nil { return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) } @@ -322,16 +371,8 @@ func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChang switch change.Operation { case filter.ChangeCreate: plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove")) - case filter.ChangeAdopt: - target, targetErr := validateMutationTarget(snapshot, change, normalized, false) - if targetErr != nil { - return filter.RuleCommands{}, targetErr - } - plan.Previous = &target - plan.Expected = target - plan.Expected.Rule.UUID = normalized.UUID case filter.ChangeUpdate: - target, targetErr := validateMutationTarget(snapshot, change, normalized, true) + target, targetErr := validateMutationTarget(snapshot, change) if targetErr != nil { return filter.RuleCommands{}, targetErr } @@ -343,19 +384,6 @@ func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChang addCommands, removeNewCommands := ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove")) plan.Commands = append(removeCommands, addCommands...) plan.RollbackCommands = append(restoreCommands, removeNewCommands...) - case filter.ChangeDelete: - if change.CommandOnly && change.Locator == nil { - plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "remove"), nativeOption(normalized, "add")) - break - } - target, targetErr := validateMutationTarget(snapshot, change, normalized, true) - if targetErr != nil { - return filter.RuleCommands{}, targetErr - } - plan.Previous = &target - plan.Expected = target - plan.Expected.Rule.UUID = normalized.UUID - plan.Commands, plan.RollbackCommands = observedRuleCommands(target, "remove", "add") default: return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) } @@ -441,42 +469,20 @@ func nativeOption(rule filter.FirewallRule, operation string) string { return "--" + operation + "-rich-rule=" + canonicalRichRule(rule) } -func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, normalized filter.FirewallRule, requireOwned bool) (filter.ObservedRule, error) { - if change.Locator == nil || change.Locator.Canonical == "" { - return filter.ObservedRule{}, fmt.Errorf("%w: firewalld mutation requires canonical locator", filter.ErrInvalidRule) - } - if change.Locator.Provider != "" && change.Locator.Provider != filter.ProviderFirewalld { - return filter.ObservedRule{}, fmt.Errorf("%w: locator provider mismatch", filter.ErrInvalidRule) - } - if change.Locator.ScopeKey != "" && change.Locator.ScopeKey != snapshot.Scope.Key() { - return filter.ObservedRule{}, fmt.Errorf("%w: locator scope mismatch", filter.ErrInvalidRule) - } - matches := make([]filter.ObservedRule, 0, 1) - for _, observed := range snapshot.Rules { - if observed.Locator.Canonical == change.Locator.Canonical { - matches = append(matches, observed) - } - } - if len(matches) != 1 { - return filter.ObservedRule{}, filter.ErrRuleStale - } - target := matches[0] - if target.Protected { - return filter.ObservedRule{}, filter.ErrProtectedRule +func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange) (filter.ObservedRule, error) { + target, err := filter.LocateRule(snapshot, change.Locator) + if err != nil { + return filter.ObservedRule{}, err } if target.ParseStatus != filter.ParseStatusSupported { - return filter.ObservedRule{}, filter.ErrRuleStale + return filter.ObservedRule{}, filter.ErrUnsupportedScope } - want := normalized - if requireOwned { - if change.Before == nil || change.Before.UUID == "" { - return filter.ObservedRule{}, fmt.Errorf("%w: managed previous rule is required", filter.ErrInvalidRule) - } - prepared, err := (&Adapter{}).PrepareRule(*change.Before) - if err != nil { - return filter.ObservedRule{}, err - } - want = prepared + if change.Before == nil { + return filter.ObservedRule{}, filter.ErrInvalidRule + } + want, err := (&Adapter{}).PrepareRule(*change.Before) + if err != nil { + return filter.ObservedRule{}, err } wantKey, wantErr := filter.RuleKey(want) targetKey, targetErr := filter.RuleKey(target.Rule) @@ -665,6 +671,7 @@ type mergedObject struct { func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filter.ObservedRule, error) { objects := make(map[string]*mergedObject) + var ordered []*mergedObject richObjects := make(map[string]*mergedObject) add := func(rule filter.ObservedRule, runtimeState bool) *mergedObject { key := string(rule.Rule.NativeKind) + "\x00" + rule.Locator.Canonical @@ -673,6 +680,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt copy := rule object = &mergedObject{rule: copy} objects[key] = object + ordered = append(ordered, object) } if runtimeState { object.runtime = true @@ -716,7 +724,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt } rules := make([]filter.ObservedRule, 0, len(objects)) - for _, object := range objects { + for _, object := range ordered { switch { case object.runtime && object.permanent: object.rule.Persistence = filter.PersistenceStatusConverged @@ -727,17 +735,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt } rules = append(rules, object.rule) } - sort.SliceStable(rules, func(i, j int) bool { - left, right := rules[i], rules[j] - leftRank, rightRank := bucketRank(left.Rule.OrderBucket), bucketRank(right.Rule.OrderBucket) - if leftRank != rightRank { - return leftRank < rightRank - } - if left.Rule.Priority != nil && right.Rule.Priority != nil && *left.Rule.Priority != *right.Rule.Priority { - return *left.Rule.Priority < *right.Rule.Priority - } - return left.Locator.Canonical < right.Locator.Canonical - }) + return rules, nil } @@ -1009,21 +1007,6 @@ func sameStringSet(left, right []string) bool { return true } -func bucketRank(bucket string) int { - switch bucket { - case filter.OrderBucketRichPre: - return 0 - case filter.OrderBucketRichZeroDeny: - return 1 - case filter.OrderBucketZonePrimitiveAllow, filter.OrderBucketRichZeroAllow: - return 2 - case filter.OrderBucketRichPost: - return 3 - default: - return 4 - } -} - func richOrderBucket(priority int, action filter.Action) string { if priority < 0 { return filter.OrderBucketRichPre @@ -1049,7 +1032,7 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro if err := validateSystemCommand(command); err != nil { return err } - options, removals, alreadyEnabled := 0, 0, 0 + options, removals, alreadyEnabled, missing := 0, 0, 0, 0 for _, arg := range command.Args { if strings.HasPrefix(arg, "--add-") || strings.HasPrefix(arg, "--remove-") { options++ @@ -1078,6 +1061,7 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro case strings.HasPrefix(line, "Warning: ALREADY_ENABLED:"): alreadyEnabled++ case strings.HasPrefix(line, "Warning: NOT_ENABLED:"): + missing++ if removals != options { return fmt.Errorf("%w: %s", filter.ErrRuleStale, stderr.String()) } @@ -1088,6 +1072,9 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro if alreadyEnabled > 0 && alreadyEnabled == options { return ErrAlreadyEnabled } + if missing > 0 { + return fmt.Errorf("%w: %s", filter.ErrRuleNotFound, stderr.String()) + } return nil } diff --git a/agent/utils/firewall/filter/providers/iptables/adapter.go b/agent/utils/firewall/filter/providers/iptables/adapter.go index 22f5811a3..2e9810569 100644 --- a/agent/utils/firewall/filter/providers/iptables/adapter.go +++ b/agent/utils/firewall/filter/providers/iptables/adapter.go @@ -49,12 +49,6 @@ func NewAdapterWithBackend(reader RuleReader, writer RuleWriter) *Adapter { func (a *Adapter) Provider() filter.Provider { return filter.ProviderIptables } -func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) { - return filter.Capabilities{ - Marker: true, OwnedChains: true, ExplicitPosition: true, - }, nil -} - func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error { rule, err := filter.NormalizeRule(rule) if err != nil { @@ -163,8 +157,7 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh if createOnly { return compileCreateBatch(snapshot, changes) } - externalDelete := len(changes) == 1 && changes[0].Locator == nil && (changes[0].UnmarkedAdopted || changes[0].PreviousMarker != "") - if deleteOnly && !externalDelete { + if deleteOnly { return compileDeleteBatch(snapshot, changes) } if len(changes) != 1 { @@ -221,14 +214,15 @@ func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f var script strings.Builder fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table) for _, change := range changes { - rulePlan, err := compileChange(snapshot, change) - if err != nil { - return filter.CommandBatch{}, err + if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() || change.Target.Raw == "" { + return filter.CommandBatch{}, filter.ErrInvalidRule } - line, err := restoreRuleLine(snapshot.Scope, *rulePlan.Previous) + line, err := restoreRuleLine(snapshot.Scope, *change.Target) if err != nil { return filter.CommandBatch{}, err } + target := parseRule(snapshot.Scope, line, 0) + rulePlan := filter.RuleCommands{Operation: filter.ChangeDelete, Previous: &target, Expected: target} script.WriteString(strings.Replace(line, "-A ", "-D ", 1)) script.WriteByte('\n') rulePlan.Commands, rulePlan.RollbackCommands = nil, nil @@ -238,6 +232,14 @@ func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f plan.Rules[0].Commands = []filter.NativeCommand{{ Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: script.String(), }} + if len(changes) == 1 { + args, err := shellwords.Parse(plan.Rules[0].Expected.Raw) + if err != nil { + return filter.CommandBatch{}, err + } + args[0] = "-D" + plan.Rules[0].Commands = []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: append([]string{"-w", "-t", snapshot.Scope.Table}, args...)}} + } return plan, nil } @@ -246,6 +248,17 @@ func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f var script strings.Builder fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table) for _, change := range changes { + if change.Raw != "" { + observed := parseRule(snapshot.Scope, change.Raw, len(snapshot.Rules)+1) + line, err := restoreRuleLine(snapshot.Scope, observed) + if err != nil { + return filter.CommandBatch{}, err + } + script.WriteString(line + "\n") + observed.Locator.Position = nil + plan.Rules = append(plan.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: observed}) + continue + } rulePlan, err := compileChange(snapshot, change) if err != nil { return filter.CommandBatch{}, err @@ -379,17 +392,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru return filter.RuleCommands{}, fmt.Errorf("%w: protocol %q does not match %s", filter.ErrInvalidRule, normalized.Protocol, normalized.Scope.Family) } marker := "1panel-rule:" + normalized.UUID - if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil { - previous := filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported} - var commands []filter.NativeCommand - if change.UnmarkedAdopted || change.PreviousMarker != "" { - previous.Marker = change.PreviousMarker - args := []string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain} - args = append(args, compileObservedRuleArgs(previous)...) - commands = []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: args}} - } - return filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Previous: &previous, Expected: previous, Commands: commands}, nil - } + position := len(snapshot.Rules) + 1 verb := "-I" var target filter.ObservedRule @@ -400,12 +403,6 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru return filter.RuleCommands{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule) } position = insertionPosition(snapshot, normalized) - case filter.ChangeAdopt: - position, target, err = validateMutationTarget(snapshot, change, normalized, marker) - if err != nil { - return filter.RuleCommands{}, err - } - verb = "-R" case filter.ChangeUpdate: position, target, err = validateMutationTarget(snapshot, change, normalized, marker) if err != nil { @@ -450,7 +447,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru case filter.ChangeCreate: rollbackArgs = append(rollbackArgs, "-D", snapshot.Scope.Chain) rollbackArgs = append(rollbackArgs, compileRuleArgs(normalized, marker)...) - case filter.ChangeAdopt, filter.ChangeUpdate: + case filter.ChangeUpdate: rollbackArgs = append(rollbackArgs, "-R", snapshot.Scope.Chain, strconv.Itoa(position)) rollbackArgs = append(rollbackArgs, compileObservedRuleArgs(target)...) case filter.ChangeDelete: @@ -483,10 +480,11 @@ func positionalMutationPlan(snapshot filter.RuleSet, rule filter.FirewallRule, p RuleUUID: rule.UUID, Operation: operation, Previous: &previous, Expected: expected, } if position == targetPosition { + plan.Expected = previous return plan } executable := executableForFamily(snapshot.Scope.Family) - deleteArgs := append([]string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain}, compileObservedRuleArgs(previous)...) + deleteArgs := []string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain, strconv.Itoa(position)} insertArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(targetPosition)} insertArgs = append(insertArgs, compileRuleArgs(rule, marker)...) restoreArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(position)} @@ -587,8 +585,7 @@ func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, a if wantErr != nil || observedErr != nil || wantKey != observedKey { return 0, filter.ObservedRule{}, filter.ErrRuleStale } - if change.Operation != filter.ChangeAdopt && observed.Marker != marker && - !(change.Operation == filter.ChangeDelete && change.UnmarkedAdopted && observed.Marker == "") { + if observed.Marker != change.PreviousMarker { return 0, filter.ObservedRule{}, filter.ErrRuleStale } return position, observed, nil @@ -654,11 +651,15 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro if err != nil { return err } - options := []cmd.Option{cmd.WithContext(ctx), cmd.WithTimeout(60 * time.Second)} + options := []cmd.Option{cmd.WithContext(ctx), cmd.WithTimeout(60 * time.Second), cmd.WithEnv("LC_ALL=C")} if command.Stdin != "" { options = append(options, cmd.WithStdin(strings.NewReader(command.Stdin))) } - return cmd.NewCommandMgr(options...).RunWithOptionalSudo(executable, command.Args...) + err = cmd.NewCommandMgr(options...).RunWithOptionalSudo(executable, command.Args...) + if err != nil && (strings.Contains(err.Error(), "Bad rule (does a matching rule exist in that chain?)") || strings.Contains(err.Error(), "No chain/target/match by that name")) { + return fmt.Errorf("%w: %v", filter.ErrRuleNotFound, err) + } + return err } func (systemBackend) Save(ctx context.Context, scope filter.Scope) error { diff --git a/agent/utils/firewall/filter/providers/nftables/adapter.go b/agent/utils/firewall/filter/providers/nftables/adapter.go index 1a1f1ed61..c24a6ba42 100644 --- a/agent/utils/firewall/filter/providers/nftables/adapter.go +++ b/agent/utils/firewall/filter/providers/nftables/adapter.go @@ -33,12 +33,6 @@ func NewAdapterWithBackend(backend Backend) *Adapter { return &Adapter{backend: func (a *Adapter) Provider() filter.Provider { return filter.ProviderNftables } -func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) { - return filter.Capabilities{ - Marker: true, OwnedChains: true, ExplicitPosition: true, - }, nil -} - func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error { rule, err := filter.NormalizeRule(rule) if err != nil { @@ -164,7 +158,7 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh return filter.CommandBatch{}, fmt.Errorf("%w: nftables mutation requires exactly one change", filter.ErrInvalidRule) } change := changes[0] - if change.Operation != filter.ChangeUpdate && change.Operation != filter.ChangeAdopt && change.Operation != filter.ChangeReorder { + if change.Operation != filter.ChangeUpdate && change.Operation != filter.ChangeReorder { return filter.CommandBatch{}, fmt.Errorf("%w: unsupported nftables mutation %s", filter.ErrInvalidRule, change.Operation) } expected, previous, err := compileChange(snapshot, change) @@ -180,7 +174,9 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh target := *expected.Locator.Position var script string if target == *previous.Locator.Position { - if change.Operation != filter.ChangeReorder { + if change.Operation == filter.ChangeReorder { + rulePlan.Expected = *previous + } else { script = fmt.Sprintf("replace rule %s handle %s %s\n", chain, handle, expected.Raw) if strings.ContainsAny(previous.Raw, "\r\n") || previous.Raw == "" { return filter.CommandBatch{}, fmt.Errorf("%w: invalid native nftables rule", filter.ErrInvalidRule) @@ -213,17 +209,17 @@ func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f plan := filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: true} var script strings.Builder for _, change := range changes { - expected, previous, err := compileChange(snapshot, change) - if err != nil { - return filter.CommandBatch{}, err + if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() || change.Target.Raw == "" { + return filter.CommandBatch{}, filter.ErrInvalidRule } - handle := previous.Locator.NativeID - if _, err := strconv.ParseUint(handle, 10, 64); err != nil || change.Locator.NativeID != handle { - return filter.CommandBatch{}, filter.ErrRuleStale + handle := change.Target.Locator.NativeID + if _, err := strconv.ParseUint(handle, 10, 64); err != nil { + return filter.CommandBatch{}, filter.ErrInvalidRule } + target := parseRule(snapshot.Scope, change.Target.Raw, handle, 0) fmt.Fprintf(&script, "delete rule %s %s %s handle %s\n", nftables_helper.TableFamily(snapshot.Scope.Family), nftables_helper.TableName, nativeChainName(snapshot.Scope), handle) plan.Rules = append(plan.Rules, filter.RuleCommands{ - RuleUUID: ruleUUID(change), Operation: filter.ChangeDelete, Previous: previous, Expected: expected, + Operation: filter.ChangeDelete, Previous: &target, Expected: target, }) } plan.Rules[0].Commands = []filter.NativeCommand{{Executable: "nft", Stdin: script.String()}} @@ -234,6 +230,16 @@ func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f plan := filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: true} var script strings.Builder for _, change := range changes { + if change.Raw != "" { + if strings.ContainsAny(change.Raw, "\r\n;\x00") { + return filter.CommandBatch{}, filter.ErrInvalidRule + } + expected := parseRule(snapshot.Scope, change.Raw, "", len(snapshot.Rules)+1) + expected.Locator.Position = nil + fmt.Fprintf(&script, "add rule %s %s %s %s\n", nftables_helper.TableFamily(snapshot.Scope.Family), nftables_helper.TableName, nativeChainName(snapshot.Scope), change.Raw) + plan.Rules = append(plan.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected}) + continue + } if change.After == nil { return filter.CommandBatch{}, fmt.Errorf("%w: create rule is required", filter.ErrInvalidRule) } @@ -664,7 +670,11 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro return fmt.Errorf("unexpected nftables executable %q", command.Executable) } if command.Stdin != "" { - return nftables_helper.RunScriptContext(ctx, command.Stdin) + err := nftables_helper.RunScriptContext(ctx, command.Stdin) + if err != nil && strings.Contains(err.Error(), "No such file or directory") && strings.HasPrefix(command.Stdin, "delete rule ") { + return fmt.Errorf("%w: %v", filter.ErrRuleNotFound, err) + } + return err } return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)). RunWithOptionalSudo(command.Executable, command.Args...) diff --git a/agent/utils/firewall/filter/providers/ufw/adapter.go b/agent/utils/firewall/filter/providers/ufw/adapter.go index f153e89e5..65e8e3a11 100644 --- a/agent/utils/firewall/filter/providers/ufw/adapter.go +++ b/agent/utils/firewall/filter/providers/ufw/adapter.go @@ -9,12 +9,13 @@ import ( "strings" "time" - "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/re" ) +var ErrAlreadyEnabled = errors.New("ufw rule already exists") + type CommandReader interface { filter.CommentRuleReader Read(context.Context, ...string) (string, error) @@ -79,13 +80,6 @@ func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule return a.writer.Run(ctx, command) } -func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) { - return filter.Capabilities{ - - Marker: true, ExplicitPosition: true, - }, nil -} - func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, comment string) ([]filter.ObservedRule, error) { var rules []filter.ObservedRule var output string @@ -101,7 +95,7 @@ func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, return nil, err } } - rules = append(rules, parseNumberedRules(scope, output)...) + rules = append(rules, ParseRules(scope, output)...) } return rules, nil } @@ -150,7 +144,7 @@ func (a *Adapter) ListRuleScopes(ctx context.Context, scopes []filter.Scope) ([] notices := statusNotices(numbered) snapshots := make([]filter.RuleSet, 0, len(normalizedScopes)) for _, scope := range normalizedScopes { - snapshot, err := filter.NewRuleSet(scope, parseNumberedRules(scope, numbered)) + snapshot, err := filter.NewRuleSet(scope, ParseRules(scope, numbered)) if err != nil { return nil, err } @@ -215,35 +209,11 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err executed := 0 for index, command := range plan.Rules[0].Commands { err := a.writer.Run(ctx, command) - if err != nil && plan.CommandOnly && plan.CreatesOnly() && command.Args[0] == "insert" && - (strings.Contains(err.Error(), "Invalid position") || strings.Contains(err.Error(), "Cannot insert rule at position")) { - ipv4, ipv6 := plan.Scope, plan.Scope - ipv4.Family, ipv6.Family = filter.FamilyIPv4, filter.FamilyIPv6 - snapshots, readErr := a.ListRuleScopes(ctx, []filter.Scope{ipv4, ipv6}) - if readErr != nil { - return errors.Join(err, readErr) - } - lastPosition := maximumObservedPosition(snapshots[0]) - if plan.Scope.Family == filter.FamilyIPv6 { - lastPosition = max(lastPosition, maximumObservedPosition(snapshots[1])) - } - position, _ := strconv.Atoi(command.Args[1]) - if position == lastPosition+1 && !hasScopeNotice(snapshots[0].Notices, filter.ScopeNoticeManagedScopeInactive) { - err = a.writer.Run(ctx, commentCommand(plan.Rules[0].Expected.Rule, plan.Rules[0].Expected.Marker)) - } - } if err != nil { if plan.CommandOnly { return 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)) + cause := fmt.Errorf("execute UFW rule: %w", err) return a.compensate(ctx, plan.Rules[0], executed, cause) } executed = index + 1 @@ -251,39 +221,6 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err return nil } -func ufwApplyError(plan filter.RuleCommands, cause error) error { - if plan.Operation == filter.ChangeAdopt { - return buserr.WithDetail("ErrUFWRuleAdopt", cause.Error(), cause) - } - return cause -} - -func (a *Adapter) failedCommandApplied(ctx context.Context, plan filter.RuleCommands, commandIndex int) bool { - snapshot, err := a.ListRules(ctx, plan.Expected.Rule.Scope) - if err != nil { - return true - } - markerCount := countMarker(snapshot, plan.Expected.Marker) - switch plan.Operation { - case filter.ChangeCreate: - return markerCount > 0 - case filter.ChangeAdopt: - if commandIndex == 0 { - return plan.Previous == nil || !containsObservedRule(snapshot, *plan.Previous) - } - return markerCount > 0 - case filter.ChangeUpdate, filter.ChangeReorder: - if commandIndex == 0 { - return markerCount == 0 - } - return markerCount > 0 - case filter.ChangeDelete: - return markerCount == 0 - default: - return true - } -} - func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error { if err := validateBackendPlan(plan); err != nil { return err @@ -317,17 +254,71 @@ func validateBackendPlan(plan filter.CommandBatch) error { if err := validateScope(plan.Scope); err != nil { return err } - if len(plan.Rules[0].Commands) != len(plan.Rules[0].RollbackCommands) { + if !plan.CommandOnly && len(plan.Rules[0].Commands) != len(plan.Rules[0].RollbackCommands) { return fmt.Errorf("%w: incomplete ufw rollback plan", filter.ErrInvalidRule) } return nil } func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) { - rule := change.After - if change.Operation == filter.ChangeDelete { - rule = change.Before + if change.Operation == filter.ChangeCreate && change.Raw != "" { + observed := ParseRules(snapshot.Scope, change.Raw) + if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported && change.After != nil { + rule := observed[0].Rule + rule.UUID, rule.OrderIndex = change.After.UUID, nil + if rule.UUID == "" { + rule.UUID = "import" + } + change.After, change.Raw = &rule, "" + return compileChange(snapshot, change) + } + if len(observed) != 1 || observed[0].Rule.NativeKind != filter.NativeKindUFWApplication || observed[0].ParseStatus != filter.ParseStatusOpaque { + return filter.RuleCommands{}, fmt.Errorf("%w: UFW listing cannot fully describe this rule", filter.ErrUnsupportedScope) + } + target := observed[0] + rule := target.Rule + if !validApplicationProfileName(rule.Description) || nativeAction(rule.Action) == "" { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + args := []string{nativeAction(rule.Action), "in"} + if rule.Interface != "" { + args = append(args, "on", rule.Interface) + } + args = append(args, "from", nativeAddress(rule.SourceAddress, rule.Scope.Family), "to", nativeAddress("", rule.Scope.Family), "app", rule.Description) + if _, comment, ok := strings.Cut(change.Raw, "#"); ok { + args = append(args, "comment", strings.TrimSpace(comment)) + } + return filter.RuleCommands{Operation: filter.ChangeCreate, Expected: target, Commands: []filter.NativeCommand{{Executable: "ufw", Args: args}}}, nil } + if change.Operation == filter.ChangeDelete { + if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + targets := ParseRules(snapshot.Scope, change.Target.Raw) + if len(targets) != 1 { + return filter.RuleCommands{}, filter.ErrInvalidRule + } + target := targets[0] + var command filter.NativeCommand + if target.ParseStatus == filter.ParseStatusSupported { + command = deleteRuleCommand(target.Rule, observedComment(target)) + } else if target.Rule.NativeKind == filter.NativeKindUFWApplication && target.ParseStatus == filter.ParseStatusOpaque { + matches := re.UFWNumberedRuleRegex.FindStringSubmatch(strings.TrimSpace(target.Raw)) + if len(matches) == 0 || matches[3] == "LIMIT" { + return filter.RuleCommands{}, filter.ErrUnsupportedScope + } + created, err := compileChange(snapshot, filter.RuleChange{Operation: filter.ChangeCreate, After: &target.Rule, Raw: target.Raw}) + if err != nil { + return filter.RuleCommands{}, err + } + command = created.Commands[0] + command.Args = append([]string{"--force", "delete"}, command.Args...) + } else { + return filter.RuleCommands{}, fmt.Errorf("%w: UFW listing cannot fully describe this rule", filter.ErrUnsupportedScope) + } + return filter.RuleCommands{Operation: filter.ChangeDelete, Previous: &target, Expected: target, Commands: []filter.NativeCommand{command}}, nil + } + rule := change.After if rule == nil { return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) } @@ -345,17 +336,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru return filter.RuleCommands{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule) } marker := "1panel-rule:" + normalized.UUID - if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil { - if change.UnmarkedAdopted || change.PreviousMarker != "" { - marker = observedComment(filter.ObservedRule{Rule: normalized, Marker: change.PreviousMarker}) - } - return filter.RuleCommands{ - RuleUUID: normalized.UUID, Operation: change.Operation, - Expected: filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported}, - Commands: []filter.NativeCommand{deleteRuleCommand(normalized, marker)}, - RollbackCommands: []filter.NativeCommand{commentCommand(normalized, marker)}, - }, nil - } + position := insertionPosition(snapshot, normalized) expected := observedForRule(normalized, marker, position) plan := filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected} @@ -378,28 +359,6 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru } plan.Commands = []filter.NativeCommand{command} plan.RollbackCommands = []filter.NativeCommand{deleteRuleCommand(normalized, marker)} - case filter.ChangeAdopt: - target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, false) - if targetErr != nil { - return filter.RuleCommands{}, targetErr - } - position = *target.Locator.Position - appendAtEnd := position == maximumObservedPosition(snapshot) - restoreAtEnd := change.RestoreAtEnd || appendAtEnd - plan.Previous = &target - plan.Expected = observedForRule(normalized, marker, position) - if appendAtEnd { - plan.Expected.Locator.NativeID = "" - plan.Expected.Locator.Position = nil - } - plan.Commands = []filter.NativeCommand{ - deletePositionCommand(position), - positionedCommand(position, normalized, marker, appendAtEnd), - } - plan.RollbackCommands = []filter.NativeCommand{ - positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd), - deleteRuleCommand(normalized, marker), - } case filter.ChangeUpdate, filter.ChangeReorder: if change.Before == nil { return filter.RuleCommands{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule) @@ -408,7 +367,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru if err != nil { return filter.RuleCommands{}, err } - target, targetErr := validateMutationTarget(snapshot, change, before, marker, true) + target, targetErr := validateMutationTarget(snapshot, change, before) if targetErr != nil { return filter.RuleCommands{}, targetErr } @@ -443,19 +402,6 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd), deleteRuleCommand(normalized, marker), } - case filter.ChangeDelete: - target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, !change.UnmarkedAdopted) - if targetErr != nil { - return filter.RuleCommands{}, targetErr - } - position = *target.Locator.Position - plan.Previous = &target - plan.Expected = target - plan.Commands = []filter.NativeCommand{deleteRuleCommand(target.Rule, observedComment(target))} - restoreAtEnd := change.RestoreAtEnd || position == maximumObservedPosition(snapshot) - plan.RollbackCommands = []filter.NativeCommand{ - positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd), - } default: return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) } @@ -476,71 +422,20 @@ func validateWritableRule(rule filter.FirewallRule) error { return nil } -func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, desired filter.FirewallRule, marker string, requireOwned bool) (filter.ObservedRule, error) { - if change.Locator == nil || change.Locator.Position == nil { - return filter.ObservedRule{}, fmt.Errorf("%w: ufw mutation requires a numbered locator", filter.ErrInvalidRule) +func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, desired filter.FirewallRule) (filter.ObservedRule, error) { + target, err := filter.LocateRule(snapshot, change.Locator) + if err != nil { + return filter.ObservedRule{}, err } - if change.Locator.Provider != "" && change.Locator.Provider != filter.ProviderUFW { - return filter.ObservedRule{}, fmt.Errorf("%w: ufw locator provider mismatch", filter.ErrInvalidRule) - } - if change.Locator.ScopeKey != "" && change.Locator.ScopeKey != snapshot.Scope.Key() { - return filter.ObservedRule{}, fmt.Errorf("%w: ufw locator scope mismatch", filter.ErrInvalidRule) - } - var matches []filter.ObservedRule - for _, observed := range snapshot.Rules { - if observed.Locator.Position == nil || *observed.Locator.Position != *change.Locator.Position { - continue - } - if change.Locator.NativeID != "" && observed.Locator.NativeID != change.Locator.NativeID { - continue - } - if change.Locator.Canonical != "" && observed.Locator.Canonical != change.Locator.Canonical { - continue - } - matches = append(matches, observed) - } - if len(matches) != 1 { + if target.Marker != strings.TrimSpace(change.PreviousMarker) { return filter.ObservedRule{}, filter.ErrRuleStale } - target := matches[0] - if target.Protected { - return filter.ObservedRule{}, filter.ErrProtectedRule - } - previousMarker := strings.TrimSpace(change.PreviousMarker) - if requireOwned { - if target.Marker != marker { - return filter.ObservedRule{}, fmt.Errorf("%w: ufw rule is not owned by this rule UUID", filter.ErrInvalidRule) - } - } else if target.Marker != previousMarker { - if target.Marker != "" { - return filter.ObservedRule{}, fmt.Errorf("%w: ufw adoption target marker changed", filter.ErrRuleStale) - } - return filter.ObservedRule{}, filter.ErrRuleStale - } - semanticMatch := filter.ObservedRuleMatchesExpected(target, desired) - canHydrateOwned := target.Marker == marker && - (target.ParseStatus == filter.ParseStatusOpaque || semanticMatch) - canHydrateAdopted := !requireOwned && target.Marker == previousMarker && - target.ParseStatus != filter.ParseStatusOpaque && semanticMatch - if target.ParseStatus != filter.ParseStatusSupported && (canHydrateOwned || canHydrateAdopted) { - orderIndex := target.Rule.OrderIndex - target.Rule = desired - target.Rule.OrderIndex = orderIndex - target.ParseStatus = filter.ParseStatusSupported - target.UncertainFields = nil - } if target.ParseStatus != filter.ParseStatusSupported { - return filter.ObservedRule{}, fmt.Errorf("%w: opaque ufw rules cannot be modified", filter.ErrInvalidRule) + return filter.ObservedRule{}, filter.ErrUnsupportedScope } - wantKey, err := filter.RuleKey(desired) - if err != nil { - return filter.ObservedRule{}, err - } - gotKey, err := filter.RuleKey(target.Rule) - if err != nil { - return filter.ObservedRule{}, err - } - if change.Operation == filter.ChangeAdopt && wantKey != gotKey { + wantKey, wantErr := filter.RuleKey(desired) + gotKey, gotErr := filter.RuleKey(target.Rule) + if wantErr != nil || gotErr != nil || wantKey != gotKey { return filter.ObservedRule{}, filter.ErrRuleStale } return target, nil @@ -680,29 +575,6 @@ func (a *Adapter) rollback(ctx context.Context, plan filter.RuleCommands, execut return rollbackErr } -func countMarker(snapshot filter.RuleSet, marker string) int { - count := 0 - for _, observed := range snapshot.Rules { - if observed.Marker == marker { - count++ - } - } - return count -} - -func containsObservedRule(snapshot filter.RuleSet, expected filter.ObservedRule) bool { - for _, observed := range snapshot.Rules { - if expected.Locator.Position != nil && - (observed.Locator.Position == nil || *observed.Locator.Position != *expected.Locator.Position) { - continue - } - if observed.Marker == expected.Marker && filter.ObservedRuleMatchesExpected(observed, expected.Rule) { - return true - } - } - return false -} - func hasScopeNotice(notices []filter.ScopeNotice, code filter.ScopeNoticeCode) bool { for _, notice := range notices { if notice.Code == code { @@ -731,7 +603,7 @@ func validateCommand(command filter.NativeCommand) error { } } -func parseNumberedRules(scope filter.Scope, output string) []filter.ObservedRule { +func ParseRules(scope filter.Scope, output string) []filter.ObservedRule { rules := make([]filter.ObservedRule, 0) for _, rawLine := range strings.Split(output, "\n") { raw := strings.TrimSpace(rawLine) @@ -802,9 +674,13 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di return opaque() } + sourceProtocol := "all" sourceAddress, ok := parseSource(source) if !ok { - return opaque() + sourceAddress, sourceProtocol, ok = parseAddressProtocol(source) + if !ok { + return opaque() + } } partialRule.SourceAddress = sourceAddress if ruleAction == "" { @@ -827,6 +703,12 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di Persistence: filter.PersistenceStatusConverged, } } + if sourceProtocol != "all" { + if protocol != "all" && protocol != sourceProtocol || destinationPort != "" { + return opaque() + } + protocol = sourceProtocol + } partialRule.Protocol = protocol partialRule.DestinationAddress = destinationAddress partialRule.DestinationPort = destinationPort @@ -861,15 +743,9 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di return opaque() } locator.Canonical = canonicalRule(normalized) - parseStatus := filter.ParseStatusSupported - var uncertainFields []string - if normalized.Protocol == "all" && normalized.DestinationPort == "" { - parseStatus = filter.ParseStatusPartial - uncertainFields = []string{filter.ObservedFieldProtocol} - } return filter.ObservedRule{ - Rule: normalized, Locator: locator, Marker: marker, ParseStatus: parseStatus, - UncertainFields: uncertainFields, Raw: raw, Persistence: filter.PersistenceStatusConverged, + Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, + Raw: raw, Persistence: filter.PersistenceStatusConverged, } } @@ -937,6 +813,9 @@ func familyForNumberedRule(destination, source string) filter.Family { func containsIPv6Address(value string) bool { for _, token := range strings.Fields(value) { token = strings.Trim(token, "(),") + if address, _, ok := parseAddressProtocol(token); ok { + token = address + } if prefix, err := netip.ParsePrefix(token); err == nil && prefix.Addr().Is6() { return true } @@ -1225,11 +1104,14 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro output, err := cmd.NewCommandMgr( cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"), ).RunWithOptionalSudoAndStdout(command.Executable, command.Args...) + if strings.Contains(output, "Could not delete non-existent rule") || err != nil && strings.Contains(err.Error(), "Could not delete non-existent rule") { + return fmt.Errorf("%w: %s", filter.ErrRuleNotFound, strings.TrimSpace(output)) + } if err != nil { return err } - if strings.Contains(output, "Could not delete non-existent rule") { - return fmt.Errorf("%w: %s", filter.ErrRuleStale, strings.TrimSpace(output)) + if strings.Contains(output, "Skipping adding existing rule") { + return fmt.Errorf("%w: %s", ErrAlreadyEnabled, strings.TrimSpace(output)) } return nil } diff --git a/agent/utils/firewall/filter/safety.go b/agent/utils/firewall/filter/safety.go index 8c503f0cd..5a0b237eb 100644 --- a/agent/utils/firewall/filter/safety.go +++ b/agent/utils/firewall/filter/safety.go @@ -2,28 +2,20 @@ package filter import ( "errors" - "fmt" - "net/netip" "slices" - "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 ProtectRuleSet(snapshot RuleSet, ports []PortWhitelist) (RuleSet, error) { rules := slices.Clone(snapshot.Rules) whitelist := NewPortWhitelistIndex(ports) for index := range rules { - if rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule) { - rules[index].Protected = true - } + rules[index].Protected = rules[index].Protected || rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule) } protected := snapshot protected.Rules = rules @@ -71,19 +63,11 @@ func NewPortWhitelistIndex(ports []PortWhitelist) PortWhitelistIndex { return index } -func RuleMatchesPortWhitelist(rule FirewallRule, ports []PortWhitelist) bool { - return NewPortWhitelistIndex(ports).Matches(rule) -} - func (index PortWhitelistIndex) Matches(rule FirewallRule) bool { rule, err := NormalizeRule(rule) if err != nil || rule.Action != ActionAccept || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" || len(rule.ConnectionStates) != 0 { return false } - if rule.Scope.Provider == ProviderFirewalld && (rule.NativeKind == NativeKindZonePort || - (rule.NativeKind == NativeKindRule && rule.Scope.Family == FamilyInet && rule.Priority == nil)) { - return false - } families := []Family{rule.Scope.Family} if rule.Scope.Family == FamilyInet { families = []Family{FamilyIPv4, FamilyIPv6} @@ -144,118 +128,21 @@ func SameLocator(left, right Locator) bool { return left.Canonical != "" && left.Canonical == right.Canonical } -func MatchObservedByRuleKey(observed []ObservedRule, rule FirewallRule) ([]ObservedRule, error) { - wanted, err := RuleKey(rule) - if err != nil { - return nil, err +func LocateRule(snapshot RuleSet, locator *Locator) (ObservedRule, error) { + if locator == nil || locator.Provider != snapshot.Scope.Provider || locator.ScopeKey != snapshot.Scope.Key() { + return ObservedRule{}, ErrInvalidRule } - matches := make([]ObservedRule, 0, 1) - for _, candidate := range observed { - if candidate.ParseStatus != ParseStatusSupported { - continue + var matches []ObservedRule + for _, observed := range snapshot.Rules { + if SameLocator(observed.Locator, *locator) { + matches = append(matches, observed) } - candidateKey, keyErr := RuleKey(candidate.Rule) - if keyErr == nil && candidateKey == wanted { - matches = append(matches, candidate) - } - } - return matches, nil -} - -func FindCommittedObserved(snapshot RuleSet, requested FirewallRule, plan CommandBatch) (ObservedRule, error) { - if len(plan.Rules) == 1 && plan.Rules[0].Expected.Marker != "" { - matches := make([]ObservedRule, 0, 1) - for _, observed := range snapshot.Rules { - if observed.Marker == plan.Rules[0].Expected.Marker { - matches = append(matches, observed) - } - } - if len(matches) == 1 { - return matches[0], nil - } - } - matches, err := MatchObservedByRuleKey(snapshot.Rules, requested) - if err != nil { - return ObservedRule{}, err } if len(matches) != 1 { - return ObservedRule{}, fmt.Errorf("%w: expected one committed rule, found %d", ErrVerificationFailed, len(matches)) + return ObservedRule{}, ErrRuleStale + } + if err := GuardMutation(matches[0]); err != nil { + return ObservedRule{}, err } 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 (left.Scope.Family == FamilyInet || right.Scope.Family == FamilyInet || left.Scope.Family == right.Scope.Family) && - (left.Protocol == "all" || right.Protocol == "all" || 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 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 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/forwarding.go b/agent/utils/firewall/forwarding/forwarding.go index 4adf4ba80..b963bca53 100644 --- a/agent/utils/firewall/forwarding/forwarding.go +++ b/agent/utils/firewall/forwarding/forwarding.go @@ -49,10 +49,13 @@ type Adapter interface { CreateRules(context.Context, []Rule) error DeleteRules(context.Context, []Rule) error ReplaceRules(rules []Rule) error - Enable() error + Enable(...string) error + OperateFamily(string, bool) error + UnbindFamily(string) error Cleanup() error InitStatus() (bool, bool, error) FamilyStatus(family string) (bool, bool, error) + FamilyState(family string) (bool, bool, bool, error) Replay() error } diff --git a/agent/utils/firewall/forwarding/iptables.go b/agent/utils/firewall/forwarding/iptables.go index a6e3689b2..44b250835 100644 --- a/agent/utils/firewall/forwarding/iptables.go +++ b/agent/utils/firewall/forwarding/iptables.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strconv" "strings" "time" @@ -13,6 +14,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" firewallutil "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/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/mattn/go-shellwords" @@ -29,33 +31,35 @@ type iptablesBackend interface { LoadIPv6RulesFromFile(table, chain, fileName string) error } -type systemIptablesBackend struct{} +type systemIptablesBackend struct{ ctx context.Context } func (systemIptablesBackend) IPv6Available() bool { commands, err := lifecycle.ResolveIptablesCommands() return err == nil && commands.IPv6Available() } -func (systemIptablesBackend) Run(table string, args ...string) error { - return iptables_helper.Run(table, args...) +func (b systemIptablesBackend) Run(table string, args ...string) error { + _, err := iptables_helper.RunWithStdContext(b.ctx, table, args...) + return err } -func (systemIptablesBackend) RunWithStd(table string, args ...string) (string, error) { +func (b 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.ReadTable(b.ctx, table, false) } - return iptables_helper.RunWithStd(table, args...) + return iptables_helper.RunWithStdContext(b.ctx, table, args...) } -func (systemIptablesBackend) RunIPv6(table string, args ...string) error { - return iptables_helper.RunIPv6(table, args...) +func (b systemIptablesBackend) RunIPv6(table string, args ...string) error { + _, err := iptables_helper.RunIPv6WithStdContext(b.ctx, table, args...) + return err } -func (systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (string, error) { +func (b 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.ReadTable(b.ctx, table, true) } - return iptables_helper.RunIPv6WithStd(table, args...) + return iptables_helper.RunIPv6WithStdContext(b.ctx, table, args...) } func (systemIptablesBackend) Restore(ctx context.Context, family, input string) error { @@ -76,7 +80,7 @@ func (systemIptablesBackend) Restore(ctx context.Context, family, input string) if err == nil && strings.TrimSpace(stderr.String()) != "" { err = fmt.Errorf("firewall command warning: %s", strings.TrimSpace(stderr.String())) } - return firewallutil.WrapBatchCommandError(executable+" --noflush --wait", input, err) + return errors.Join(firewallutil.WrapBatchCommandError(executable+" --noflush --wait", input, err), ctx.Err()) } func (systemIptablesBackend) LoadRulesFromFile(table, chain, fileName string) error { @@ -103,15 +107,17 @@ func (defaultForwardingSystem) WriteFile(name string, data []byte, perm os.FileM } type Iptables struct { + ctx context.Context provider string backend iptablesBackend system forwardingSystem } -func NewIptables(provider string) *Iptables { +func NewIptables(ctx context.Context, provider string) *Iptables { return &Iptables{ + ctx: ctx, provider: provider, - backend: systemIptablesBackend{}, + backend: systemIptablesBackend{ctx: ctx}, system: defaultForwardingSystem{}, } } @@ -130,6 +136,9 @@ func (l *Iptables) List() ([]Rule, error) { return rules, nil } stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-S") + if errors.Is(err, filter.ErrFamilyUnavailable) { + return rules, nil + } if err != nil { return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err) } @@ -156,31 +165,25 @@ func (l *Iptables) ReplaceRules(rules []Rule) error { } continue } - if family == FamilyIPv6 { - if len(byFamily[family]) > 0 { - if err := ensureForwardingSysctls(l.system, true); err != nil { - failures = append(failures, err) - continue - } - } else { - initialized, _, err := l.familyInitStatus(family) - if err != nil { - failures = append(failures, err) - continue - } - if !initialized { - continue - } - } + initialized, _, err := l.FamilyStatus(family) + if family == FamilyIPv6 && len(byFamily[family]) == 0 && errors.Is(err, filter.ErrFamilyUnavailable) { + continue } - if err := l.batchEnsureChains(family); err != nil { - return err + if err != nil { + failures = append(failures, err) + continue + } + if !initialized { + if len(byFamily[family]) > 0 { + failures = append(failures, fmt.Errorf("%s forwarding chains are not initialized", family)) + } + continue } script, err := buildIptablesForwardScript(byFamily[family], OperationAdd, true) if err != nil { return err } - if err := l.backend.Restore(context.Background(), family, script); err != nil { + if err := l.backend.Restore(l.ctx, family, script); err != nil { return fmt.Errorf("restore %s forwarding rules: %w", family, err) } } @@ -199,17 +202,6 @@ func (l *Iptables) CreateRules(ctx context.Context, rules []Rule) error { if family == "" { family = FamilyIPv4 } - if family == FamilyIPv6 { - if !l.backend.IPv6Available() { - return fmt.Errorf("ip6tables command family is unavailable") - } - if err := ensureForwardingSysctls(l.system, true); err != nil { - return err - } - if err := l.batchEnsureChains(family); err != nil { - return err - } - } return l.backend.Restore(ctx, family, script) } @@ -310,14 +302,21 @@ func isRemoteTarget(family, target string) bool { return target != "" && target != "127.0.0.1" && target != "localhost" } -func (l *Iptables) Enable() error { - if err := ensureForwardingSysctls(l.system, false); err != nil { +func (l *Iptables) Enable(families ...string) error { + if err := l.OperateFamily(FamilyIPv4, true); err != nil { return err } - return l.batchEnsureChains(FamilyIPv4) + if len(families) > 0 && !slices.Contains(families, FamilyIPv6) { + return nil + } + initialized, _, err := l.FamilyStatus(FamilyIPv6) + if err != nil || !initialized { + return err + } + return l.OperateFamily(FamilyIPv6, false) } -func (l *Iptables) batchEnsureChains(family string) error { +func (l *Iptables) batchEnsureChains(family string, initialize bool) error { list := l.backend.RunWithStd if family == FamilyIPv6 { list = l.backend.RunIPv6WithStd @@ -330,11 +329,16 @@ func (l *Iptables) batchEnsureChains(family string) error { } outputs[table] = output } + if !initialize && (!containsExactLine(outputs[iptables_helper.NatTab], "-N "+ChainPreRouting) || + !containsExactLine(outputs[iptables_helper.NatTab], "-N "+ChainPostRouting) || + !containsExactLine(outputs[iptables_helper.FilterTab], "-N "+ChainForward)) { + return fmt.Errorf("%s forwarding chains are not initialized", family) + } script := buildIptablesForwardLifecycleScript(outputs, true) if script == "" { return nil } - if err := l.backend.Restore(context.Background(), family, script); err != nil { + if err := l.backend.Restore(l.ctx, family, script); err != nil { return fmt.Errorf("batch initialize %s forwarding chains: %w", family, err) } return nil @@ -359,7 +363,7 @@ func (l *Iptables) Cleanup() error { } script := buildIptablesForwardLifecycleScript(outputs, false) if script != "" { - if err := l.backend.Restore(context.Background(), family, script); err != nil { + if err := l.backend.Restore(l.ctx, family, script); err != nil { return fmt.Errorf("batch delete %s forwarding chains: %w", family, err) } } @@ -518,6 +522,9 @@ func (l *Iptables) InitStatus() (bool, bool, error) { return ipv4Init, ipv4Bind, nil } ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6) + if errors.Is(err, filter.ErrFamilyUnavailable) { + return ipv4Init, ipv4Bind, nil + } if err != nil { return false, false, err } @@ -525,6 +532,14 @@ func (l *Iptables) InitStatus() (bool, bool, error) { } func (l *Iptables) familyInitStatus(family string) (bool, bool, error) { + initialized, bound, _, err := l.FamilyState(family) + return initialized, bound, err +} + +func (l *Iptables) FamilyState(family string) (bool, bool, bool, error) { + if family == FamilyIPv6 && !l.backend.IPv6Available() { + return false, false, false, nil + } sysctlPath := "/proc/sys/net/ipv4/ip_forward" label := "IPv4" list := l.backend.RunWithStd @@ -535,27 +550,24 @@ func (l *Iptables) familyInitStatus(family string) (bool, bool, error) { } data, err := l.system.ReadFile(sysctlPath) if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) { - return false, false, nil + return false, false, false, nil } if err != nil { - return false, false, fmt.Errorf("read %s forwarding status: %w", label, err) + return false, false, false, fmt.Errorf("read %s forwarding status: %w", label, err) } forwardingEnabled := strings.TrimSpace(string(data)) != "0" natRules, err := list(iptables_helper.NatTab, "-S") if err != nil { - return false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err) + return false, false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err) } natInit, natBind := checkInitAndBind( []string{"-N " + ChainPreRouting, "-N " + ChainPostRouting}, []string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting}, strings.Split(natRules, "\n"), ) - if !natInit { - return false, false, nil - } filterRules, err := list(iptables_helper.FilterTab, "-S") if err != nil { - return false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err) + return false, false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err) } filterInit, _ := checkInitAndBind( []string{"-N " + ChainForward}, @@ -563,7 +575,9 @@ func (l *Iptables) familyInitStatus(family string) (bool, bool, error) { strings.Split(filterRules, "\n"), ) filterBind := forwardBindingEffective(filterRules) - return natInit && filterInit, forwardingEnabled && natBind && filterInit && filterBind, nil + initialized := natInit && filterInit + present := containsExactLine(natRules, "-N "+ChainPreRouting) || containsExactLine(natRules, "-N "+ChainPostRouting) || containsExactLine(filterRules, "-N "+ChainForward) + return initialized, forwardingEnabled && natBind && filterInit && filterBind, present && !initialized, nil } func (l *Iptables) FamilyStatus(family string) (bool, bool, error) { @@ -601,7 +615,7 @@ func (l *Iptables) Replay() error { if family == FamilyIPv6 && !l.backend.IPv6Available() { continue } - if err := l.batchEnsureChains(family); err != nil { + if err := l.batchEnsureChains(family, true); err != nil { return err } } @@ -726,3 +740,59 @@ func loadProtocol(protocol string) string { return protocol } } + +func (l *Iptables) OperateFamily(family string, initialize bool) error { + if family != FamilyIPv4 && family != FamilyIPv6 { + return fmt.Errorf("unsupported forwarding family %q", family) + } + if family == FamilyIPv6 && !l.backend.IPv6Available() { + return fmt.Errorf("ip6tables command family is unavailable") + } + if !initialize { + initialized, _, err := l.FamilyStatus(family) + if err != nil { + return err + } + if !initialized { + return fmt.Errorf("%s forwarding chains are not initialized", family) + } + } + if err := ensureFamilyForwardingSysctls(l.system, family); err != nil { + return err + } + return l.batchEnsureChains(family, initialize) +} + +func (l *Iptables) UnbindFamily(family string) error { + if family != FamilyIPv4 && family != FamilyIPv6 { + return fmt.Errorf("unsupported forwarding family %q", family) + } + if family == FamilyIPv6 && !l.backend.IPv6Available() { + return nil + } + read := l.backend.RunWithStd + if family == FamilyIPv6 { + read = l.backend.RunIPv6WithStd + } + var script strings.Builder + for _, table := range []string{iptables_helper.NatTab, iptables_helper.FilterTab} { + output, err := read(table, "-S") + if err != nil { + return err + } + var removals []string + for _, pair := range [][2]string{{"PREROUTING", ChainPreRouting}, {"POSTROUTING", ChainPostRouting}, {"FORWARD", ChainForward}} { + rule := "-A " + pair[0] + " -j " + pair[1] + for range countExactLines(output, rule) { + removals = append(removals, "-D "+pair[0]+" -j "+pair[1]) + } + } + if len(removals) > 0 { + script.WriteString("*" + table + "\n" + strings.Join(removals, "\n") + "\nCOMMIT\n") + } + } + if script.Len() == 0 { + return nil + } + return l.backend.Restore(l.ctx, family, script.String()) +} diff --git a/agent/utils/firewall/forwarding/nftables.go b/agent/utils/firewall/forwarding/nftables.go index bb0bb900d..23c0c344d 100644 --- a/agent/utils/firewall/forwarding/nftables.go +++ b/agent/utils/firewall/forwarding/nftables.go @@ -2,11 +2,11 @@ package forwarding import ( "context" - "encoding/base64" "errors" "fmt" "os" "path/filepath" + "slices" "strconv" "strings" "time" @@ -14,6 +14,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" firewallutil "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/nftables_helper" ) @@ -24,10 +25,13 @@ const ( nftForwardMarker = "1panel-forward:" ) -type Nftables struct{ system forwardingSystem } +type Nftables struct { + ctx context.Context + system forwardingSystem +} -func NewNftables() *Nftables { - return &Nftables{system: defaultForwardingSystem{}} +func NewNftables(ctx context.Context) *Nftables { + return &Nftables{ctx: ctx, system: defaultForwardingSystem{}} } func (n *Nftables) Name() string { return "nftables" } @@ -35,8 +39,8 @@ func (n *Nftables) Name() string { return "nftables" } func (n *Nftables) List() ([]Rule, error) { rules := make([]Rule, 0) for _, family := range []string{FamilyIPv4, FamilyIPv6} { - stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, "NFT_"+ChainPreRouting) - if errors.Is(err, nftables_helper.ErrChainNotFound) { + stdout, err := nftables_helper.ReadChain(n.run, nftTableFamily(family), nftForwardTable, "NFT_"+ChainPreRouting) + if errors.Is(err, nftables_helper.ErrChainNotFound) || (family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable)) { continue } if err != nil { @@ -58,31 +62,25 @@ func (n *Nftables) ReplaceRules(rules []Rule) error { } var failures []error for _, family := range []string{FamilyIPv4, FamilyIPv6} { - if family == FamilyIPv6 { - if len(byFamily[family]) > 0 { - if err := ensureForwardingSysctls(n.system, true); err != nil { - failures = append(failures, err) - continue - } - } else { - _, exists, err := nftables_helper.ReadTable(nftRun, nftTableFamily(family), nftForwardTable) - if err != nil { - failures = append(failures, err) - continue - } - if !exists { - continue - } - } + initialized, _, err := n.FamilyStatus(family) + if family == FamilyIPv6 && len(byFamily[family]) == 0 && errors.Is(err, filter.ErrFamilyUnavailable) { + continue } - if err := ensureNftForwardTables(family); err != nil { - return fmt.Errorf("initialize nftables forwarding table: %w", err) + if err != nil { + failures = append(failures, err) + continue + } + if !initialized { + if len(byFamily[family]) > 0 { + failures = append(failures, fmt.Errorf("%s forwarding chains are not initialized", family)) + } + continue } commands, err := rebuildNftForwardCommands(byFamily[family], family) if err != nil { return err } - if err := nftRunCommands(context.Background(), commands); err != nil { + if err := nftRunCommands(n.ctx, commands); err != nil { return err } } @@ -97,17 +95,6 @@ func (n *Nftables) CreateRules(ctx context.Context, rules []Rule) error { if err != nil { return err } - for _, rule := range rules { - if strings.EqualFold(strings.TrimSpace(rule.Family), FamilyIPv6) { - if err := ensureForwardingSysctls(n.system, true); err != nil { - return err - } - if err := ensureNftForwardTables(FamilyIPv6); err != nil { - return err - } - break - } - } return nftRunCommands(ctx, commands) } @@ -154,27 +141,35 @@ func (n *Nftables) DeleteRules(ctx context.Context, rules []Rule) error { return nftRunCommands(ctx, commands) } -func (n *Nftables) Enable() error { - if err := ensureForwardingSysctls(n.system, false); err != nil { +func (n *Nftables) Enable(families ...string) error { + if err := n.OperateFamily(FamilyIPv4, true); err != nil { return err } - if err := ensureNftForwardTables(FamilyIPv4); err != nil { - return fmt.Errorf("initialize nftables forwarding table: %w", err) + if len(families) > 0 && !slices.Contains(families, FamilyIPv6) { + return nil } - return nil + initialized, _, err := n.FamilyStatus(FamilyIPv6) + if err != nil || !initialized { + return err + } + return n.OperateFamily(FamilyIPv6, false) } func (n *Nftables) Cleanup() error { commands := make([][]string, 0, 2) for _, family := range []string{FamilyIPv4, FamilyIPv6} { tableFamily := nftTableFamily(family) - if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil { + _, exists, err := nftables_helper.ReadTable(n.run, tableFamily, nftForwardTable) + if err != nil { + return err + } + if !exists { continue } commands = append(commands, []string{"delete", "table", tableFamily, nftForwardTable}) } if len(commands) > 0 { - if err := nftRunCommands(context.Background(), commands); err != nil { + if err := nftRunCommands(n.ctx, commands); err != nil { return err } } @@ -189,6 +184,9 @@ func (n *Nftables) InitStatus() (bool, bool, error) { var anyInitialized, anyBound bool for _, family := range []string{FamilyIPv4, FamilyIPv6} { initialized, bound, err := n.FamilyStatus(family) + if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { + continue + } if err != nil { return false, false, err } @@ -199,31 +197,37 @@ func (n *Nftables) InitStatus() (bool, bool, error) { } func (n *Nftables) FamilyStatus(family string) (bool, bool, error) { + initialized, bound, _, err := n.FamilyState(family) + return initialized, bound, err +} + +func (n *Nftables) FamilyState(family string) (bool, bool, bool, error) { sysctlPath := "/proc/sys/net/ipv4/ip_forward" if family == FamilyIPv6 { sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding" } data, err := n.system.ReadFile(sysctlPath) if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) { - return false, false, nil + return false, false, false, nil } if err != nil { - return false, false, fmt.Errorf("read %s forwarding status: %w", family, err) + return false, false, false, fmt.Errorf("read %s forwarding status: %w", family, err) } - output, exists, err := nftables_helper.ReadTable(nftRun, nftTableFamily(family), nftForwardTable) + output, exists, err := nftables_helper.ReadTable(n.run, nftTableFamily(family), nftForwardTable) if err != nil { - return false, false, err + return false, false, false, err } if !exists { - return false, false, nil + return false, false, false, nil } chains := nftables_helper.ParseTableChains(output) + count := 0 for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} { - if _, exists := chains["NFT_"+chain]; !exists { - return false, false, nil + if _, exists := chains["NFT_"+chain]; exists { + count++ } } - return true, strings.TrimSpace(string(data)) != "0", nil + return count == 3, count == 3 && !nftables_helper.TableDormant(output) && strings.TrimSpace(string(data)) != "0", count > 0 && count < 3, nil } func (n *Nftables) Replay() error { @@ -235,21 +239,21 @@ func (n *Nftables) Replay() error { } allPresent := true for _, family := range []string{FamilyIPv4, FamilyIPv6} { - if _, err := nftRun("list", "table", nftTableFamily(family), nftForwardTable); err != nil { + if _, err := n.run("list", "table", nftTableFamily(family), nftForwardTable); err != nil { allPresent = false } } if allPresent { return nil } - return nftRunCommand("-f", file) + return n.runCommand("-f", file) } -func ensureNftForwardTables(families ...string) error { +func (n *Nftables) ensureTables(families ...string) error { commands := make([][]string, 0, 8) for _, family := range families { tableFamily := nftTableFamily(family) - output, tableExists, err := nftables_helper.ReadTable(nftRun, tableFamily, nftForwardTable) + output, tableExists, err := nftables_helper.ReadTable(n.run, tableFamily, nftForwardTable) if err != nil { return err } @@ -277,7 +281,7 @@ func ensureNftForwardTables(families ...string) error { if len(commands) == 0 { return nil } - return nftRunCommands(context.Background(), commands) + return nftRunCommands(n.ctx, commands) } func rebuildNftForwardCommands(rules []Rule, family string) ([][]string, error) { @@ -350,13 +354,6 @@ func decodeNftForwardRule(value string) (Rule, bool) { return Rule{}, false } value = strings.TrimPrefix(value, nftForwardMarker) - if strings.HasPrefix(value, "v2|") { - return decodeCompactNftForwardRule(value) - } - return decodeLegacyNftForwardRule(value) -} - -func decodeCompactNftForwardRule(value string) (Rule, bool) { parts := strings.Split(value, "|") if len(parts) != 7 || parts[0] != "v2" { return Rule{}, false @@ -383,22 +380,6 @@ func decodeCompactNftForwardRule(value string) (Rule, bool) { }, true } -func decodeLegacyNftForwardRule(value string) (Rule, bool) { - parts := strings.Split(value, ".") - if len(parts) != 6 { - return Rule{}, false - } - decoded := make([]string, len(parts)) - for index, part := range parts { - data, err := base64.RawURLEncoding.DecodeString(part) - if err != nil { - return Rule{}, false - } - decoded[index] = string(data) - } - return Rule{Family: decoded[0], Protocol: decoded[1], Port: decoded[2], TargetIP: decoded[3], TargetPort: decoded[4], Interface: decoded[5]}, true -} - func parseNftForwardRules(stdout string) []Rule { result := make([]Rule, 0) for _, line := range strings.Split(stdout, "\n") { @@ -422,16 +403,17 @@ func parseNftForwardRules(stdout string) []Rule { return result } -func nftRun(args ...string) (string, error) { - stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...) +func (n *Nftables) run(args ...string) (string, error) { + stdout, err := cmd.NewCommandMgr(cmd.WithContext(n.ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...) + err = errors.Join(err, n.ctx.Err()) if err != nil { return stdout, fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err) } return stdout, nil } -func nftRunCommand(args ...string) error { - err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudo("nft", args...) +func (n *Nftables) runCommand(args ...string) error { + err := cmd.NewCommandMgr(cmd.WithContext(n.ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudo("nft", args...) if err != nil { return fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err) } @@ -448,7 +430,7 @@ func nftRunCommands(ctx context.Context, commands [][]string) error { if err == nil && strings.TrimSpace(stderr.String()) != "" { err = fmt.Errorf("firewall command warning: %s", strings.TrimSpace(stderr.String())) } - return firewallutil.WrapBatchCommandError("nft -f -", script, err) + return errors.Join(firewallutil.WrapBatchCommandError("nft -f -", script, err), ctx.Err()) } func nftCommandsScript(commands [][]string) (string, error) { @@ -467,3 +449,34 @@ func nftCommandsScript(commands [][]string) (string, error) { } return script.String(), nil } + +func (n *Nftables) OperateFamily(family string, initialize bool) error { + if family != FamilyIPv4 && family != FamilyIPv6 { + return fmt.Errorf("unsupported forwarding family %q", family) + } + if !initialize { + initialized, _, err := n.FamilyStatus(family) + if err != nil { + return err + } + if !initialized { + return fmt.Errorf("%s forwarding chains are not initialized", family) + } + } + if err := ensureFamilyForwardingSysctls(n.system, family); err != nil { + return err + } + if initialize { + if err := n.ensureTables(family); err != nil { + return err + } + } + return nftRunCommands(n.ctx, [][]string{{"add", "table", nftTableFamily(family), nftForwardTable}}) +} + +func (n *Nftables) UnbindFamily(family string) error { + if family != FamilyIPv4 && family != FamilyIPv6 { + return fmt.Errorf("unsupported forwarding family %q", family) + } + return nftables_helper.SetTableDormant(n.ctx, nftTableFamily(family), nftForwardTable) +} diff --git a/agent/utils/firewall/forwarding/sysctl.go b/agent/utils/firewall/forwarding/sysctl.go index 4e294be19..4f7a4d69a 100644 --- a/agent/utils/firewall/forwarding/sysctl.go +++ b/agent/utils/firewall/forwarding/sysctl.go @@ -12,7 +12,8 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" ) -func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error { +func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error { + withIPv6 := family == FamilyIPv6 if withIPv6 { interfaces, err := IPv6RAInterfaces(system.ReadFile) if err != nil { @@ -24,7 +25,7 @@ func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error { } paths := []string{"/proc/sys/net/ipv4/ip_forward"} if withIPv6 { - paths = append(paths, "/proc/sys/net/ipv6/conf/all/forwarding") + paths = []string{"/proc/sys/net/ipv6/conf/all/forwarding"} } for _, path := range paths { if err := system.WriteFile(path, []byte("1"), constant.FilePerm); err != nil { @@ -35,7 +36,7 @@ func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error { if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to read /etc/sysctl.conf: %w", err) } - content := enableForwardingSysctls(string(data), withIPv6) + content := enableFamilyForwardingSysctls(string(data), family) if err := system.WriteFile("/etc/sysctl.conf", []byte(content), constant.FilePerm); err != nil { return fmt.Errorf("failed to persist IP forwarding: %w", err) } @@ -126,11 +127,11 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) { return interfaces, nil } -func enableForwardingSysctls(content string, withIPv6 bool) string { +func enableFamilyForwardingSysctls(content, family string) string { lines := strings.Split(strings.TrimRight(content, "\n"), "\n") wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"} - if withIPv6 { - wanted["net.ipv6.conf.all.forwarding"] = "net.ipv6.conf.all.forwarding = 1" + if family == FamilyIPv6 { + wanted = map[string]string{"net.ipv6.conf.all.forwarding": "net.ipv6.conf.all.forwarding = 1"} } found := make(map[string]bool, len(wanted)) for index, line := range lines { diff --git a/agent/utils/firewall/iptables_helper/inspect.go b/agent/utils/firewall/iptables_helper/inspect.go index b986d190a..6b2fe5523 100644 --- a/agent/utils/firewall/iptables_helper/inspect.go +++ b/agent/utils/firewall/iptables_helper/inspect.go @@ -9,18 +9,39 @@ import ( ) func LoadInitStatus(tab string) (bool, bool, error) { - return loadInitStatus(tab, RunWithStd, true) + return loadInitStatus(tab, RunWithStd) } func LoadFamilyInitStatus(family, tab string) (bool, bool, error) { + initialized, bound, _, err := LoadFamilyState(family, tab) + return initialized, bound, err +} + +func LoadFamilyState(family, tab string) (bool, bool, bool, error) { + runner := RunWithStd switch family { case constant.FirewallFamilyIPv4: - return loadInitStatus(tab, RunWithStd, true) case constant.FirewallFamilyIPv6: - return loadInitStatus(tab, RunIPv6WithStd, true) + runner = RunIPv6WithStd default: - return false, false, fmt.Errorf("unsupported iptables family %q", family) + return false, false, false, fmt.Errorf("unsupported iptables family %q", family) } + if tab != "base" { + return false, false, false, nil + } + output, err := runner(FilterTab, "-S") + if err != nil { + return false, false, false, err + } + count := 0 + for _, chain := range BasicChains() { + if containsIptablesRule(output, "-N "+chain) { + count++ + } + } + initialized := count == len(BasicChains()) + _, bound := checkWithInitAndBind([]string{"-N " + BasicBeforeChain, "-N " + BasicChain, "-N " + BasicAfterChain}, []string{"-A INPUT -j " + BasicBeforeChain, "-A INPUT -j " + BasicChain, "-A INPUT -j " + BasicAfterChain}, strings.Split(output, "\n")) + return initialized, bound, count > 0 && !initialized, nil } func LoadFamilyBindStatus(family string) (bool, error) { @@ -54,7 +75,7 @@ func hasBaseChainBinding(output string) bool { return false } -func loadInitStatus(tab string, runner func(string, ...string) (string, error), requireTerminalRules bool) (bool, bool, error) { +func loadInitStatus(tab string, runner func(string, ...string) (string, error)) (bool, bool, error) { switch tab { case "base": filterRules, err := runner(FilterTab, "-S") @@ -66,14 +87,6 @@ func loadInitStatus(tab string, runner func(string, ...string) (string, error), "-N " + BasicBeforeChain, "-N " + BasicChain, "-N " + BasicAfterChain, - fmt.Sprintf("-A %s %s -j ACCEPT", BasicBeforeChain, strings.ReplaceAll(strings.ReplaceAll(IoRuleIn, "'", "\""), " -j ACCEPT", "")), - fmt.Sprintf("-A %s %s -j ACCEPT", BasicBeforeChain, strings.ReplaceAll(strings.ReplaceAll(EstablishedRule, "'", "\""), " -j ACCEPT", "")), - } - if requireTerminalRules { - initRules = append(initRules, - fmt.Sprintf("-A %s %s", BasicAfterChain, DropAllTcp), - fmt.Sprintf("-A %s %s", BasicAfterChain, DropAllUdp), - ) } bindRules := []string{ fmt.Sprintf("-A %s -j %s", InputChain, BasicBeforeChain), @@ -88,15 +101,9 @@ func loadInitStatus(tab string, runner func(string, ...string) (string, error), } func checkWithInitAndBind(initRules, bindRules []string, lines []string) (bool, bool) { + output := strings.Join(lines, "\n") for _, rule := range initRules { - found := false - for _, line := range lines { - if strings.TrimSpace(line) == strings.TrimSpace(rule) { - found = true - break - } - } - if !found { + if !containsIptablesRule(output, rule) { if global.LOG != nil { global.LOG.Debugf("not found init rule: %s", rule) } diff --git a/agent/utils/firewall/iptables_helper/ipv6.go b/agent/utils/firewall/iptables_helper/ipv6.go index 908163f9d..2339ffb1e 100644 --- a/agent/utils/firewall/iptables_helper/ipv6.go +++ b/agent/utils/firewall/iptables_helper/ipv6.go @@ -52,8 +52,9 @@ func BindIPv6BaseChains() error { } func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelist, output string) (string, error) { + existing := iptablesRuleIndex(output) for _, chain := range BasicChains() { - if !containsIptablesRule(output, "-N "+chain) { + if !existing[canonicalIptablesRule("-N "+chain)] { return buildBaseChainsRestoreScript(dir, true, ports...) } } @@ -64,7 +65,9 @@ func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelis var script strings.Builder script.WriteString("*filter\n") for _, rule := range defaults { - if !containsIptablesRule(output, rule) { + key := canonicalIptablesRule(rule) + if !existing[key] { + existing[key] = true script.WriteString(rule + "\n") } } diff --git a/agent/utils/firewall/iptables_helper/legacy.go b/agent/utils/firewall/iptables_helper/legacy.go deleted file mode 100644 index 15c516fa5..000000000 --- a/agent/utils/firewall/iptables_helper/legacy.go +++ /dev/null @@ -1,84 +0,0 @@ -package iptables_helper - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" - "github.com/mattn/go-shellwords" -) - -const ( - LegacyInputChain = "1PANEL_INPUT" - LegacyOutputChain = "1PANEL_OUTPUT" - legacyInputFileName = "1panel_input.rules" - legacyOutputFileName = "1panel_out.rules" -) - -func CleanupLegacyAdvancedChains(ctx context.Context) error { - commands, err := lifecycle.ResolveIptablesCommands() - if err != nil { - return err - } - output, err := RunWithStdContext(ctx, FilterTab, "-S") - if err != nil { - return fmt.Errorf("inspect legacy iptables advanced chains: %w", err) - } - if script := buildLegacyAdvancedChainCleanupScript(output); script != "" { - if err := restoreRules(commands.Restore4, script); err != nil { - return fmt.Errorf("remove legacy iptables advanced chains: %w", err) - } - } - for _, name := range []string{legacyInputFileName, legacyOutputFileName} { - file := filepath.Join(global.Dir.FirewallDir, name) - if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("remove legacy iptables rules file %s: %w", file, err) - } - } - return nil -} - -func buildLegacyAdvancedChainCleanupScript(output string) string { - legacyChains := map[string]struct{}{LegacyInputChain: {}, LegacyOutputChain: {}} - existing := make(map[string]bool, len(legacyChains)) - deletions := make([]string, 0) - for _, raw := range strings.Split(output, "\n") { - line := strings.TrimSpace(raw) - fields, err := shellwords.Parse(line) - if err != nil { - continue - } - if len(fields) == 2 && fields[0] == "-N" { - if _, legacy := legacyChains[fields[1]]; legacy { - existing[fields[1]] = true - } - continue - } - if len(fields) < 4 || fields[0] != "-A" { - continue - } - for index := 2; index+1 < len(fields); index++ { - if fields[index] != "-j" && fields[index] != "-g" { - continue - } - if _, legacy := legacyChains[fields[index+1]]; legacy { - deletions = append(deletions, strings.Replace(line, "-A ", "-D ", 1)) - } - break - } - } - for _, chain := range []string{LegacyInputChain, LegacyOutputChain} { - if existing[chain] { - deletions = append(deletions, "-F "+chain, "-X "+chain) - } - } - if len(deletions) == 0 { - return "" - } - return "*filter\n" + strings.Join(deletions, "\n") + "\nCOMMIT\n" -} diff --git a/agent/utils/firewall/iptables_helper/manager.go b/agent/utils/firewall/iptables_helper/manager.go index c5b4d0590..13746aa40 100644 --- a/agent/utils/firewall/iptables_helper/manager.go +++ b/agent/utils/firewall/iptables_helper/manager.go @@ -6,6 +6,7 @@ import ( "net/netip" "os" "path/filepath" + "slices" "sort" "strings" @@ -253,7 +254,7 @@ func saveBaseChainsFamily(ipv6 bool) error { return nil } -func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error { +func RestoreBaseChains(requiredPorts []firewall.PortWhitelist, families ...string) error { commands, err := lifecycle.ResolveIptablesCommands() if err != nil { return err @@ -268,7 +269,7 @@ func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error { if err := restoreRules(commands.Restore4, input); err != nil { return fmt.Errorf("batch restore IPv4 base chains: %w", err) } - if !commands.IPv6Available() { + if !commands.IPv6Available() || (len(families) > 0 && !slices.Contains(families, constant.FirewallFamilyIPv6)) { return nil } if err := ensureBaseChainsFamily(true); err != nil { @@ -285,6 +286,7 @@ func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error { } func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) { + existing := make(map[string]bool) var script strings.Builder script.WriteString("*filter\n") for _, chain := range BasicChains() { @@ -314,6 +316,7 @@ func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts . if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") { continue } + existing[canonicalIptablesRule(line)] = true script.WriteString(line) script.WriteByte('\n') } @@ -327,7 +330,9 @@ func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts . return "", err } for _, rule := range defaults { - if !containsIptablesRule(script.String(), rule) { + key := canonicalIptablesRule(rule) + if !existing[key] { + existing[key] = true if strings.HasPrefix(rule, "-A "+BasicBeforeChain+" ") && strings.Contains(rule, " --dport ") { rule = strings.Replace(rule, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1) } @@ -419,23 +424,29 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel } func buildRequiredPortsRestoreScript(desired []firewall.SystemPort, family string, beforeRaw, afterRaw string, includeDefaults bool) string { + existing := iptablesRuleIndex(beforeRaw + "\n" + afterRaw) var commands []string for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} { - if !containsIptablesRule(beforeRaw, line) { + key := canonicalIptablesRule(line) + if !existing[key] { + existing[key] = true commands = append(commands, line) } } for _, rule := range desired { line := iptablesSystemPortRuleLine(rule) - if rule.Family == family && !containsIptablesRule(beforeRaw, line) { + key := canonicalIptablesRule(line) + if rule.Family == family && !existing[key] { + existing[key] = true commands = append(commands, strings.Replace(line, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1)) - beforeRaw += "\n" + line } } if includeDefaults { for _, rule := range []string{DropAllTcp, DropAllUdp} { line := "-A " + BasicAfterChain + " " + rule - if !containsIptablesRule(afterRaw, line) { + key := canonicalIptablesRule(line) + if !existing[key] { + existing[key] = true commands = append(commands, line) } } @@ -459,35 +470,101 @@ func containsIptablesRule(output, rule string) bool { } func countIptablesRule(output, rule string) int { - canonical := func(value string) string { - fields, err := shellwords.Parse(value) - if err != nil || len(fields)%2 != 0 { - return strings.TrimSpace(value) - } - var options []string - for index := 0; index < len(fields); index += 2 { - key, value := fields[index], fields[index+1] - if key == "--comment" || key == "-m" && (value == "comment" || value == "tcp" || value == "udp") { - continue - } - if prefix, err := netip.ParsePrefix(value); err == nil { - prefix = prefix.Masked() - value = prefix.String() - if prefix.Bits() == prefix.Addr().BitLen() { - value = prefix.Addr().String() - } - } - options = append(options, key+" "+value) - } - sort.Strings(options) - return strings.Join(options, " ") - } - rule = canonical(rule) + rule = canonicalIptablesRule(rule) count := 0 for _, line := range strings.Split(output, "\n") { - if canonical(line) == rule { + if canonicalIptablesRule(line) == rule { count++ } } return count } + +func canonicalIptablesRule(value string) string { + fields, err := shellwords.Parse(value) + if err != nil || len(fields)%2 != 0 { + return strings.TrimSpace(value) + } + var options []string + for index := 0; index < len(fields); index += 2 { + key, value := fields[index], fields[index+1] + if key == "--comment" || key == "-m" && (value == "comment" || value == "tcp" || value == "udp") { + continue + } + if key == "-m" && value == "state" { + value = "conntrack" + } + if key == "--state" || key == "--ctstate" { + key = "--ctstate" + states := strings.Split(strings.ToUpper(value), ",") + sort.Strings(states) + value = strings.Join(states, ",") + } + if prefix, err := netip.ParsePrefix(value); err == nil { + prefix = prefix.Masked() + value = prefix.String() + if prefix.Bits() == prefix.Addr().BitLen() { + value = prefix.Addr().String() + } + } + options = append(options, key+" "+value) + } + sort.Strings(options) + return strings.Join(options, " ") +} + +func iptablesRuleIndex(output string) map[string]bool { + rules := make(map[string]bool) + for _, line := range strings.Split(output, "\n") { + rules[canonicalIptablesRule(line)] = true + } + return rules +} + +func OperateFamily(family string, initialize bool, ports []firewall.PortWhitelist) error { + if family != constant.FirewallFamilyIPv4 && family != constant.FirewallFamilyIPv6 { + return fmt.Errorf("unsupported iptables family %q", family) + } + commands, err := lifecycle.ResolveIptablesCommands() + if err != nil { + return err + } + ipv6 := family == constant.FirewallFamilyIPv6 + read, executable := RunWithStd, commands.Restore4 + if ipv6 { + if !commands.IPv6Available() { + return fmt.Errorf("ip6tables and ip6tables-restore are required") + } + read, executable = RunIPv6WithStd, commands.Restore6 + } + output, err := read(FilterTab, "-S") + if err != nil { + return err + } + lines := make([]string, 0) + for _, chain := range BasicChains() { + if !containsIptablesRule(output, "-N "+chain) { + if !initialize { + return fmt.Errorf("%s chain %s is not initialized", family, chain) + } + lines = append(lines, "-N "+chain) + } + } + if initialize { + defaults, err := baseDefaultRules(ports, family) + if err != nil { + return err + } + existing := iptablesRuleIndex(output) + for _, rule := range defaults { + if !existing[canonicalIptablesRule(rule)] { + lines = append(lines, rule) + } + } + } + lines = append(lines, baseChainBindingCommands(output, true)...) + if err := restoreRules(executable, "*filter\n"+strings.Join(lines, "\n")+"\nCOMMIT\n"); err != nil { + return err + } + return saveBaseChainsFamily(ipv6) +} diff --git a/agent/utils/firewall/iptables_helper/repair.go b/agent/utils/firewall/iptables_helper/repair.go index 676eaa936..79a1f0aaa 100644 --- a/agent/utils/firewall/iptables_helper/repair.go +++ b/agent/utils/firewall/iptables_helper/repair.go @@ -14,12 +14,15 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" ) -func RepairBaseChains(ports []firewall.PortWhitelist) error { +func RepairBaseChains(ports []firewall.PortWhitelist, families ...string) error { + if len(families) == 0 { + families = []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} + } commands, err := lifecycle.ResolveIptablesCommands() if err != nil { return err } - for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} { + for _, family := range families { ipv6 := family == constant.FirewallFamilyIPv6 run, executable := RunWithStd, commands.Restore4 if ipv6 { diff --git a/agent/utils/firewall/nftables_helper/manager.go b/agent/utils/firewall/nftables_helper/manager.go index 9a109e7b5..b5fff5d80 100644 --- a/agent/utils/firewall/nftables_helper/manager.go +++ b/agent/utils/firewall/nftables_helper/manager.go @@ -21,7 +21,11 @@ func Cleanup() error { commands := make([][]string, 0, 2) for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { tableFamily := TableFamily(family) - if _, err := run("list", "table", tableFamily, TableName); err != nil { + _, exists, err := ReadTable(run, tableFamily, TableName) + if err != nil { + return err + } + if !exists { continue } commands = append(commands, []string{"delete", "table", tableFamily, TableName}) @@ -64,9 +68,12 @@ func enableBase(prepare bool, requiredPorts []firewall.PortWhitelist) error { return nil } -func ensureBaseChains() error { +func ensureBaseChains(families ...filter.Family) error { + if len(families) == 0 { + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } commands := make([][]string, 0, 10) - for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { + for _, family := range families { tableFamily := TableFamily(family) output, tableExists, err := ReadTable(run, tableFamily, TableName) if err != nil { @@ -106,22 +113,35 @@ func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string "accept", "comment", `"`+requiredPortComment+`"`) } -func initPreRules(requiredPorts []firewall.PortWhitelist) error { +func initPreRules(requiredPorts []firewall.PortWhitelist, families ...filter.Family) error { + if len(families) == 0 { + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } ports, err := firewall.NormalizeRequiredPorts(requiredPorts) if err != nil { return err } rules := firewall.ExpandPortWhitelist(ports) var commands [][]string - for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { + for _, family := range families { tableFamily := TableFamily(family) - output, _, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain) + output, _, err := ReadTable(run, tableFamily, TableName) if err != nil { return err } + chains := ParseTableChains(output) + existing := make(map[string]map[string]bool) + for _, chain := range []string{BasicBeforeChain, BasicAfterChain} { + existing[chain] = make(map[string]bool) + for _, line := range strings.Split(chains[chain], "\n") { + existing[chain][canonicalRequiredPortRule(line)] = true + } + } candidates := [][]string{ {"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`}, {"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`}, + {"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"}, + {"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "udp", "drop"}, } for _, rule := range rules { if rule.Family == string(family) { @@ -129,90 +149,86 @@ func initPreRules(requiredPorts []firewall.PortWhitelist) error { } } for _, command := range candidates { + chain := command[4] expression := strings.Join(command[5:], " ") - if !containsRequiredPortRule(output, expression) { + key := canonicalRequiredPortRule(expression) + if !existing[chain][key] { + existing[chain][key] = true commands = append(commands, command) - output += "\n" + expression } } - commands = append(commands, - []string{"flush", "chain", tableFamily, TableName, BasicAfterChain}, - []string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"}, - []string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "udp", "drop"}, - ) } return runBatch(commands...) } -func containsRequiredPortRule(output, expression string) bool { - canonical := func(line string) string { - line, _, _ = strings.Cut(line, " comment ") - line, _, _ = strings.Cut(line, " # handle ") - for _, protocol := range []string{"tcp", "udp"} { - line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "") - } - line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line) - fields := strings.Fields(line) - for index, field := range fields { - if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" { - states := strings.Split(field, ",") - for i, state := range states { - value, err := strconv.ParseUint(state, 0, 64) - if err != nil { - continue - } - switch value { - case 1: - states[i] = "invalid" - case 2: - states[i] = "established" - case 4: - states[i] = "related" - case 8: - states[i] = "new" - case 64: - states[i] = "untracked" - } +func canonicalRequiredPortRule(line string) string { + line, _, _ = strings.Cut(line, " comment ") + line, _, _ = strings.Cut(line, " # handle ") + for _, protocol := range []string{"tcp", "udp"} { + line = strings.ReplaceAll(line, "meta l4proto "+protocol+" "+protocol+" ", protocol+" ") + } + line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line) + fields := strings.Fields(line) + for index, field := range fields { + if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" { + states := strings.Split(field, ",") + for i, state := range states { + value, err := strconv.ParseUint(state, 0, 64) + if err != nil { + continue } - slices.Sort(states) - fields[index] = strings.Join(states, ",") - } - if prefix, err := netip.ParsePrefix(field); err == nil { - prefix = prefix.Masked() - fields[index] = prefix.String() - if prefix.Bits() == prefix.Addr().BitLen() { - fields[index] = prefix.Addr().String() + switch value { + case 1: + states[i] = "invalid" + case 2: + states[i] = "established" + case 4: + states[i] = "related" + case 8: + states[i] = "new" + case 64: + states[i] = "untracked" } } + slices.Sort(states) + fields[index] = strings.Join(states, ",") } - return strings.Join(fields, " ") - } - wanted := canonical(expression) - for _, line := range strings.Split(output, "\n") { - if canonical(line) == wanted { - return true + if prefix, err := netip.ParsePrefix(field); err == nil { + prefix = prefix.Masked() + fields[index] = prefix.String() + if prefix.Bits() == prefix.Addr().BitLen() { + fields[index] = prefix.Addr().String() + } } } - return false + return strings.Join(fields, " ") } -func Bind() error { - for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { +func Bind(families ...filter.Family) error { + if len(families) == 0 { + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } + for _, family := range families { tableFamily := TableFamily(family) - if _, err := run("list", "chain", tableFamily, TableName, InputChain); err != nil { - return fmt.Errorf("1Panel nftables %s input chain is not initialized: %w", tableFamily, err) + initialized, _, err := LoadFamilyInitStatus(family, "base") + if err != nil { + return err + } + if !initialized { + return fmt.Errorf("1Panel nftables %s chains are not initialized", tableFamily) } } commands := make([][]string, 0, 8) - for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { + for _, family := range families { tableFamily := TableFamily(family) + commands = append(commands, []string{"add", "table", tableFamily, TableName}) commands = append(commands, []string{"flush", "chain", tableFamily, TableName, InputChain}) for _, chain := range BasicChains() { commands = append(commands, []string{"add", "rule", tableFamily, TableName, InputChain, "jump", chain}) } } if err := runBatch(commands...); err != nil { - cleanupErr := flushInputChains() + cleanupErr := flushInputChains(families...) return errors.Join(err, cleanupErr) } return PersistRuleset(context.Background()) @@ -225,10 +241,28 @@ func Unbind() error { return PersistRuleset(context.Background()) } -func flushInputChains() error { +func flushInputChains(families ...filter.Family) error { + if len(families) == 0 { + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } commands := make([][]string, 0, 2) - for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { + for _, family := range families { commands = append(commands, []string{"flush", "chain", TableFamily(family), TableName, InputChain}) } return runBatch(commands...) } + +func OperateFamily(family filter.Family, initialize bool, ports []firewall.PortWhitelist) error { + if family != filter.FamilyIPv4 && family != filter.FamilyIPv6 { + return fmt.Errorf("unsupported nftables family %q", family) + } + if initialize { + if err := ensureBaseChains(family); err != nil { + return err + } + if err := initPreRules(ports, family); err != nil { + return err + } + } + return Bind(family) +} diff --git a/agent/utils/firewall/nftables_helper/persistence.go b/agent/utils/firewall/nftables_helper/persistence.go index ed9547f24..21998c30d 100644 --- a/agent/utils/firewall/nftables_helper/persistence.go +++ b/agent/utils/firewall/nftables_helper/persistence.go @@ -40,15 +40,19 @@ func PersistRuleset(ctx context.Context) error { return atomicWrite(filepath.Join(global.Dir.FirewallDir, RulesFile), []byte(ruleset.String())) } -func Restore() error { - file := filepath.Join(global.Dir.FirewallDir, RulesFile) - if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { +func Restore(families ...filter.Family) error { + if len(families) == 0 { + families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} + } + data, err := os.ReadFile(filepath.Join(global.Dir.FirewallDir, RulesFile)) + if errors.Is(err, os.ErrNotExist) { return nil - } else if err != nil { + } + if err != nil { return err } - existing := make([]string, 0, 2) - for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { + var script strings.Builder + for _, family := range families { tableFamily := TableFamily(family) _, exists, err := readNftObject(run, "list", "table", tableFamily, TableName) if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { @@ -58,18 +62,35 @@ func Restore() error { return err } if exists { - existing = append(existing, tableFamily) + continue } + table, err := savedTableRules(string(data), tableFamily, TableName) + if err != nil { + return err + } + script.WriteString(table) } - if len(existing) == 2 { + if script.Len() == 0 { return nil } - for _, tableFamily := range existing { - if _, err := run("delete", "table", tableFamily, TableName); err != nil { - return fmt.Errorf("remove partial nftables %s table before restore: %w", tableFamily, err) + return RunScript(script.String()) +} + +func savedTableRules(ruleset, family, table string) (string, error) { + header := "table " + family + " " + table + " {" + lines := strings.Split(ruleset, "\n") + for index, line := range lines { + if line != header { + continue } + for end := index + 1; end < len(lines); end++ { + if lines[end] == "}" { + return strings.Join(lines[index:end+1], "\n") + "\n", nil + } + } + return "", fmt.Errorf("incomplete saved nftables table %s %s", family, table) } - return runCommand("-f", file) + return "", nil } func atomicWrite(target string, data []byte) error { diff --git a/agent/utils/firewall/nftables_helper/runtime.go b/agent/utils/firewall/nftables_helper/runtime.go index c7336b38e..ffe1c3969 100644 --- a/agent/utils/firewall/nftables_helper/runtime.go +++ b/agent/utils/firewall/nftables_helper/runtime.go @@ -81,6 +81,7 @@ func RunScriptContext(ctx context.Context, script string) error { return runScriptFile(script, func(file string) error { return cmd.NewCommandMgr( cmd.WithContext(ctx), + cmd.WithEnv("LC_ALL=C"), cmd.WithTimeout(60*time.Second), ).RunWithOptionalSudo("nft", "-f", file) }) @@ -169,26 +170,35 @@ func hasBaseChainBinding(output string) bool { } func loadFamilyInitStatus(family filter.Family) (bool, bool, error) { + initialized, bound, _, err := LoadFamilyState(family) + return initialized, bound, err +} + +func LoadFamilyState(family filter.Family) (bool, bool, bool, error) { output, exists, err := ReadTable(run, TableFamily(family), TableName) if err != nil || !exists { - return false, false, err + return false, false, false, err } chains := ParseTableChains(output) - for _, chain := range BasicChains() { - if _, exists := chains[chain]; !exists { - return false, false, nil + count := 0 + for _, chain := range append(BasicChains(), InputChain) { + if _, ok := chains[chain]; ok { + count++ } } + if count != len(BasicChains())+1 { + return false, false, count > 0, nil + } input, exists := chains[InputChain] if !exists { - return false, false, nil + return false, false, false, nil } for _, chain := range BasicChains() { if !strings.Contains(input, "jump "+chain) { - return true, false, nil + return true, false, false, nil } } - return true, true, nil + return true, !TableDormant(output), false, nil } func ReadTable(run func(...string) (string, error), family, table string) (string, bool, error) { @@ -228,3 +238,33 @@ func ReadChain(run func(...string) (string, error), family, table, chain string) } return output, nil } + +func TableDormant(output string) bool { + for _, line := range strings.Split(output, "\n") { + fields := strings.Fields(strings.TrimSuffix(strings.TrimSpace(line), ";")) + if len(fields) > 0 && fields[0] == "chain" { + break + } + if len(fields) > 1 && fields[0] == "flags" { + for _, flag := range strings.Split(strings.Join(fields[1:], ""), ",") { + if flag == "dormant" { + return true + } + } + } + } + return false +} + +func SetTableDormant(ctx context.Context, family, table string) error { + manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)) + output, exists, err := ReadTable(func(args ...string) (string, error) { return manager.RunWithOptionalSudoAndStdout("nft", args...) }, family, table) + if err != nil || !exists || TableDormant(output) { + return err + } + script, err := buildBatchScript([]string{"add", "table", family, table, "{", "flags", "dormant", ";", "}"}) + if err != nil { + return err + } + return RunScriptContext(ctx, script) +} diff --git a/agent/utils/firewall/port_whitelist.go b/agent/utils/firewall/port_whitelist.go index 367bf3a47..d9897cc4c 100644 --- a/agent/utils/firewall/port_whitelist.go +++ b/agent/utils/firewall/port_whitelist.go @@ -263,7 +263,7 @@ func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) { } func SystemPortKey(port SystemPort) string { - key := LegacySystemPortKey(port) + key := strings.ToLower(strings.TrimSpace(port.Protocol)) + "/" + strings.TrimSpace(port.Port) if family := strings.ToLower(strings.TrimSpace(port.Family)); family != "" { key = family + "/" + key } @@ -273,10 +273,6 @@ func SystemPortKey(port SystemPort) string { return key } -func LegacySystemPortKey(port SystemPort) string { - return strings.ToLower(strings.TrimSpace(port.Protocol)) + "/" + strings.TrimSpace(port.Port) -} - func SortedSystemPortKeys(ports map[string]SystemPort) []string { keys := make([]string, 0, len(ports)) for key := range ports { diff --git a/agent/utils/firewall/sync/diff.go b/agent/utils/firewall/sync/diff.go deleted file mode 100644 index 2360af79b..000000000 --- a/agent/utils/firewall/sync/diff.go +++ /dev/null @@ -1,21 +0,0 @@ -package sync - -type Status string - -const ( - StatusReady Status = "ready" - StatusExisting Status = "existing" - StatusRemove Status = "remove" - StatusBlocked Status = "blocked" -) - -type ReasonCode string - -const ( - ReasonInvalidPolicy ReasonCode = "invalid_policy" - ReasonAlreadyExists ReasonCode = "already_exists_in_target" - ReasonOnlyExistsInTarget ReasonCode = "only_exists_in_target" - ReasonManagedOnlyInTarget ReasonCode = "managed_only_exists_in_target" - ReasonUnsafeRemoval ReasonCode = "unsafe_managed_rule_removal" - ReasonReadOnlyRule ReasonCode = "read_only_rule" -) diff --git a/agent/utils/terminal/session.go b/agent/utils/terminal/session.go index be8562539..4cc1d7924 100644 --- a/agent/utils/terminal/session.go +++ b/agent/utils/terminal/session.go @@ -309,16 +309,15 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) { if revalidate { s.revalidateCursor = cursor } - shouldClose := clean || (!s.Persistent && !revalidate) - if !shouldClose { + if !clean { timeout := graceTimeout - if revalidate { + if !s.Persistent || revalidate { timeout = revalidateGrace } s.grace = time.AfterFunc(timeout, s.Close) } s.mu.Unlock() - if shouldClose { + if clean { s.Close() } } diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index 3dad4da3b..c1d887038 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -18644,6 +18644,33 @@ const docTemplate = `{ ] } }, + "/hosts/firewall/docker/endpoints": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.DockerPortGuardContainer" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List Docker published ports", + "tags": [ + "Firewall" + ] + } + }, "/hosts/firewall/docker/operate": { "post": { "consumes": [ @@ -18807,11 +18834,28 @@ const docTemplate = `{ ] } }, - "/hosts/firewall/docker/sync": { + "/hosts/firewall/family/operate": { "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallFamilyOperation" + } + } + ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18822,15 +18866,19 @@ const docTemplate = `{ "Timestamp": [] } ], - "summary": "Sync Docker port guard rules", + "summary": "Initialize, repair or bind one firewall address family", "tags": [ "Firewall" ], "x-panel-log": { "BeforeFunctions": [], - "bodyKeys": [], - "formatEN": "sync Docker port guard rules", - "formatZH": "同步 Docker 端口防护规则", + "bodyKeys": [ + "subsystem", + "family", + "operation" + ], + "formatEN": "[operation] [subsystem] [family] firewall chains", + "formatZH": "[operation] [subsystem] [family] 防火墙链", "paramKeys": [] } } @@ -19055,7 +19103,10 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FirewallLifecycleOperationResponse" + } } }, "security": [ @@ -19139,30 +19190,27 @@ const docTemplate = `{ } } }, - "/hosts/firewall/rules/adopt": { - "post": { - "consumes": [ - "application/json" - ], + "/hosts/firewall/rules/backups": { + "get": { "parameters": [ { - "description": "request", - "in": "body", - "name": "request", - "required": true, - "schema": { - "$ref": "#/definitions/dto.FirewallRuleAdopt" - } + "default": "system", + "description": "Firewall subsystem", + "enum": [ + "system", + "forwarding", + "docker" + ], + "in": "query", + "name": "subsystem", + "type": "string" } ], "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request", + "description": "OK", "schema": { - "$ref": "#/definitions/dto.Response" + "$ref": "#/definitions/dto.FirewallRuleBackups" } } }, @@ -19174,25 +19222,18 @@ const docTemplate = `{ "Timestamp": [] } ], - "summary": "Adopt an external firewall rule", + "summary": "List firewall rule backups", "tags": [ "Firewall" - ], - "x-panel-log": { - "bodyKeys": [], - "paramKeys": [], - "BeforeFunctions": [], - "formatZH": "纳管防火墙规则", - "formatEN": "adopt firewall rule" - } + ] } }, "/hosts/firewall/rules/delete": { "post": { - "description": "Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.", "consumes": [ "application/json" ], + "description": "Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.", "parameters": [ { "description": "request", @@ -19318,17 +19359,68 @@ const docTemplate = `{ "Timestamp": [] } ], - "summary": "Reorder a managed unified firewall v2 rule", + "summary": "Reorder a firewall rule", "tags": [ "Firewall" ], "x-panel-log": { "BeforeFunctions": [], "bodyKeys": [ - "uuid" + "instanceKey" ], - "formatEN": "reorder firewall rule [uuid]", - "formatZH": "调整防火墙规则顺序 [uuid]", + "formatEN": "reorder firewall rule [instanceKey]", + "formatZH": "调整防火墙规则顺序 [instanceKey]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/rules/reset": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallRuleReset" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FirewallRuleResetResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Reset firewall rules", + "tags": [ + "Firewall" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "reset firewall rules", + "formatZH": "重置防火墙规则", "paramKeys": [] } } @@ -19412,17 +19504,17 @@ const docTemplate = `{ "Timestamp": [] } ], - "summary": "Update a managed unified firewall v2 rule", + "summary": "Update a firewall rule", "tags": [ "Firewall" ], "x-panel-log": { "BeforeFunctions": [], "bodyKeys": [ - "uuid" + "instanceKey" ], - "formatEN": "update firewall rule [uuid]", - "formatZH": "更新防火墙规则 [uuid]", + "formatEN": "update firewall rule [instanceKey]", + "formatZH": "更新防火墙规则 [instanceKey]", "paramKeys": [] } } @@ -19451,6 +19543,53 @@ const docTemplate = `{ ] } }, + "/hosts/firewall/settings/ipv6": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallIPv6Operation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update firewall IPv6 support", + "tags": [ + "Firewall" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "status" + ], + "formatEN": "Set firewall IPv6 support to [status]", + "formatZH": "设置防火墙 IPv6 支持为 [status]", + "paramKeys": [] + } + } + }, "/hosts/firewall/settings/operate": { "post": { "consumes": [ @@ -19502,7 +19641,7 @@ const docTemplate = `{ "consumes": [ "application/json" ], - "description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.", + "description": "Saves whitelist configuration and applies missing allowances; existing rules are not removed.", "parameters": [ { "description": "request", @@ -19547,7 +19686,7 @@ const docTemplate = `{ "consumes": [ "application/json" ], - "description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.", + "description": "Removes whitelist configuration; existing firewall rules are not removed.", "parameters": [ { "description": "request", @@ -19592,7 +19731,7 @@ const docTemplate = `{ "consumes": [ "application/json" ], - "description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.", + "description": "Saves whitelist configuration and applies missing allowances; existing rules are not removed.", "parameters": [ { "description": "request", @@ -35694,6 +35833,9 @@ const docTemplate = `{ }, "dto.DockerPortGuardBase": { "properties": { + "ipv6Enabled": { + "type": "boolean" + }, "backend": { "type": "string" }, @@ -35709,11 +35851,17 @@ const docTemplate = `{ "ipv6": { "$ref": "#/definitions/dto.DockerPortGuardFamilyStatus" }, + "isExist": { + "type": "boolean" + }, "message": { "type": "string" }, "name": { "type": "string" + }, + "version": { + "type": "string" } }, "type": "object" @@ -35764,7 +35912,7 @@ const docTemplate = `{ "containerPort": { "type": "integer" }, - "description": { + "containerState": { "type": "string" }, "effective": { @@ -35779,6 +35927,12 @@ const docTemplate = `{ "hostPort": { "type": "integer" }, + "managementReason": { + "type": "string" + }, + "managementTarget": { + "type": "string" + }, "mode": { "type": "string" }, @@ -35793,6 +35947,9 @@ const docTemplate = `{ "type": "string" }, "type": "array" + }, + "trafficPath": { + "type": "string" } }, "type": "object" @@ -35841,6 +35998,9 @@ const docTemplate = `{ "initialized": { "type": "boolean" }, + "partial": { + "type": "boolean" + }, "reason": { "type": "string" }, @@ -35860,12 +36020,22 @@ const docTemplate = `{ "$ref": "#/definitions/dto.DockerPortGuardContainer" }, "type": "array" + }, + "orphanPolicies": { + "items": { + "$ref": "#/definitions/dto.DockerPortGuardEndpoint" + }, + "type": "array" } }, "type": "object" }, "dto.DockerPortGuardOperation": { "properties": { + "backupFile": { + "maxLength": 255, + "type": "string" + }, "operation": { "enum": [ "initialize", @@ -35886,10 +36056,6 @@ const docTemplate = `{ }, "dto.DockerPortGuardPolicy": { "properties": { - "description": { - "maxLength": 256, - "type": "string" - }, "family": { "enum": [ "ipv4", @@ -35909,7 +36075,9 @@ const docTemplate = `{ "enum": [ "deny_sources", "allow_sources", - "deny_all" + "deny_all", + "accept_sources", + "accept_all" ], "type": "string" }, @@ -35932,12 +36100,16 @@ const docTemplate = `{ "hostIP", "hostPort", "mode", - "protocol" + "protocol", + "sources" ], "type": "object" }, "dto.DockerPortGuardPolicyBatch": { "properties": { + "import": { + "type": "boolean" + }, "policies": { "items": { "$ref": "#/definitions/dto.DockerPortGuardPolicy" @@ -36194,11 +36366,29 @@ const docTemplate = `{ }, "dto.FirewallBackendFamilyStatus": { "properties": { + "available": { + "type": "boolean" + }, "bound": { "type": "boolean" }, + "forwardPolicy": { + "type": "string" + }, "initialized": { "type": "boolean" + }, + "raInterfaces": { + "items": { + "type": "string" + }, + "type": "array" + }, + "partial": { + "type": "boolean" + }, + "reason": { + "type": "string" } }, "type": "object" @@ -36284,14 +36474,77 @@ const docTemplate = `{ "name": { "type": "string" }, + "supportReason": { + "type": "string" + }, "supported": { "type": "boolean" } }, "type": "object" }, + "dto.FirewallFamilyOperation": { + "properties": { + "backend": { + "enum": [ + "iptables", + "nftables" + ], + "type": "string" + }, + "family": { + "enum": [ + "ipv4", + "ipv6" + ], + "type": "string" + }, + "operation": { + "enum": [ + "initialize", + "repair", + "bind" + ], + "type": "string" + }, + "subsystem": { + "enum": [ + "system", + "forwarding", + "docker" + ], + "type": "string" + } + }, + "required": [ + "backend", + "family", + "operation", + "subsystem" + ], + "type": "object" + }, + "dto.FirewallIPv6Operation": { + "properties": { + "status": { + "enum": [ + "Enable", + "Disable" + ], + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, "dto.FirewallInitializationTask": { "properties": { + "backupFile": { + "maxLength": 255, + "type": "string" + }, "taskID": { "maxLength": 64, "type": "string" @@ -36320,6 +36573,17 @@ const docTemplate = `{ ], "type": "object" }, + "dto.FirewallLifecycleOperationResponse": { + "properties": { + "queued": { + "type": "boolean" + }, + "taskID": { + "type": "string" + } + }, + "type": "object" + }, "dto.FirewallNativeDetail": { "properties": { "name": { @@ -36391,35 +36655,53 @@ const docTemplate = `{ ], "type": "object" }, - "dto.FirewallRuleAdopt": { - "type": "object", - "required": [ - "scope", - "instanceKey" - ], + "dto.FirewallRuleBackup": { "properties": { - "scope": { - "$ref": "#/definitions/filter.Scope" + "modifiedAt": { + "type": "integer" }, - "instanceKey": { - "type": "string", - "maxLength": 128 + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "ruleCount": { + "type": "integer" } - } + }, + "type": "object" + }, + "dto.FirewallRuleBackups": { + "properties": { + "directory": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/definitions/dto.FirewallRuleBackup" + }, + "type": "array" + } + }, + "type": "object" }, "dto.FirewallRuleCreate": { "properties": { + "backupFile": { + "maxLength": 255, + "type": "string" + }, + "initialize": { + "type": "boolean" + }, "items": { "items": { "$ref": "#/definitions/dto.FirewallRuleCreateItem" }, - "minItems": 1, "type": "array" } }, - "required": [ - "items" - ], "type": "object" }, "dto.FirewallRuleCreateFailure": { @@ -36441,12 +36723,15 @@ const docTemplate = `{ }, "dto.FirewallRuleCreateItem": { "properties": { + "parseStatus": { + "$ref": "#/definitions/filter.ParseStatus" + }, + "raw": { + "type": "string" + }, "rule": { "$ref": "#/definitions/filter.FirewallRule" }, - "sourceID": { - "type": "string" - }, "sourceKind": { "enum": [ "user", @@ -36487,36 +36772,17 @@ const docTemplate = `{ "type": "object" }, "dto.FirewallRuleDelete": { - "description": "Provide managed rule UUIDs, before-chain rule targets, or both.", "properties": { - "beforeRules": { + "targets": { "items": { - "$ref": "#/definitions/dto.FirewallRuleDeleteTarget" + "$ref": "#/definitions/dto.FirewallRuleDeleteItem" }, + "minItems": 1, "type": "array" - }, - "uuids": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "dto.FirewallRuleDeleteTarget": { - "properties": { - "instanceKey": { - "maxLength": 128, - "type": "string" - }, - "scope": { - "$ref": "#/definitions/filter.Scope" } }, "required": [ - "scope", - "instanceKey" + "targets" ], "type": "object" }, @@ -36528,12 +36794,32 @@ const docTemplate = `{ "index": { "type": "integer" }, - "uuid": { + "instanceKey": { "type": "string" } }, "type": "object" }, + "dto.FirewallRuleDeleteItem": { + "properties": { + "instanceKey": { + "maxLength": 128, + "type": "string" + }, + "observed": { + "$ref": "#/definitions/filter.ObservedRule" + }, + "scope": { + "$ref": "#/definitions/filter.Scope" + } + }, + "required": [ + "instanceKey", + "observed", + "scope" + ], + "type": "object" + }, "dto.FirewallRuleDeleteResponse": { "properties": { "errors": { @@ -36557,14 +36843,26 @@ const docTemplate = `{ }, "type": "object" }, + "dto.FirewallRuleDeleteTarget": { + "properties": { + "instanceKey": { + "maxLength": 128, + "type": "string" + }, + "scope": { + "$ref": "#/definitions/filter.Scope" + } + }, + "required": [ + "instanceKey", + "scope" + ], + "type": "object" + }, "dto.FirewallRuleInventory": { "properties": { "actions": { "items": { - "enum": [ - "accept", - "deny" - ], "type": "string" }, "type": "array" @@ -36574,21 +36872,12 @@ const docTemplate = `{ }, "excludeChains": { "items": { - "enum": [ - "1PANEL_BASIC_BEFORE", - "1PANEL_BASIC", - "1PANEL_BASIC_AFTER" - ], "type": "string" }, "type": "array" }, "families": { "items": { - "enum": [ - "ipv4", - "ipv6" - ], "type": "string" }, "type": "array" @@ -36611,19 +36900,6 @@ const docTemplate = `{ }, "maxItems": 16, "type": "array" - }, - "states": { - "items": { - "enum": [ - "managed", - "adopted", - "external", - "drifted", - "protected" - ], - "type": "string" - }, - "type": "array" } }, "required": [ @@ -36637,15 +36913,18 @@ const docTemplate = `{ "allTotal": { "type": "integer" }, + "ipv4Range": { + "$ref": "#/definitions/filter.PositionRange" + }, + "ipv6Range": { + "$ref": "#/definitions/filter.PositionRange" + }, "items": { "items": { "$ref": "#/definitions/filter.InventoryItem" }, "type": "array" }, - "managedTotal": { - "type": "integer" - }, "notices": { "items": { "$ref": "#/definitions/filter.ScopeNotice" @@ -36660,34 +36939,102 @@ const docTemplate = `{ }, "dto.FirewallRuleReorder": { "properties": { + "instanceKey": { + "maxLength": 128, + "type": "string" + }, "priority": { "type": "integer" }, + "scope": { + "$ref": "#/definitions/filter.Scope" + }, "targetPosition": { "type": "integer" + } + }, + "required": [ + "instanceKey", + "scope" + ], + "type": "object" + }, + "dto.FirewallRuleReset": { + "properties": { + "backup": { + "default": true, + "type": "boolean" }, - "uuid": { + "provider": { + "enum": [ + "firewalld", + "ufw", + "iptables", + "nftables" + ], "type": "string" + }, + "subsystem": { + "enum": [ + "system", + "forwarding", + "docker" + ], + "type": "string" + }, + "withDockerRestart": { + "type": "boolean" + } + }, + "type": "object" + }, + "dto.FirewallRuleResetResponse": { + "properties": { + "backupPath": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "removed": { + "type": "integer" } }, "type": "object" }, "dto.FirewallRuleUpdate": { "properties": { + "description": { + "type": "string" + }, + "instanceKey": { + "maxLength": 128, + "type": "string" + }, + "orderIndex": { + "type": "integer" + }, + "priority": { + "type": "integer" + }, "rule": { "$ref": "#/definitions/filter.FirewallRule" }, - "uuid": { - "type": "string" + "scope": { + "$ref": "#/definitions/filter.Scope" } }, "required": [ - "rule" + "instanceKey", + "scope" ], "type": "object" }, "dto.FirewallSettings": { "properties": { + "ipv6Enabled": { + "type": "boolean" + }, "docker": { "$ref": "#/definitions/dto.FirewallBackendGroup" }, @@ -36701,10 +37048,10 @@ const docTemplate = `{ "type": "string" }, "portWhiteList": { - "type": "array", "items": { "$ref": "#/definitions/filter.PortWhitelist" - } + }, + "type": "array" }, "sshPort": { "type": "string" @@ -36717,9 +37064,21 @@ const docTemplate = `{ }, "dto.FirewallSubsystemStatus": { "properties": { + "ipv6Enabled": { + "type": "boolean" + }, "backend": { "type": "string" }, + "conflictBackend": { + "type": "string" + }, + "ipv4": { + "$ref": "#/definitions/dto.FirewallBackendFamilyStatus" + }, + "ipv6": { + "$ref": "#/definitions/dto.FirewallBackendFamilyStatus" + }, "isActive": { "type": "boolean" }, @@ -36732,13 +37091,19 @@ const docTemplate = `{ "isInit": { "type": "boolean" }, + "lifecycleTaskID": { + "type": "string" + }, + "message": { + "type": "string" + }, "name": { "type": "string" }, "pingStatus": { "type": "string" }, - "syncError": { + "reason": { "type": "string" }, "version": { @@ -36783,23 +37148,76 @@ const docTemplate = `{ }, "type": "object" }, + "dto.ForwardRule": { + "properties": { + "address": { + "type": "string" + }, + "chain": { + "type": "string" + }, + "description": { + "type": "string" + }, + "family": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "interface": { + "type": "string" + }, + "isDesired": { + "type": "boolean" + }, + "isRuntime": { + "type": "boolean" + }, + "num": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "strategy": { + "type": "string" + }, + "syncStatus": { + "type": "string" + }, + "targetIP": { + "type": "string" + }, + "targetPort": { + "type": "string" + }, + "usedStatus": { + "type": "string" + } + }, + "type": "object" + }, "dto.ForwardRuleOperate": { "properties": { - "forceDelete": { + "import": { "type": "boolean" }, "rules": { "items": { "$ref": "#/definitions/dto.ForwardRuleOperation" }, - "type": "array", - "minItems": 1 + "minItems": 1, + "type": "array" } }, - "type": "object", "required": [ "rules" - ] + ], + "type": "object" }, "dto.ForwardRuleOperation": { "properties": { @@ -36851,6 +37269,9 @@ const docTemplate = `{ }, "dto.ForwardRuleSearch": { "properties": { + "all": { + "type": "boolean" + }, "info": { "type": "string" }, @@ -41465,29 +41886,6 @@ const docTemplate = `{ "ActionReject" ] }, - "filter.DesiredRule": { - "properties": { - "marker": { - "type": "string" - }, - "observedInstanceKey": { - "type": "string" - }, - "origin": { - "type": "string" - }, - "rule": { - "$ref": "#/definitions/filter.FirewallRule" - }, - "ruleKey": { - "type": "string" - }, - "uuid": { - "type": "string" - } - }, - "type": "object" - }, "filter.Direction": { "enum": [ "input" @@ -41552,63 +41950,21 @@ const docTemplate = `{ }, "filter.InventoryItem": { "properties": { - "desired": { - "$ref": "#/definitions/filter.DesiredRule" + "descriptionID": { + "type": "string" }, - "match": { - "$ref": "#/definitions/filter.InventoryMatch" + "isWhitelist": { + "type": "boolean" }, "observed": { "$ref": "#/definitions/filter.ObservedRule" }, "rule": { "$ref": "#/definitions/filter.FirewallRule" - }, - "state": { - "$ref": "#/definitions/filter.InventoryState" - }, - "usage": { - "$ref": "#/definitions/filter.RuntimeUsage" } }, "type": "object" }, - "filter.InventoryMatch": { - "enum": [ - "none", - "exact", - "changed", - "missing", - "ambiguous", - "opaque" - ], - "type": "string", - "x-enum-varnames": [ - "InventoryMatchNone", - "InventoryMatchExact", - "InventoryMatchChanged", - "InventoryMatchMissing", - "InventoryMatchAmbiguous", - "InventoryMatchOpaque" - ] - }, - "filter.InventoryState": { - "enum": [ - "managed", - "adopted", - "external", - "drifted", - "protected" - ], - "type": "string", - "x-enum-varnames": [ - "InventoryStateManaged", - "InventoryStateAdopted", - "InventoryStateExternal", - "InventoryStateDrifted", - "InventoryStateProtected" - ] - }, "filter.Locator": { "properties": { "canonical": { @@ -41675,6 +42031,12 @@ const docTemplate = `{ }, "rule": { "$ref": "#/definitions/filter.FirewallRule" + }, + "uncertainFields": { + "items": { + "type": "string" + }, + "type": "array" } }, "type": "object" @@ -41714,10 +42076,10 @@ const docTemplate = `{ "type": "string" }, "sources": { - "type": "array", "items": { "type": "string" - } + }, + "type": "array" }, "type": { "type": "string" @@ -41725,19 +42087,13 @@ const docTemplate = `{ }, "type": "object" }, - "filter.RuntimeUsage": { + "filter.PositionRange": { "properties": { - "reason": { - "type": "string" + "max": { + "type": "integer" }, - "used": { - "type": "boolean" - }, - "usedBy": { - "items": { - "type": "string" - }, - "type": "array" + "min": { + "type": "integer" } }, "type": "object" @@ -41781,19 +42137,47 @@ const docTemplate = `{ }, "filter.ScopeNoticeCode": { "enum": [ - "default_scope_mismatch", "managed_scope_inactive", - "unmanaged_active_scopes", "runtime_permanent_mismatch", - "managed_scope_missing" + "managed_scope_missing", + "family_unavailable" ], "type": "string", "x-enum-varnames": [ - "ScopeNoticeDefaultScopeMismatch", "ScopeNoticeManagedScopeInactive", - "ScopeNoticeUnmanagedActiveScopes", "ScopeNoticeRuntimePermanentMismatch", - "ScopeNoticeManagedScopeMissing" + "ScopeNoticeManagedScopeMissing", + "ScopeNoticeFamilyUnavailable" + ] + }, + "github_com_1Panel-dev_1Panel_agent_utils_firewall_sync.ReasonCode": { + "enum": [ + "invalid_policy", + "already_exists_in_target", + "only_exists_in_target", + "read_only_rule" + ], + "type": "string", + "x-enum-varnames": [ + "ReasonInvalidPolicy", + "ReasonAlreadyExists", + "ReasonOnlyExistsInTarget", + "ReasonReadOnlyRule" + ] + }, + "github_com_1Panel-dev_1Panel_agent_utils_firewall_sync.Status": { + "enum": [ + "ready", + "existing", + "remove", + "blocked" + ], + "type": "string", + "x-enum-varnames": [ + "StatusReady", + "StatusExisting", + "StatusRemove", + "StatusBlocked" ] }, "mfa.Otp": { diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index 5ebf2ce42..0a180e4cf 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -18640,6 +18640,33 @@ ] } }, + "/hosts/firewall/docker/endpoints": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.DockerPortGuardContainer" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List Docker published ports", + "tags": [ + "Firewall" + ] + } + }, "/hosts/firewall/docker/operate": { "post": { "consumes": [ @@ -18803,11 +18830,28 @@ ] } }, - "/hosts/firewall/docker/sync": { + "/hosts/firewall/family/operate": { "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallFamilyOperation" + } + } + ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } } }, "security": [ @@ -18818,15 +18862,19 @@ "Timestamp": [] } ], - "summary": "Sync Docker port guard rules", + "summary": "Initialize, repair or bind one firewall address family", "tags": [ "Firewall" ], "x-panel-log": { "BeforeFunctions": [], - "bodyKeys": [], - "formatEN": "sync Docker port guard rules", - "formatZH": "同步 Docker 端口防护规则", + "bodyKeys": [ + "subsystem", + "family", + "operation" + ], + "formatEN": "[operation] [subsystem] [family] firewall chains", + "formatZH": "[operation] [subsystem] [family] 防火墙链", "paramKeys": [] } } @@ -19051,7 +19099,10 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FirewallLifecycleOperationResponse" + } } }, "security": [ @@ -19135,30 +19186,27 @@ } } }, - "/hosts/firewall/rules/adopt": { - "post": { - "consumes": [ - "application/json" - ], + "/hosts/firewall/rules/backups": { + "get": { "parameters": [ { - "description": "request", - "in": "body", - "name": "request", - "required": true, - "schema": { - "$ref": "#/definitions/dto.FirewallRuleAdopt" - } + "default": "system", + "description": "Firewall subsystem", + "enum": [ + "system", + "forwarding", + "docker" + ], + "in": "query", + "name": "subsystem", + "type": "string" } ], "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request", + "description": "OK", "schema": { - "$ref": "#/definitions/dto.Response" + "$ref": "#/definitions/dto.FirewallRuleBackups" } } }, @@ -19170,25 +19218,18 @@ "Timestamp": [] } ], - "summary": "Adopt an external firewall rule", + "summary": "List firewall rule backups", "tags": [ "Firewall" - ], - "x-panel-log": { - "bodyKeys": [], - "paramKeys": [], - "BeforeFunctions": [], - "formatZH": "纳管防火墙规则", - "formatEN": "adopt firewall rule" - } + ] } }, "/hosts/firewall/rules/delete": { "post": { - "description": "Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.", "consumes": [ "application/json" ], + "description": "Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.", "parameters": [ { "description": "request", @@ -19314,17 +19355,68 @@ "Timestamp": [] } ], - "summary": "Reorder a managed unified firewall v2 rule", + "summary": "Reorder a firewall rule", "tags": [ "Firewall" ], "x-panel-log": { "BeforeFunctions": [], "bodyKeys": [ - "uuid" + "instanceKey" ], - "formatEN": "reorder firewall rule [uuid]", - "formatZH": "调整防火墙规则顺序 [uuid]", + "formatEN": "reorder firewall rule [instanceKey]", + "formatZH": "调整防火墙规则顺序 [instanceKey]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/rules/reset": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallRuleReset" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FirewallRuleResetResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Reset firewall rules", + "tags": [ + "Firewall" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "reset firewall rules", + "formatZH": "重置防火墙规则", "paramKeys": [] } } @@ -19408,17 +19500,17 @@ "Timestamp": [] } ], - "summary": "Update a managed unified firewall v2 rule", + "summary": "Update a firewall rule", "tags": [ "Firewall" ], "x-panel-log": { "BeforeFunctions": [], "bodyKeys": [ - "uuid" + "instanceKey" ], - "formatEN": "update firewall rule [uuid]", - "formatZH": "更新防火墙规则 [uuid]", + "formatEN": "update firewall rule [instanceKey]", + "formatZH": "更新防火墙规则 [instanceKey]", "paramKeys": [] } } @@ -19447,6 +19539,53 @@ ] } }, + "/hosts/firewall/settings/ipv6": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallIPv6Operation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FilterChainOperationResponse" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update firewall IPv6 support", + "tags": [ + "Firewall" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "status" + ], + "formatEN": "Set firewall IPv6 support to [status]", + "formatZH": "设置防火墙 IPv6 支持为 [status]", + "paramKeys": [] + } + } + }, "/hosts/firewall/settings/operate": { "post": { "consumes": [ @@ -19498,7 +19637,7 @@ "consumes": [ "application/json" ], - "description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.", + "description": "Saves whitelist configuration and applies missing allowances; existing rules are not removed.", "parameters": [ { "description": "request", @@ -19543,7 +19682,7 @@ "consumes": [ "application/json" ], - "description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.", + "description": "Removes whitelist configuration; existing firewall rules are not removed.", "parameters": [ { "description": "request", @@ -19588,7 +19727,7 @@ "consumes": [ "application/json" ], - "description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.", + "description": "Saves whitelist configuration and applies missing allowances; existing rules are not removed.", "parameters": [ { "description": "request", @@ -35690,6 +35829,9 @@ }, "dto.DockerPortGuardBase": { "properties": { + "ipv6Enabled": { + "type": "boolean" + }, "backend": { "type": "string" }, @@ -35705,11 +35847,17 @@ "ipv6": { "$ref": "#/definitions/dto.DockerPortGuardFamilyStatus" }, + "isExist": { + "type": "boolean" + }, "message": { "type": "string" }, "name": { "type": "string" + }, + "version": { + "type": "string" } }, "type": "object" @@ -35760,7 +35908,7 @@ "containerPort": { "type": "integer" }, - "description": { + "containerState": { "type": "string" }, "effective": { @@ -35775,6 +35923,12 @@ "hostPort": { "type": "integer" }, + "managementReason": { + "type": "string" + }, + "managementTarget": { + "type": "string" + }, "mode": { "type": "string" }, @@ -35789,6 +35943,9 @@ "type": "string" }, "type": "array" + }, + "trafficPath": { + "type": "string" } }, "type": "object" @@ -35837,6 +35994,9 @@ "initialized": { "type": "boolean" }, + "partial": { + "type": "boolean" + }, "reason": { "type": "string" }, @@ -35856,12 +36016,22 @@ "$ref": "#/definitions/dto.DockerPortGuardContainer" }, "type": "array" + }, + "orphanPolicies": { + "items": { + "$ref": "#/definitions/dto.DockerPortGuardEndpoint" + }, + "type": "array" } }, "type": "object" }, "dto.DockerPortGuardOperation": { "properties": { + "backupFile": { + "maxLength": 255, + "type": "string" + }, "operation": { "enum": [ "initialize", @@ -35882,10 +36052,6 @@ }, "dto.DockerPortGuardPolicy": { "properties": { - "description": { - "maxLength": 256, - "type": "string" - }, "family": { "enum": [ "ipv4", @@ -35905,7 +36071,9 @@ "enum": [ "deny_sources", "allow_sources", - "deny_all" + "deny_all", + "accept_sources", + "accept_all" ], "type": "string" }, @@ -35928,12 +36096,16 @@ "hostIP", "hostPort", "mode", - "protocol" + "protocol", + "sources" ], "type": "object" }, "dto.DockerPortGuardPolicyBatch": { "properties": { + "import": { + "type": "boolean" + }, "policies": { "items": { "$ref": "#/definitions/dto.DockerPortGuardPolicy" @@ -36190,11 +36362,29 @@ }, "dto.FirewallBackendFamilyStatus": { "properties": { + "available": { + "type": "boolean" + }, "bound": { "type": "boolean" }, + "forwardPolicy": { + "type": "string" + }, "initialized": { "type": "boolean" + }, + "raInterfaces": { + "items": { + "type": "string" + }, + "type": "array" + }, + "partial": { + "type": "boolean" + }, + "reason": { + "type": "string" } }, "type": "object" @@ -36280,14 +36470,77 @@ "name": { "type": "string" }, + "supportReason": { + "type": "string" + }, "supported": { "type": "boolean" } }, "type": "object" }, + "dto.FirewallFamilyOperation": { + "properties": { + "backend": { + "enum": [ + "iptables", + "nftables" + ], + "type": "string" + }, + "family": { + "enum": [ + "ipv4", + "ipv6" + ], + "type": "string" + }, + "operation": { + "enum": [ + "initialize", + "repair", + "bind" + ], + "type": "string" + }, + "subsystem": { + "enum": [ + "system", + "forwarding", + "docker" + ], + "type": "string" + } + }, + "required": [ + "backend", + "family", + "operation", + "subsystem" + ], + "type": "object" + }, + "dto.FirewallIPv6Operation": { + "properties": { + "status": { + "enum": [ + "Enable", + "Disable" + ], + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, "dto.FirewallInitializationTask": { "properties": { + "backupFile": { + "maxLength": 255, + "type": "string" + }, "taskID": { "maxLength": 64, "type": "string" @@ -36316,6 +36569,17 @@ ], "type": "object" }, + "dto.FirewallLifecycleOperationResponse": { + "properties": { + "queued": { + "type": "boolean" + }, + "taskID": { + "type": "string" + } + }, + "type": "object" + }, "dto.FirewallNativeDetail": { "properties": { "name": { @@ -36387,35 +36651,53 @@ ], "type": "object" }, - "dto.FirewallRuleAdopt": { - "type": "object", - "required": [ - "scope", - "instanceKey" - ], + "dto.FirewallRuleBackup": { "properties": { - "scope": { - "$ref": "#/definitions/filter.Scope" + "modifiedAt": { + "type": "integer" }, - "instanceKey": { - "type": "string", - "maxLength": 128 + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "ruleCount": { + "type": "integer" } - } + }, + "type": "object" + }, + "dto.FirewallRuleBackups": { + "properties": { + "directory": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/definitions/dto.FirewallRuleBackup" + }, + "type": "array" + } + }, + "type": "object" }, "dto.FirewallRuleCreate": { "properties": { + "backupFile": { + "maxLength": 255, + "type": "string" + }, + "initialize": { + "type": "boolean" + }, "items": { "items": { "$ref": "#/definitions/dto.FirewallRuleCreateItem" }, - "minItems": 1, "type": "array" } }, - "required": [ - "items" - ], "type": "object" }, "dto.FirewallRuleCreateFailure": { @@ -36437,12 +36719,15 @@ }, "dto.FirewallRuleCreateItem": { "properties": { + "parseStatus": { + "$ref": "#/definitions/filter.ParseStatus" + }, + "raw": { + "type": "string" + }, "rule": { "$ref": "#/definitions/filter.FirewallRule" }, - "sourceID": { - "type": "string" - }, "sourceKind": { "enum": [ "user", @@ -36483,36 +36768,17 @@ "type": "object" }, "dto.FirewallRuleDelete": { - "description": "Provide managed rule UUIDs, before-chain rule targets, or both.", "properties": { - "beforeRules": { + "targets": { "items": { - "$ref": "#/definitions/dto.FirewallRuleDeleteTarget" + "$ref": "#/definitions/dto.FirewallRuleDeleteItem" }, + "minItems": 1, "type": "array" - }, - "uuids": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "dto.FirewallRuleDeleteTarget": { - "properties": { - "instanceKey": { - "maxLength": 128, - "type": "string" - }, - "scope": { - "$ref": "#/definitions/filter.Scope" } }, "required": [ - "scope", - "instanceKey" + "targets" ], "type": "object" }, @@ -36524,12 +36790,32 @@ "index": { "type": "integer" }, - "uuid": { + "instanceKey": { "type": "string" } }, "type": "object" }, + "dto.FirewallRuleDeleteItem": { + "properties": { + "instanceKey": { + "maxLength": 128, + "type": "string" + }, + "observed": { + "$ref": "#/definitions/filter.ObservedRule" + }, + "scope": { + "$ref": "#/definitions/filter.Scope" + } + }, + "required": [ + "instanceKey", + "observed", + "scope" + ], + "type": "object" + }, "dto.FirewallRuleDeleteResponse": { "properties": { "errors": { @@ -36553,14 +36839,26 @@ }, "type": "object" }, + "dto.FirewallRuleDeleteTarget": { + "properties": { + "instanceKey": { + "maxLength": 128, + "type": "string" + }, + "scope": { + "$ref": "#/definitions/filter.Scope" + } + }, + "required": [ + "instanceKey", + "scope" + ], + "type": "object" + }, "dto.FirewallRuleInventory": { "properties": { "actions": { "items": { - "enum": [ - "accept", - "deny" - ], "type": "string" }, "type": "array" @@ -36570,21 +36868,12 @@ }, "excludeChains": { "items": { - "enum": [ - "1PANEL_BASIC_BEFORE", - "1PANEL_BASIC", - "1PANEL_BASIC_AFTER" - ], "type": "string" }, "type": "array" }, "families": { "items": { - "enum": [ - "ipv4", - "ipv6" - ], "type": "string" }, "type": "array" @@ -36607,19 +36896,6 @@ }, "maxItems": 16, "type": "array" - }, - "states": { - "items": { - "enum": [ - "managed", - "adopted", - "external", - "drifted", - "protected" - ], - "type": "string" - }, - "type": "array" } }, "required": [ @@ -36633,15 +36909,18 @@ "allTotal": { "type": "integer" }, + "ipv4Range": { + "$ref": "#/definitions/filter.PositionRange" + }, + "ipv6Range": { + "$ref": "#/definitions/filter.PositionRange" + }, "items": { "items": { "$ref": "#/definitions/filter.InventoryItem" }, "type": "array" }, - "managedTotal": { - "type": "integer" - }, "notices": { "items": { "$ref": "#/definitions/filter.ScopeNotice" @@ -36656,34 +36935,102 @@ }, "dto.FirewallRuleReorder": { "properties": { + "instanceKey": { + "maxLength": 128, + "type": "string" + }, "priority": { "type": "integer" }, + "scope": { + "$ref": "#/definitions/filter.Scope" + }, "targetPosition": { "type": "integer" + } + }, + "required": [ + "instanceKey", + "scope" + ], + "type": "object" + }, + "dto.FirewallRuleReset": { + "properties": { + "backup": { + "default": true, + "type": "boolean" }, - "uuid": { + "provider": { + "enum": [ + "firewalld", + "ufw", + "iptables", + "nftables" + ], "type": "string" + }, + "subsystem": { + "enum": [ + "system", + "forwarding", + "docker" + ], + "type": "string" + }, + "withDockerRestart": { + "type": "boolean" + } + }, + "type": "object" + }, + "dto.FirewallRuleResetResponse": { + "properties": { + "backupPath": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "removed": { + "type": "integer" } }, "type": "object" }, "dto.FirewallRuleUpdate": { "properties": { + "description": { + "type": "string" + }, + "instanceKey": { + "maxLength": 128, + "type": "string" + }, + "orderIndex": { + "type": "integer" + }, + "priority": { + "type": "integer" + }, "rule": { "$ref": "#/definitions/filter.FirewallRule" }, - "uuid": { - "type": "string" + "scope": { + "$ref": "#/definitions/filter.Scope" } }, "required": [ - "rule" + "instanceKey", + "scope" ], "type": "object" }, "dto.FirewallSettings": { "properties": { + "ipv6Enabled": { + "type": "boolean" + }, "docker": { "$ref": "#/definitions/dto.FirewallBackendGroup" }, @@ -36697,10 +37044,10 @@ "type": "string" }, "portWhiteList": { - "type": "array", "items": { "$ref": "#/definitions/filter.PortWhitelist" - } + }, + "type": "array" }, "sshPort": { "type": "string" @@ -36713,9 +37060,21 @@ }, "dto.FirewallSubsystemStatus": { "properties": { + "ipv6Enabled": { + "type": "boolean" + }, "backend": { "type": "string" }, + "conflictBackend": { + "type": "string" + }, + "ipv4": { + "$ref": "#/definitions/dto.FirewallBackendFamilyStatus" + }, + "ipv6": { + "$ref": "#/definitions/dto.FirewallBackendFamilyStatus" + }, "isActive": { "type": "boolean" }, @@ -36728,13 +37087,19 @@ "isInit": { "type": "boolean" }, + "lifecycleTaskID": { + "type": "string" + }, + "message": { + "type": "string" + }, "name": { "type": "string" }, "pingStatus": { "type": "string" }, - "syncError": { + "reason": { "type": "string" }, "version": { @@ -36779,23 +37144,76 @@ }, "type": "object" }, + "dto.ForwardRule": { + "properties": { + "address": { + "type": "string" + }, + "chain": { + "type": "string" + }, + "description": { + "type": "string" + }, + "family": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "interface": { + "type": "string" + }, + "isDesired": { + "type": "boolean" + }, + "isRuntime": { + "type": "boolean" + }, + "num": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "strategy": { + "type": "string" + }, + "syncStatus": { + "type": "string" + }, + "targetIP": { + "type": "string" + }, + "targetPort": { + "type": "string" + }, + "usedStatus": { + "type": "string" + } + }, + "type": "object" + }, "dto.ForwardRuleOperate": { "properties": { - "forceDelete": { + "import": { "type": "boolean" }, "rules": { "items": { "$ref": "#/definitions/dto.ForwardRuleOperation" }, - "type": "array", - "minItems": 1 + "minItems": 1, + "type": "array" } }, - "type": "object", "required": [ "rules" - ] + ], + "type": "object" }, "dto.ForwardRuleOperation": { "properties": { @@ -36847,6 +37265,9 @@ }, "dto.ForwardRuleSearch": { "properties": { + "all": { + "type": "boolean" + }, "info": { "type": "string" }, @@ -41461,29 +41882,6 @@ "ActionReject" ] }, - "filter.DesiredRule": { - "properties": { - "marker": { - "type": "string" - }, - "observedInstanceKey": { - "type": "string" - }, - "origin": { - "type": "string" - }, - "rule": { - "$ref": "#/definitions/filter.FirewallRule" - }, - "ruleKey": { - "type": "string" - }, - "uuid": { - "type": "string" - } - }, - "type": "object" - }, "filter.Direction": { "enum": [ "input" @@ -41548,63 +41946,21 @@ }, "filter.InventoryItem": { "properties": { - "desired": { - "$ref": "#/definitions/filter.DesiredRule" + "descriptionID": { + "type": "string" }, - "match": { - "$ref": "#/definitions/filter.InventoryMatch" + "isWhitelist": { + "type": "boolean" }, "observed": { "$ref": "#/definitions/filter.ObservedRule" }, "rule": { "$ref": "#/definitions/filter.FirewallRule" - }, - "state": { - "$ref": "#/definitions/filter.InventoryState" - }, - "usage": { - "$ref": "#/definitions/filter.RuntimeUsage" } }, "type": "object" }, - "filter.InventoryMatch": { - "enum": [ - "none", - "exact", - "changed", - "missing", - "ambiguous", - "opaque" - ], - "type": "string", - "x-enum-varnames": [ - "InventoryMatchNone", - "InventoryMatchExact", - "InventoryMatchChanged", - "InventoryMatchMissing", - "InventoryMatchAmbiguous", - "InventoryMatchOpaque" - ] - }, - "filter.InventoryState": { - "enum": [ - "managed", - "adopted", - "external", - "drifted", - "protected" - ], - "type": "string", - "x-enum-varnames": [ - "InventoryStateManaged", - "InventoryStateAdopted", - "InventoryStateExternal", - "InventoryStateDrifted", - "InventoryStateProtected" - ] - }, "filter.Locator": { "properties": { "canonical": { @@ -41671,6 +42027,12 @@ }, "rule": { "$ref": "#/definitions/filter.FirewallRule" + }, + "uncertainFields": { + "items": { + "type": "string" + }, + "type": "array" } }, "type": "object" @@ -41710,10 +42072,10 @@ "type": "string" }, "sources": { - "type": "array", "items": { "type": "string" - } + }, + "type": "array" }, "type": { "type": "string" @@ -41721,19 +42083,13 @@ }, "type": "object" }, - "filter.RuntimeUsage": { + "filter.PositionRange": { "properties": { - "reason": { - "type": "string" + "max": { + "type": "integer" }, - "used": { - "type": "boolean" - }, - "usedBy": { - "items": { - "type": "string" - }, - "type": "array" + "min": { + "type": "integer" } }, "type": "object" @@ -41777,19 +42133,47 @@ }, "filter.ScopeNoticeCode": { "enum": [ - "default_scope_mismatch", "managed_scope_inactive", - "unmanaged_active_scopes", "runtime_permanent_mismatch", - "managed_scope_missing" + "managed_scope_missing", + "family_unavailable" ], "type": "string", "x-enum-varnames": [ - "ScopeNoticeDefaultScopeMismatch", "ScopeNoticeManagedScopeInactive", - "ScopeNoticeUnmanagedActiveScopes", "ScopeNoticeRuntimePermanentMismatch", - "ScopeNoticeManagedScopeMissing" + "ScopeNoticeManagedScopeMissing", + "ScopeNoticeFamilyUnavailable" + ] + }, + "github_com_1Panel-dev_1Panel_agent_utils_firewall_sync.ReasonCode": { + "enum": [ + "invalid_policy", + "already_exists_in_target", + "only_exists_in_target", + "read_only_rule" + ], + "type": "string", + "x-enum-varnames": [ + "ReasonInvalidPolicy", + "ReasonAlreadyExists", + "ReasonOnlyExistsInTarget", + "ReasonReadOnlyRule" + ] + }, + "github_com_1Panel-dev_1Panel_agent_utils_firewall_sync.Status": { + "enum": [ + "ready", + "existing", + "remove", + "blocked" + ], + "type": "string", + "x-enum-varnames": [ + "StatusReady", + "StatusExisting", + "StatusRemove", + "StatusBlocked" ] }, "mfa.Otp": { diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index a62cd236f..7974f4b8a 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -265,8 +265,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "应用端口修改 [key]-[name] =\u003e [port]", - "formatEN": "Application port update [key]-[name] =\u003e [port]" + "formatZH": "应用端口修改 [key]-[name] => [port]", + "formatEN": "Application port update [key]-[name] => [port]" }, "/apps/installed/sort/update": { "bodyKeys": [], @@ -679,8 +679,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "容器重命名 [name] =\u003e [newName]", - "formatEN": "rename container [name] =\u003e [newName]" + "formatZH": "容器重命名 [name] => [newName]", + "formatEN": "rename container [name] => [newName]" }, "/containers/repo": { "bodyKeys": [ @@ -831,8 +831,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", - "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" + "formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/auth/expired/reset": { "bodyKeys": [], @@ -1716,8 +1716,8 @@ "output_value": "name" } ], - "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", - "formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" + "formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/enterprise/users/del": { "bodyKeys": [ @@ -1918,8 +1918,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", - "formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]" + "formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]", + "formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]" }, "/core/settings/memo": { "bodyKeys": [], @@ -1948,8 +1948,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统端口 =\u003e [serverPort]", - "formatEN": "update system port =\u003e [serverPort]" + "formatZH": "修改系统端口 => [serverPort]", + "formatEN": "update system port => [serverPort]" }, "/core/settings/proxy/update": { "bodyKeys": [ @@ -1974,8 +1974,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统 ssl =\u003e [ssl]", - "formatEN": "update system ssl =\u003e [ssl]" + "formatZH": "修改系统 ssl => [ssl]", + "formatEN": "update system ssl => [ssl]" }, "/core/settings/terminal/update": { "bodyKeys": [], @@ -1991,8 +1991,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统配置 [key] =\u003e [value]", - "formatEN": "update system setting [key] =\u003e [value]" + "formatZH": "修改系统配置 [key] => [value]", + "formatEN": "update system setting [key] => [value]" }, "/core/settings/upgrade": { "bodyKeys": [ @@ -2000,8 +2000,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新系统 =\u003e [version]", - "formatEN": "upgrade system =\u003e [version]" + "formatZH": "更新系统 => [version]", + "formatEN": "upgrade system => [version]" }, "/core/xapp/appQRCode": { "bodyKeys": [ @@ -2888,8 +2888,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "首页应用 [key] =\u003e 显示:[value]", - "formatEN": "app launcher [key] =\u003e show: [value]" + "formatZH": "首页应用 [key] => 显示:[value]", + "formatEN": "app launcher [key] => show: [value]" }, "/dashboard/quick/change": { "bodyKeys": [], @@ -3017,7 +3017,7 @@ } ], "formatZH": "mysql 数据库 [name] 描述信息修改 [description]", - "formatEN": "The description of the mysql database [name] is modified =\u003e [description]" + "formatEN": "The description of the mysql database [name] is modified => [description]" }, "/databases/grants": { "bodyKeys": [ @@ -3098,7 +3098,7 @@ } ], "formatZH": "mongodb 数据库 [name] 描述信息修改 [description]", - "formatEN": "The description of the mongodb database [name] is modified =\u003e [description]" + "formatEN": "The description of the mongodb database [name] is modified => [description]" }, "/databases/mongodb/password": { "bodyKeys": [ @@ -3183,7 +3183,7 @@ } ], "formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", - "formatEN": "The description of the postgresql database [name] is modified =\u003e [description]" + "formatEN": "The description of the postgresql database [name] is modified => [description]" }, "/databases/pg/password": { "bodyKeys": [ @@ -3288,8 +3288,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]", - "formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]" + "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]", + "formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]" }, "/databases/variables/update": { "bodyKeys": [], @@ -3325,8 +3325,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]", - "formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]" + "formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]", + "formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]" }, "/files/chunkdownload": { "bodyKeys": [ @@ -3416,8 +3416,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改权限 [path] =\u003e [mode]", - "formatEN": "Change mode [path] =\u003e [mode]" + "formatZH": "修改权限 [path] => [mode]", + "formatEN": "Change mode [path] => [mode]" }, "/files/move": { "bodyKeys": [ @@ -3426,8 +3426,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "移动文件 [oldPaths] =\u003e [newPath]", - "formatEN": "Move [oldPaths] =\u003e [newPath]" + "formatZH": "移动文件 [oldPaths] => [newPath]", + "formatEN": "Move [oldPaths] => [newPath]" }, "/files/owner": { "bodyKeys": [ @@ -3437,8 +3437,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改用户/组 [path] =\u003e [user]/[group]", - "formatEN": "Change owner [path] =\u003e [user]/[group]" + "formatZH": "修改用户/组 [path] => [user]/[group]", + "formatEN": "Change owner [path] => [user]/[group]" }, "/files/preview": { "bodyKeys": [ @@ -3472,8 +3472,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "重命名 [oldName] =\u003e [newName]", - "formatEN": "Rename [oldName] =\u003e [newName]" + "formatZH": "重命名 [oldName] => [newName]", + "formatEN": "Rename [oldName] => [newName]" }, "/files/save": { "bodyKeys": [ @@ -3530,8 +3530,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "下载 url =\u003e [path]/[name]", - "formatEN": "Download url =\u003e [path]/[name]" + "formatZH": "下载 url => [path]/[name]", + "formatEN": "Download url => [path]/[name]" }, "/files/wget/process/remove": { "bodyKeys": [ @@ -3681,12 +3681,16 @@ "formatZH": "删除 Docker 端口防护策略 [uuids]", "formatEN": "delete Docker port guard policies [uuids]" }, - "/hosts/firewall/docker/sync": { - "bodyKeys": [], + "/hosts/firewall/family/operate": { + "bodyKeys": [ + "subsystem", + "family", + "operation" + ], + "formatEN": "[operation] [subsystem] [family] firewall chains", + "formatZH": "[operation] [subsystem] [family] 防火墙链", "paramKeys": [], - "beforeFunctions": [], - "formatZH": "同步 Docker 端口防护规则", - "formatEN": "sync Docker port guard rules" + "beforeFunctions": [] }, "/hosts/firewall/filter/operate": { "bodyKeys": [ @@ -3727,13 +3731,6 @@ "formatZH": "添加防火墙规则", "formatEN": "create firewall rules" }, - "/hosts/firewall/rules/adopt": { - "bodyKeys": [], - "paramKeys": [], - "beforeFunctions": [], - "formatZH": "纳管防火墙规则", - "formatEN": "adopt firewall rule" - }, "/hosts/firewall/rules/delete": { "bodyKeys": [], "paramKeys": [], @@ -3743,12 +3740,12 @@ }, "/hosts/firewall/rules/reorder": { "bodyKeys": [ - "uuid" + "instanceKey" ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "调整防火墙规则顺序 [uuid]", - "formatEN": "reorder firewall rule [uuid]" + "formatZH": "调整防火墙规则顺序 [instanceKey]", + "formatEN": "reorder firewall rule [instanceKey]" }, "/hosts/firewall/rules/reset": { "bodyKeys": [], @@ -3757,25 +3754,23 @@ "formatZH": "重置防火墙规则", "formatEN": "reset firewall rules" }, - "/hosts/firewall/rules/sync": { - "bodyKeys": [ - "subsystem", - "sourceProvider", - "targetProvider" - ], - "paramKeys": [], - "beforeFunctions": [], - "formatZH": "同步 [subsystem] 防火墙规则到 [targetProvider]", - "formatEN": "sync [subsystem] firewall rules to [targetProvider]" - }, "/hosts/firewall/rules/update": { "bodyKeys": [ - "uuid" + "instanceKey" ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新防火墙规则 [uuid]", - "formatEN": "update firewall rule [uuid]" + "formatZH": "更新防火墙规则 [instanceKey]", + "formatEN": "update firewall rule [instanceKey]" + }, + "/hosts/firewall/settings/ipv6": { + "bodyKeys": [ + "status" + ], + "formatEN": "Set firewall IPv6 support to [status]", + "formatZH": "设置防火墙 IPv6 支持为 [status]", + "paramKeys": [], + "beforeFunctions": [] }, "/hosts/firewall/settings/operate": { "bodyKeys": [ @@ -3893,8 +3888,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改 SSH 配置 [key] =\u003e [newValue]", - "formatEN": "update SSH setting [key] =\u003e [newValue]" + "formatZH": "修改 SSH 配置 [key] => [newValue]", + "formatEN": "update SSH setting [key] => [newValue]" }, "/hosts/tool/config/set": { "bodyKeys": [ @@ -4155,7 +4150,7 @@ } ], "formatZH": "快照 [name] 描述信息修改 [description]", - "formatEN": "The description of the snapshot [name] is modified =\u003e [description]" + "formatEN": "The description of the snapshot [name] is modified => [description]" }, "/settings/snapshot/import": { "bodyKeys": [ @@ -4247,8 +4242,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改系统配置 [key] =\u003e [value]", - "formatEN": "update system setting [key] =\u003e [value]" + "formatZH": "修改系统配置 [key] => [value]", + "formatEN": "update system setting [key] => [value]" }, "/toolbox/clam": { "bodyKeys": [ @@ -4366,8 +4361,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改主机参数 [key] =\u003e [value]", - "formatEN": "update device conf [key] =\u003e [value]" + "formatZH": "修改主机参数 [key] => [value]", + "formatEN": "update device conf [key] => [value]" }, "/toolbox/device/update/host": { "bodyKeys": [ @@ -4376,8 +4371,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改主机 Host [key] =\u003e [value]", - "formatEN": "update device host [key] =\u003e [value]" + "formatZH": "修改主机 Host [key] => [value]", + "formatEN": "update device host [key] => [value]" }, "/toolbox/device/update/swap": { "bodyKeys": [ @@ -4405,8 +4400,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]", - "formatEN": "update fail2ban conf [key] =\u003e [value]" + "formatZH": "修改 Fail2ban 配置 [key] => [value]", + "formatEN": "update fail2ban conf [key] => [value]" }, "/toolbox/ftp": { "bodyKeys": [ @@ -4665,8 +4660,8 @@ "output_value": "domain" } ], - "formatZH": "修改默认 server =\u003e [domain]", - "formatEN": "Change default server =\u003e [domain]" + "formatZH": "修改默认 server => [domain]", + "formatEN": "Change default server => [domain]" }, "/websites/del": { "bodyKeys": [ @@ -5429,8 +5424,8 @@ ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新防篡改信息 [website][path] =\u003e [status]", - "formatEN": "update tamper info [website][path] =\u003e [status]" + "formatZH": "更新防篡改信息 [website][path] => [status]", + "formatEN": "update tamper info [website][path] => [status]" }, "/xpack/vllm/command-template/create": { "bodyKeys": [ diff --git a/core/middleware/demo_handle.go b/core/middleware/demo_handle.go index 3705a4d0b..14f49bd30 100644 --- a/core/middleware/demo_handle.go +++ b/core/middleware/demo_handle.go @@ -30,7 +30,6 @@ var demoAllowedRoutes = map[demoRoute]struct{}{ {http.MethodPost, "/api/v2/hosts/firewall/base"}: {}, {http.MethodPost, "/api/v2/hosts/firewall/forward/base"}: {}, {http.MethodPost, "/api/v2/hosts/firewall/rules/native/detail"}: {}, - {http.MethodPost, "/api/v2/hosts/firewall/rules/sync/preview"}: {}, {http.MethodPost, "/api/v2/core/auth/login"}: {}, {http.MethodPost, "/api/v2/core/logs/login"}: {}, diff --git a/frontend/src/api/interface/firewall.ts b/frontend/src/api/interface/firewall.ts index 9152d0a58..3d0bc8be2 100644 --- a/frontend/src/api/interface/firewall.ts +++ b/frontend/src/api/interface/firewall.ts @@ -18,6 +18,7 @@ export namespace Firewall { ipv6: BackendFamilyStatus; } export interface BackendFamilyStatus { + partial?: boolean; available: boolean; initialized: boolean; bound: boolean; @@ -31,6 +32,7 @@ export namespace Firewall { options: BackendOption[]; } export interface Settings { + ipv6Enabled?: boolean; system: BackendGroup; forwarding: BackendGroup; docker: BackendGroup; @@ -60,6 +62,7 @@ export namespace Firewall { queued?: boolean; } export interface FirewallBase { + ipv6Enabled?: boolean; lifecycleTaskID?: string; name: string; backend: string; @@ -72,7 +75,6 @@ export namespace Firewall { pingStatus: string; message?: string; reason?: string; - syncError?: string; ipv4: BackendFamilyStatus; ipv6: BackendFamilyStatus; } @@ -102,9 +104,6 @@ export namespace Firewall { targetIP: string; targetPort: string; interface: string; - isDesired?: boolean; - isRuntime?: boolean; - syncStatus?: 'converged' | 'missing' | 'runtime_only'; } export type Family = 'ipv4' | 'ipv6' | 'inet'; export type Direction = 'input'; @@ -124,6 +123,8 @@ export namespace Firewall { } export interface Rule { + raw?: string; + parseStatus?: ParseStatus; uuid?: string; scope: Scope; nativeKind?: NativeKind; @@ -161,41 +162,21 @@ export namespace Firewall { persistence?: PersistenceStatus; } - export type RuleOrigin = 'created' | 'adopted'; - export type InventoryState = 'managed' | 'adopted' | 'external' | 'drifted' | 'protected'; - export type InventoryMatch = 'none' | 'exact' | 'changed' | 'missing' | 'ambiguous' | 'opaque'; - - export interface DesiredRule { - uuid: string; - rule: Rule; - ruleKey: string; - origin: RuleOrigin; - protected?: boolean; - expanded?: boolean; - marker?: string; - observedInstanceKey?: string; - } - export interface PositionRange { min: number; max: number; } export interface InventoryItem { - incompatible?: boolean; - error?: string; rule: Rule; - observed?: ObservedRule; - desired?: DesiredRule; - state: InventoryState; - match: InventoryMatch; + observed: ObservedRule; + isWhitelist: boolean; + descriptionID: string; } export type ScopeNoticeCode = | 'family_unavailable' - | 'default_scope_mismatch' | 'managed_scope_inactive' - | 'unmanaged_active_scopes' | 'runtime_permanent_mismatch' | 'default_policy' | 'managed_scope_missing'; @@ -210,29 +191,34 @@ export namespace Firewall { ipv6Range: PositionRange; total: number; allTotal: number; - managedTotal: number; items: InventoryItem[]; notices?: ScopeNotice[]; } export interface ResetResponse { + backupPath: string; removed: number; disabled: boolean; } + export interface RuleBackups { + directory: string; + files: Array<{ name: string; provider: Provider; ruleCount: number; modifiedAt: number }>; + } + export interface ResetRequest { + subsystem?: BackendSubsystem; + backup?: boolean; provider?: Provider; withDockerRestart?: boolean; } export interface InventoryRequest extends ReqPage { - refresh?: boolean; scopes: Scope[]; all?: boolean; info: string; families?: Array<'ipv4' | 'ipv6'>; actions?: Array<'accept' | 'deny'>; - states?: InventoryState[]; excludeChains?: string[]; } @@ -243,20 +229,16 @@ export namespace Firewall { permanent: boolean; } - export interface AdoptRequest { - scope: Scope; - instanceKey?: string; - rule?: Rule; - marker?: string; - } - export interface CreateItem { + raw?: string; + parseStatus?: ParseStatus; rule: Rule; sourceKind?: 'user' | 'panel' | 'security' | 'imported'; - sourceID?: string; } export interface CreateRequest { + backupFile?: string; + initialize?: boolean; items: CreateItem[]; } @@ -276,68 +258,13 @@ export namespace Firewall { error?: string; } - export interface RuleSyncRequest { - subsystem: BackendSubsystem; - sourceProvider?: Provider; - targetProvider: Provider; - resetSource?: boolean; - taskID?: string; - } - - export type RuleSyncStatus = 'ready' | 'existing' | 'remove' | 'blocked'; - - export interface RuleSyncItem { - sourceUUID: string; - rule?: Rule; - forwardRule?: RuleForward; - dockerRule?: DockerGuardEndpoint; - status: RuleSyncStatus; - reasonCode?: string; - reason?: string; - } - - export interface RuleSyncPreview { - subsystem: BackendSubsystem; - sourceProvider?: Provider; - targetProvider: Provider; - total: number; - ready: number; - existing: number; - removed: number; - blocked: number; - items: RuleSyncItem[]; - } - - export interface RuleSyncResult { - subsystem: BackendSubsystem; - sourceProvider?: Provider; - targetProvider: Provider; - total: number; - succeeded: number; - skipped: number; - removed: number; - failed: number; - errors?: RuleSyncFailure[]; - taskID?: string; - queued?: boolean; - } - - export interface RuleSyncTask { - taskID?: string; - executing: boolean; - } - - export interface RuleSyncFailure { - sourceUUID: string; - rule?: Rule; - forwardRule?: RuleForward; - dockerRule?: DockerGuardEndpoint; - error: string; + export interface RuleTarget { + scope: Scope; + instanceKey: string; } export interface DeleteRequest { - uuids: string[]; - beforeRules?: { scope: Scope; instanceKey: string }[]; + targets: (RuleTarget & { observed: ObservedRule })[]; } export interface DeleteResponse { @@ -350,7 +277,7 @@ export namespace Firewall { export interface DeleteFailure { index: number; - uuid: string; + instanceKey: string; error: string; } @@ -361,6 +288,7 @@ export namespace Firewall { | { rule?: never; description?: string; orderIndex?: never; priority: number }; export interface DockerGuardBase { + ipv6Enabled?: boolean; name: string; version: string; isExist: boolean; @@ -372,6 +300,7 @@ export namespace Firewall { message?: string; } export interface DockerGuardFamilyStatus { + partial?: boolean; state: 'effective' | 'disabled' | 'not_effective'; reason?: | 'command_missing' @@ -397,12 +326,9 @@ export namespace Firewall { compose?: string; application?: string; policyUUID?: string; - mode?: 'deny_sources' | 'allow_sources' | 'deny_all'; - nativeAction?: string; - readOnly?: boolean; + mode?: 'deny_sources' | 'allow_sources' | 'deny_all' | 'accept_sources' | 'accept_all'; sources: string[]; effective: boolean; - description?: string; trafficPath: 'forward' | 'input' | 'unknown'; managementTarget?: 'container_guard' | 'host_firewall' | 'needs_diagnosis'; managementReason?: 'nat_inspect_failed' | 'nat_chain_unreachable' | 'proxy_inspect_failed' | 'no_matching_path'; @@ -433,12 +359,12 @@ export namespace Firewall { protocol: 'tcp' | 'udp'; } export interface DockerGuardPolicy extends DockerGuardEndpointIdentity { - mode: 'deny_sources' | 'allow_sources' | 'deny_all'; + mode: 'deny_sources' | 'allow_sources' | 'deny_all' | 'accept_sources' | 'accept_all'; sources: string[]; - description: string; } export interface DockerGuardPolicyBatch { policies: DockerGuardPolicy[]; + import?: boolean; } export interface DockerGuardPolicyBatchDelete { uuids: string[]; diff --git a/frontend/src/api/modules/firewall.ts b/frontend/src/api/modules/firewall.ts index 27848e494..106393c42 100644 --- a/frontend/src/api/modules/firewall.ts +++ b/frontend/src/api/modules/firewall.ts @@ -28,15 +28,15 @@ export const operateFire = (operation: string, withDockerRestart: boolean) => TimeoutEnum.T_10M, ); -export const operateForwardRule = (request: { rules: Firewall.RuleForward[]; forceDelete?: boolean }) => +export const operateForwardRule = (request: { rules: Firewall.RuleForward[]; import?: boolean }) => http.postWithConfig('/hosts/firewall/forward/operate', request, { skipErrorMessage: true, }); -export const enableForwarding = (taskID?: string) => +export const enableForwarding = (taskID?: string, backupFile?: string) => http.post( '/hosts/firewall/forward/enable', - taskID ? { taskID } : {}, + { taskID, backupFile }, TimeoutEnum.T_60S, ); @@ -51,6 +51,9 @@ export const searchFirewallRules = (request: Firewall.InventoryRequest) => { return http.post('/hosts/firewall/rules/search', request, TimeoutEnum.T_40S); }; +export const listFirewallRuleBackups = (subsystem: Firewall.BackendSubsystem = 'system') => + http.get('/hosts/firewall/rules/backups', { subsystem }); + export const resetFirewallRules = (request: Firewall.ResetRequest) => { return http.post('/hosts/firewall/rules/reset', request, TimeoutEnum.T_10M); }; @@ -59,32 +62,16 @@ export const loadFirewallNativeDetail = (request: Firewall.NativeDetailRequest) return http.post('/hosts/firewall/rules/native/detail', request, TimeoutEnum.T_40S); }; -export const adoptFirewallRule = (request: Firewall.AdoptRequest) => { - return http.post('/hosts/firewall/rules/adopt', request, TimeoutEnum.T_3M); -}; - export const createFirewallRules = (request: Firewall.CreateRequest) => { return http.post('/hosts/firewall/rules', request, TimeoutEnum.T_10M); }; -export const previewFirewallRuleSync = (request: Firewall.RuleSyncRequest) => { - return http.post('/hosts/firewall/rules/sync/preview', request, TimeoutEnum.T_3M); -}; - -export const loadFirewallRuleSyncTask = () => { - return http.get('/hosts/firewall/rules/sync/task'); -}; - -export const syncFirewallRules = (request: Firewall.RuleSyncRequest) => { - return http.post('/hosts/firewall/rules/sync', request, TimeoutEnum.T_10M); -}; - export const deleteFirewallRules = (request: Firewall.DeleteRequest) => { return http.post('/hosts/firewall/rules/delete', request); }; -export const updateFirewallRule = (uuid: string, request: Firewall.UpdateRequest) => { - return http.post('/hosts/firewall/rules/update', { ...request, uuid }, TimeoutEnum.T_60S); +export const updateFirewallRule = (target: Firewall.RuleTarget, request: Firewall.UpdateRequest) => { + return http.post('/hosts/firewall/rules/update', { ...request, ...target }, TimeoutEnum.T_60S); }; export const loadDockerPortGuard = () => @@ -93,10 +80,14 @@ export const loadDockerPortGuard = () => export const loadDockerPublishedPorts = () => http.get('/hosts/firewall/docker/endpoints', {}, { timeout: TimeoutEnum.T_40S }); -export const operateDockerPortGuard = (operation: 'initialize' | 'bind' | 'unbind', taskID?: string) => +export const operateDockerPortGuard = ( + operation: 'initialize' | 'bind' | 'unbind', + taskID?: string, + backupFile?: string, +) => http.postWithConfig( '/hosts/firewall/docker/operate', - { operation, ...(taskID ? { taskID } : {}) }, + { operation, taskID, backupFile }, { timeout: TimeoutEnum.T_60S, skipErrorMessage: true, @@ -121,3 +112,15 @@ export const operateFirewallBackend = (request: Firewall.BackendOperateRequest, timeout: TimeoutEnum.T_10M, skipErrorMessage, }); + +export const operateFirewallFamily = (request: { + subsystem: Firewall.BackendSubsystem; + backend: Firewall.Provider; + family: 'ipv4' | 'ipv6'; + operation: 'initialize' | 'repair' | 'bind'; +}) => http.post('/hosts/firewall/family/operate', request, TimeoutEnum.T_60S); + +export const operateFirewallIPv6 = (enabled: boolean) => + http.post('/hosts/firewall/settings/ipv6', { + status: enabled ? 'Enable' : 'Disable', + }); diff --git a/frontend/src/components/confirm-dialog/index.vue b/frontend/src/components/confirm-dialog/index.vue index 8d7b426c8..66a812f22 100644 --- a/frontend/src/components/confirm-dialog/index.vue +++ b/frontend/src/components/confirm-dialog/index.vue @@ -2,6 +2,7 @@
{{ operationInfo }} +
{{ $t('commons.msg.operateConfirm') }} '{{ submitInputInfo }}' diff --git a/frontend/src/components/terminal/index.vue b/frontend/src/components/terminal/index.vue index 7c2a09c89..d485be755 100644 --- a/frontend/src/components/terminal/index.vue +++ b/frontend/src/components/terminal/index.vue @@ -522,17 +522,18 @@ const scheduleReconnect = (forRevalidation = false) => { writeNotice('33', i18n.global.t('terminal.sessionReconnecting')); reconnectNoticeShown = true; } - reconnectTimer = setTimeout( - () => { - reconnectTimer = null; - if (closing || !sessionId.value) return; + if (forRevalidation) { + if (!closing && sessionId.value) { initWebSocket(wsEndpoint, wsArgs); - }, - forRevalidation ? 0 : reconnectDelay, - ); - if (!forRevalidation) { - reconnectDelay = Math.min(reconnectDelay * 2, 8000); + } + return; } + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + if (closing || !sessionId.value) return; + initWebSocket(wsEndpoint, wsArgs); + }, reconnectDelay); + reconnectDelay = Math.min(reconnectDelay * 2, 8000); }; const stopReconnect = () => { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 346022374..43a98db9c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -4250,45 +4250,41 @@ const message = { useEAB: 'Use EAB authentication', }, firewall: { + ipv6Support: 'IPv6 support', + ipv6SupportHelper: + 'Controls the management and enforcement of IPv6 firewall rules without affecting IPv6 networking on the server.', + ipv6DisableContainerHelper: 'Hide IPv6 port information for containers.', + ipv6DisableWhitelistHelper: 'Remove IPv6 rules from the allowlist and retain all other existing IPv6 rules.', + ipv6DisableRulesHelper: 'Disable IPv6 rules managed by 1Panel in iptables and nftables.', + ipv6DisableNetworkHelper: 'This does not disable IPv6 networking on the server.', + ipv6DisableEffectHelper: + 'The affected IPv6 access restrictions and port forwarding will no longer take effect.', + ipv6Disabled: 'IPv6 support is disabled. Enable it in firewall settings first.', + familyRepair: 'Repair', + familyIncomplete: '{0} chains are incomplete', + resetWithBackup: 'Back up rules and reset', + resetOnly: 'Reset only', + resetWithBackupHelper: 'Save the current module rules as JSON for restoration during initialization.', + resetOnlyHelper: 'Clear current rules without creating a new backup. Existing backups are kept.', + backupDirectoryHelper: 'Valid rule backups in {0} are listed below. The newest file is selected by default.', + restoreAllRules: 'Initialize and restore rules', + importRuleBackup: 'Import from backup', + initializeOnly: 'Initialize only', + backupRuleCount: 'Rule count', + noRuleBackup: 'No restorable rule backups found', + ruleBackupLoadFailed: 'Failed to load rule backups. Retry or choose initialization without restoring a backup.', create: 'Create rule', edit: 'Edit rule', quickJump: 'Quick access', used: 'Used', unUsed: 'Unused', - managed: 'Panel-created', - managedHelper: 'Created and managed by 1Panel.', - adopted: 'Externally managed', - adoptedHelper: 'An existing system rule now managed by 1Panel.', - external: 'External rule', - externalHelper: 'Not managed by 1Panel.', - protected: 'System-protected', - protectedHelper: 'Built-in rules and rules that exactly match the whitelist cannot be edited or deleted.', - stateShort: { - managed: 'Panel-created', - adopted: 'Externally managed', - external: 'External rule', - protected: 'System-protected', - drifted: 'Abnormal', - }, + whitelist: 'Whitelist', + builtinRuleProtected: 'Built-in system protection rule. It cannot be edited or deleted.', + whitelistRuleProtected: + 'Whitelist rules allow only order and description changes. For other changes, go to Settings → Port whitelist.', ruleTargetRequired: 'Enter at least one IP address or port', - 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.', - plan_managed_rule_drifted: - 'The 1Panel management record differs from the live firewall rule. Check the system rule.', - plan_opaque_rule_in_target_scope: - 'The target scope contains a rule that cannot be parsed safely. The operation was stopped.', - plan_runtime_permanent_mismatch: - 'The runtime and permanent firewall configurations differ. Synchronize them first.', - plan_protected_rule: 'This rule is protected and cannot be taken over, changed, or deleted.', - plan_blocked: 'This rule cannot be applied safely. Refresh the rules and try again.', largeRuleSet: 'Too many firewall rules may slow down operations. We recommend using iptables or nftables.', - scopeDefaultMismatch: 'The system default zone is {0}; this page manages only the public zone.', scopeMissing: 'Managed scope {0} is missing and will be created safely when the first rule is applied.', - scopeUnmanagedActive: 'Other active scopes were detected: {0}. 1Panel will not modify their rules.', dockerRestart: 'Firewall operations require restarting the Docker service', firewallHelper: '{0} system firewall', firewallNotStart: 'The system firewall is not enabled at present. Enable it first.', @@ -4311,7 +4307,7 @@ const message = { systemFirewall: 'Host firewall', systemFirewallHelper: 'Controls host port access and inbound firewall rules.', forwardPolicyDropWarning: - 'The default FORWARD policy for {0} is DROP. Traffic forwarded to other hosts may be blocked unless explicitly allowed by iptables/ip6tables rules. Check the allow rules for the corresponding IP version.', + 'This server restricts network forwarding. Port forwarding rules may not take effect.', forwardingHelper: 'Manages port-forwarding rules.', dockerFirewallHelper: 'Selects how 1Panel manages container port protection.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, experimental', @@ -4320,6 +4316,8 @@ const message = { addressFamily: 'IP version', portOrRange: 'Port / range', batchLimit: 'Create or import at most {0} rules per batch (after expansion).', + importDuplicatesRemoved: + 'Removed {0} duplicate rules, including duplicates within the file and rules that already exist.', importLimit: 'Import up to {0} rules (counted after expansion), with a file size of at most {1} KB.', ruleSyncTitle: 'Synchronize rules', ruleSyncAction: 'Sync rules', @@ -4337,19 +4335,9 @@ const message = { ruleSyncBlocked: 'Unavailable', ruleSyncReason: 'Check result', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'The rule already matches the database policy.', - 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.', 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.', onlyInTarget: 'The rule exists only in the target firewall.', - stale: 'The firewall rule state is outdated. Refresh and try again.', - protectedRule: 'This protected firewall rule cannot be modified.', - dockerAcceptReadOnly: - 'This ACCEPT rule is read-only and will be preserved while other rules are synchronized. To remove it, delete it manually on the host.', - cannotReconcile: 'The target rule cannot be reconciled: {0}', }, ruleSyncPartial: 'Synchronization completed: {0} succeeded, {1} already existed, and {2} failed.', ruleSyncSuccess: 'Synchronization completed: {0} succeeded, {1} already existed, and {2} were removed.', @@ -4359,17 +4347,18 @@ const message = { remove: 'To remove', blocked: 'Unavailable', }, - resetDirectRulesHelper: - 'Delete all 1Panel system firewall chains, runtime rules, and persisted files from {0}; saved database policies are retained', - resetWhitelistRulesHelper: - 'Reset the active custom firewall configuration in {0}, restore installation defaults, and disable {0}; saved database policies are retained and can be synchronized again.', + initializeFromFile: 'Initialize from file', + downloadRuleBackup: 'Download rule backup', + importFileHelper: + 'Import JSON files up to 64 MB in file order, in batches. Rules that cannot be converted are reported as errors.', + resetDirectRulesHelper: 'Delete all 1Panel host firewall chains, runtime rules and persistence files in {0}.', + resetWhitelistRulesHelper: 'Reset custom configuration in {0}, restore defaults and disable it.', cleanupForwardingBackendHelper: - 'Reset the 1Panel port forwarding runtime rules in {0}: delete all related rules and chains while retaining database data', + 'Delete all 1Panel forwarding rules, chains, and automatic recovery files from {0}. Existing JSON backups are kept.', cleanupDockerBackendHelper: - 'Reset the 1Panel Docker port protection runtime rules in {0}: delete all related rules and chains while retaining database data', + 'Delete all 1Panel Docker protection rules, chains, and automatic recovery files from {0}. Existing JSON backups are kept.', cleanupBeforeBackendSwitch: - 'The current {0} backend still contains 1Panel runtime rules. Reset it before switching to {1}. Resetting only removes runtime rules; saved database policies are retained and can be initialized or synchronized after switching.', - cleanupAction: 'Reset', + 'Reset the current {0} before switching to {1}, and confirm how its rules will be restored.', backendSwitchNotice: 'Use only one firewall management method at a time. Running multiple firewalls simultaneously may cause rule conflicts, inconsistent status, or container port access failures.', switchBackendHelper: 'Switch to {0}?', @@ -4417,8 +4406,7 @@ const message = { portDetails: 'Port details', orphanEndpoints: 'Unassociated endpoints', orphanPolicies: 'Unassociated rules', - orphanPoliciesHelper: - 'The following {0} rule(s) are not bound to a Docker port. They will be bound automatically when a matching port is detected.', + orphanPoliciesHelper: 'These {0} system rules have no matching published Docker port.', composeOrApp: 'Compose / App', sources: 'Sources', deniedSources: 'Denied sources', @@ -4427,6 +4415,8 @@ const message = { protectionMode: 'Protection mode', denySources: 'Deny specified sources', allowSources: 'Allow specified sources only', + acceptSources: 'Accept specified sources directly', + acceptAll: 'Accept all traffic directly', denyAll: 'Deny all access', dockerGuardMixedFamilyHelper: 'IPv4 and IPv6 are both selected. Configure source-based policies separately; deny all can be applied directly.', @@ -4470,7 +4460,7 @@ const message = { 'Unbind - When unbound, all added firewall rules will become invalid. Proceed with caution. Confirm?', portWhiteList: 'Port allowlist', whitelistConfigHelper: - 'The allowlist only saves configuration and protects matching allow rules. Missing rules are added on start, restart, initialization, or synchronization. Editing or deleting allowlist entries does not remove existing rules.', + 'Saving the allowlist adds missing allow rules and protects matching rules. Editing or deleting allowlist entries does not remove existing rules.', whitelistDeleteConfirm: 'Only this whitelist entry will be deleted. Existing firewall rules will remain. Continue?', portWhiteListHelper: @@ -4488,7 +4478,7 @@ const message = { whitelistServicePortsHelper: 'Enter a single port (1-65535). This does not change the service listening port. Changing the port in the service settings also updates this whitelist entry.', whitelistSourcesHelper: - 'Enter IP addresses or CIDR ranges, separated by commas or new lines. Leave blank to allow all IPv4 and IPv6 sources.', + 'Enter IP addresses or CIDR ranges, separated by commas or new lines. Leave blank to allow {0}.', destinationPortPlaceholder: 'e.g. 80, 80,443, or 8080-8089', deleteRuleConfirm: 'Will delete {0} rules. Continue?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 1cd22b4f5..87b0e7b90 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -4295,48 +4295,46 @@ const message = { useEAB: 'Usar autenticación EAB', }, firewall: { + ipv6Support: 'Compatibilidad con IPv6', + ipv6SupportHelper: + 'Controla la gestión y aplicación de las reglas IPv6 del cortafuegos sin afectar a la conectividad IPv6 del servidor.', + ipv6DisableContainerHelper: 'Ocultar la información de los puertos IPv6 de los contenedores.', + ipv6DisableWhitelistHelper: + 'Eliminar las reglas IPv6 de la lista de permitidos y conservar las demás reglas IPv6 existentes.', + ipv6DisableRulesHelper: 'Desactivar las reglas IPv6 de iptables y nftables gestionadas por 1Panel.', + ipv6DisableNetworkHelper: 'Esto no desactiva la conectividad IPv6 del servidor.', + ipv6DisableEffectHelper: + 'Las restricciones de acceso IPv6 y el reenvío de puertos afectados dejarán de aplicarse.', + ipv6Disabled: 'IPv6 está desactivado. Actívelo primero en la configuración del cortafuegos.', + familyRepair: 'Reparar', + familyIncomplete: 'Las cadenas de {0} están incompletas', + resetWithBackup: 'Guardar reglas y restablecer', + resetOnly: 'Solo restablecer', + resetWithBackupHelper: + 'Guarda las reglas del módulo actual en JSON para restaurarlas durante la inicialización.', + resetOnlyHelper: 'Borrar las reglas actuales sin crear una copia nueva. Se conservan las copias existentes.', + backupDirectoryHelper: + 'Se muestran las copias válidas de reglas en {0}. Se selecciona la más reciente por defecto.', + restoreAllRules: 'Inicializar y restaurar reglas', + importRuleBackup: 'Importar desde copia de seguridad', + initializeOnly: 'Solo inicializar', + backupRuleCount: 'Número de reglas', + noRuleBackup: 'No hay copias de reglas para restaurar', + ruleBackupLoadFailed: + 'No se pudieron cargar las copias de las reglas. Reintenta o inicializa sin restaurar una copia.', create: 'Crear regla', edit: 'Editar regla', quickJump: 'Acceso rápido', used: 'En uso', unUsed: 'No usado', - managed: 'Creada por el panel', - managedHelper: 'Creada y mantenida por 1Panel. Se puede editar o eliminar.', - adopted: 'Externa gestionada', - adoptedHelper: 'Una regla existente del sistema que ahora mantiene 1Panel.', - external: 'Regla externa', - externalHelper: - 'Regla existente del sistema que aún no gestiona 1Panel. Se puede adoptar cuando sea necesario.', - protected: 'Protegida por el sistema', - protectedHelper: - 'Las reglas integradas y las que coinciden exactamente con la lista blanca no se pueden editar ni eliminar.', - stateShort: { - managed: 'Panel', - adopted: 'Gestionada', - external: 'Externa', - protected: 'Protegida', - drifted: 'Desviada', - }, + whitelist: 'Lista de permitidos', + builtinRuleProtected: 'Regla de protección integrada del sistema. No se puede editar ni eliminar.', + whitelistRuleProtected: + 'Solo se pueden cambiar el orden y la descripción de las reglas permitidas. Para otros cambios, vaya a Configuración → Lista de puertos permitidos.', ruleTargetRequired: 'Introduce al menos una dirección IP o un puerto', - 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.', - adoptRuleConfirm: 'Después de asumirla, 1Panel podrá mantener y eliminar esta regla existente. ¿Continuar?', - 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.', - plan_managed_rule_drifted: - 'La regla gestionada no coincide con el firewall activo. Resuelve primero la diferencia.', - plan_opaque_rule_in_target_scope: - 'El ámbito contiene una regla que no puede analizarse con seguridad. La operación se detuvo.', - plan_runtime_permanent_mismatch: 'Las configuraciones activa y permanente difieren. Sincronízalas primero.', - plan_protected_rule: 'Esta regla está protegida y no se puede asumir, modificar ni eliminar.', - plan_blocked: 'La regla no puede aplicarse de forma segura. Actualiza la lista e inténtalo de nuevo.', largeRuleSet: 'Demasiadas reglas de cortafuegos pueden ralentizar las operaciones. Se recomienda usar iptables o nftables.', - scopeDefaultMismatch: 'La zona predeterminada es {0}; esta página solo gestiona la zona public.', scopeMissing: 'Falta el ámbito gestionado {0}; se creará de forma segura al aplicar la primera regla.', - scopeUnmanagedActive: 'Se detectaron otros ámbitos activos: {0}. 1Panel no modificará sus reglas.', dockerRestart: 'Las operaciones del firewall requieren reiniciar el servicio de Docker', firewallHelper: 'Firewall del sistema {0}', firewallNotStart: 'El firewall del sistema no está habilitado actualmente. Actívalo primero.', @@ -4360,7 +4358,7 @@ const message = { systemFirewall: 'Firewall del host', systemFirewallHelper: 'Controla el acceso a los puertos del host y las reglas de entrada.', forwardPolicyDropWarning: - 'La política predeterminada de FORWARD para {0} es DROP. El tráfico reenviado a otros hosts puede bloquearse si las reglas de iptables/ip6tables no lo permiten explícitamente. Revise las reglas de permiso de la versión IP correspondiente.', + 'Este servidor restringe el reenvío de red. Es posible que las reglas de reenvío de puertos no surtan efecto.', forwardingHelper: 'Gestiona las reglas de reenvío de puertos.', dockerFirewallHelper: 'Selecciona cómo gestiona 1Panel la protección de puertos de contenedores.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, experimental', @@ -4369,6 +4367,8 @@ const message = { addressFamily: 'Versión de IP', portOrRange: 'Puerto / rango', batchLimit: 'Puede crear o importar hasta {0} reglas por lote, después de expandirlas.', + importDuplicatesRemoved: + 'Se eliminaron {0} reglas duplicadas, incluidas las repetidas en el archivo y las que ya existen.', importLimit: 'Importe hasta {0} reglas (contadas después de expandirlas), con un archivo de hasta {1} KB.', ruleSyncTitle: 'Sincronizar reglas', ruleSyncAction: 'Sincronizar reglas', @@ -4386,19 +4386,9 @@ const message = { ruleSyncBlocked: 'No disponibles', ruleSyncReason: 'Resultado de comprobación', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'La regla ya coincide con la política de la base de datos.', - 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.', 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.', onlyInTarget: 'La regla solo existe en el firewall de destino.', - stale: 'El estado de la regla está desactualizado. Actualiza e inténtalo de nuevo.', - protectedRule: 'Esta regla protegida del firewall no se puede modificar.', - dockerAcceptReadOnly: - 'Esta regla ACCEPT es de solo lectura y se conservará al sincronizar las demás reglas. Para eliminarla, bórrela manualmente en el host.', - cannotReconcile: 'No se puede sincronizar la regla de destino: {0}', }, ruleSyncPartial: 'Sincronización completada: {0} correctas, {1} ya existían y {2} fallaron.', ruleSyncSuccess: 'Sincronización completada: {0} correctas, {1} ya existían y {2} eliminadas.', @@ -4408,17 +4398,20 @@ const message = { remove: 'Para eliminar', blocked: 'No disponible', }, + initializeFromFile: 'Inicializar desde archivo', + downloadRuleBackup: 'Descargar copia de reglas', + importFileHelper: + 'Importa archivos JSON de hasta 64 MB por lotes y en el orden del archivo. Las reglas que no se puedan convertir se indicarán como errores.', resetDirectRulesHelper: - 'Elimina de {0} las cadenas, reglas activas y archivos persistentes del firewall de 1Panel; se conservan las políticas de la base de datos', + 'Eliminar todas las cadenas del cortafuegos del host de 1Panel, reglas activas y archivos persistentes de {0}.', resetWhitelistRulesHelper: - 'Restablece la configuración personalizada activa en {0}, restaura los valores iniciales y desactiva {0}; las políticas de la base de datos se conservan y pueden volver a sincronizarse.', + 'Restablecer la configuración personalizada de {0}, restaurar los valores predeterminados y desactivarlo.', cleanupForwardingBackendHelper: - 'Restablece las reglas de ejecución del reenvío de puertos de 1Panel en {0}: elimina todas las reglas y cadenas relacionadas y conserva los datos de la base de datos', + 'Elimina de {0} todas las reglas y cadenas de reenvío de 1Panel y los archivos de recuperación automática. Se conservan las copias JSON.', cleanupDockerBackendHelper: - 'Restablece las reglas de ejecución de protección de puertos Docker de 1Panel en {0}: elimina todas las reglas y cadenas relacionadas y conserva los datos de la base de datos', + 'Elimina de {0} todas las reglas y cadenas de protección Docker de 1Panel y los archivos de recuperación automática. Se conservan las copias JSON.', cleanupBeforeBackendSwitch: - 'El backend actual {0} todavía contiene reglas de ejecución de 1Panel. Restablézcalo antes de cambiar a {1}. El restablecimiento solo elimina las reglas de ejecución; las políticas de la base de datos se conservan y pueden inicializarse o sincronizarse después del cambio.', - cleanupAction: 'Restablecer', + 'Restablece el {0} actual antes de cambiar a {1} y confirma cómo se restaurarán sus reglas.', backendSwitchNotice: 'Se recomienda utilizar un único método de gestión de firewall. Ejecutar varios firewalls al mismo tiempo puede causar conflictos de reglas, estados inconsistentes o problemas de acceso a los puertos de los contenedores.', switchBackendHelper: '¿Cambiar a {0}?', @@ -4467,8 +4460,7 @@ const message = { portDetails: 'Detalles del puerto', orphanEndpoints: 'Puntos de conexión sin asociar', orphanPolicies: 'Reglas sin asociar', - orphanPoliciesHelper: - 'Las siguientes {0} regla(s) no están vinculadas a ningún puerto Docker. Se vincularán automáticamente cuando se detecte un puerto coincidente.', + orphanPoliciesHelper: 'Estas {0} reglas del sistema no tienen un puerto publicado de Docker coincidente.', composeOrApp: 'Compose / Aplicación', sources: 'Orígenes', deniedSources: 'Orígenes denegados', @@ -4477,6 +4469,8 @@ const message = { protectionMode: 'Modo de protección', denySources: 'Denegar orígenes especificados', allowSources: 'Permitir solo los orígenes especificados', + acceptSources: 'Aceptar directamente los orígenes especificados', + acceptAll: 'Aceptar directamente todo el tráfico', denyAll: 'Denegar todo acceso', dockerGuardMixedFamilyHelper: 'Se han seleccionado IPv4 e IPv6. Configura por separado las políticas basadas en el origen; denegar todo puede aplicarse directamente.', @@ -4523,7 +4517,7 @@ const message = { 'Desvincular: al desvincular, todas las reglas de firewall agregadas se volverán inválidas. Proceda con precaución. ¿Confirmar?', portWhiteList: 'Lista blanca de puertos', whitelistConfigHelper: - 'La lista de permitidos solo guarda la configuración y protege las reglas de acceso coincidentes. Las reglas que falten se añaden al iniciar, reiniciar, inicializar o sincronizar. Editar o eliminar entradas no elimina las reglas existentes.', + 'Guardar la lista de permitidos añade las reglas de permiso que faltan y protege las coincidentes. Editar o eliminar entradas de la lista no elimina las reglas existentes.', whitelistDeleteConfirm: 'Solo se eliminará esta entrada de la lista blanca. Se conservarán las reglas existentes del firewall. ¿Continuar?', portWhiteListHelper: @@ -4541,7 +4535,7 @@ const message = { whitelistServicePortsHelper: 'Introduzca un único puerto (1-65535). Esto no cambia el puerto de escucha del servicio. Al cambiar el puerto en los ajustes del servicio, también se actualiza esta entrada de la lista blanca.', whitelistSourcesHelper: - 'Introduzca direcciones IP o rangos CIDR separados por comas o saltos de línea. Déjelo en blanco para permitir todos los orígenes IPv4 e IPv6.', + 'Introduzca direcciones IP o rangos CIDR separados por comas o saltos de línea. Déjelo en blanco para permitir {0}.', destinationPortPlaceholder: 'p. ej. 80, 80,443 o 8080-8089', deleteRuleConfirm: 'Se eliminarán {0} reglas. ¿Continuar?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index d8f87e0e6..008ee0ced 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -4213,38 +4213,43 @@ const message = { useEAB: 'استفاده از احراز هویت EAB', }, firewall: { + ipv6Support: 'پشتیبانی IPv6', + ipv6SupportHelper: + 'مدیریت و اعمال قوانین دیوار آتش IPv6 را کنترل می‌کند و بر عملکرد شبکه IPv6 سرور تأثیری ندارد.', + ipv6DisableContainerHelper: 'اطلاعات پورت IPv6 کانتینرها پنهان می‌شود.', + ipv6DisableWhitelistHelper: 'قوانین IPv6 از فهرست مجاز حذف می‌شوند و سایر قوانین IPv6 موجود حفظ می‌شوند.', + ipv6DisableRulesHelper: 'قوانین IPv6 در iptables و nftables تحت مدیریت 1Panel غیرفعال می‌شوند.', + ipv6DisableNetworkHelper: 'این عملیات شبکه IPv6 سرور را غیرفعال نمی‌کند.', + ipv6DisableEffectHelper: 'محدودیت‌های دسترسی IPv6 و هدایت پورت مربوطه دیگر اعمال نخواهند شد.', + ipv6Disabled: 'پشتیبانی IPv6 غیرفعال است. ابتدا آن را در تنظیمات دیوار آتش فعال کنید.', + familyRepair: 'ترمیم', + familyIncomplete: 'زنجیره‌های {0} ناقص هستند', + resetWithBackup: 'پشتیبان‌گیری قوانین و بازنشانی', + resetOnly: 'فقط بازنشانی', + resetWithBackupHelper: 'قوانین ماژول فعلی را به‌صورت JSON ذخیره کنید تا هنگام مقداردهی اولیه بازیابی شوند.', + resetOnlyHelper: 'قوانین فعلی بدون ایجاد نسخه پشتیبان جدید پاک می‌شوند. نسخه‌های پشتیبان قبلی حفظ می‌شوند.', + backupDirectoryHelper: + 'نسخه‌های پشتیبان معتبر قوانین در {0} نمایش داده می‌شوند. جدیدترین فایل به‌طور پیش‌فرض انتخاب می‌شود.', + restoreAllRules: 'مقداردهی اولیه و بازیابی قوانین', + importRuleBackup: 'ورود از نسخه پشتیبان', + initializeOnly: 'فقط راه‌اندازی', + backupRuleCount: 'تعداد قوانین', + noRuleBackup: 'نسخه پشتیبان قابل بازیابی قوانین یافت نشد', + ruleBackupLoadFailed: + 'بارگذاری نسخه‌های پشتیبان قوانین ناموفق بود. دوباره تلاش کنید یا بدون بازیابی نسخه پشتیبان، مقداردهی اولیه را انجام دهید.', create: 'ایجاد قانون', edit: 'ویرایش قانون', quickJump: 'دسترسی سریع', used: 'استفاده شده', unUsed: 'استفاده نشده', - managed: 'ایجادشده در پنل', - managedHelper: 'توسط 1Panel ایجاد و نگهداری می‌شود و قابل ویرایش یا حذف است.', - adopted: 'قانون خارجی تحت مدیریت', - adoptedHelper: 'قانونی که از قبل در سیستم وجود داشته و اکنون توسط 1Panel نگهداری می‌شود.', - external: 'قانون خارجی', - externalHelper: - 'قانونی موجود در سیستم که هنوز توسط 1Panel مدیریت نمی‌شود و در صورت نیاز می‌توان آن را تحت مدیریت قرار داد.', - protected: 'محافظت‌شده توسط سیستم', - protectedHelper: 'قوانین داخلی و قوانینی که دقیقاً با فهرست سفید مطابقت دارند قابل ویرایش یا حذف نیستند.', - stateShort: { managed: 'پنل', adopted: 'مدیریت', external: 'خارجی', protected: 'محافظت', drifted: 'ناهمگام' }, + whitelist: 'فهرست مجاز', + builtinRuleProtected: 'قانون حفاظتی داخلی سیستم است و قابل ویرایش یا حذف نیست.', + whitelistRuleProtected: + 'در قوانین فهرست مجاز فقط ترتیب و توضیحات قابل تغییر است. برای سایر تغییرات به تنظیمات → فهرست مجاز پورت‌ها بروید.', ruleTargetRequired: 'حداقل یک نشانی IP یا درگاه وارد کنید', - resolution_adopt: 'واگذاری مدیریت', - plan_duplicate_rules: - 'قوانین تکراری با شرایط و اقدامات یکسان قابل پذیرش برای مدیریت نیستند. قوانین تکراری را به‌صورت دستی حذف کرده و دوباره تلاش کنید.', - adoptRuleConfirm: 'پس از واگذاری، 1Panel می‌تواند این قانون موجود را نگهداری و حذف کند. ادامه می‌دهید؟', - plan_exact_rule_conflict: 'قانونی با شرایط تطبیق یکسان، اما با اقدام مخالفِ اجازه یا رد وجود دارد.', - allRulesAlreadyExist: 'هر {0} قانون بررسی‌شده از قبل وجود دارند. قانون جدیدی برای ایجاد وجود ندارد.', - plan_managed_rule_drifted: 'قانون مدیریت‌شده با دیوارهٔ آتش فعال یکسان نیست. ابتدا ناهمگامی را رفع کنید.', - plan_opaque_rule_in_target_scope: 'در محدودهٔ هدف قانونی وجود دارد که تحلیل امن آن ممکن نیست. عملیات متوقف شد.', - plan_runtime_permanent_mismatch: 'پیکربندی فعال و دائمی دیوارهٔ آتش متفاوت است. ابتدا آن‌ها را همگام کنید.', - plan_protected_rule: 'این قانون محافظت‌شده است و نمی‌توان آن را مدیریت، تغییر یا حذف کرد.', - plan_blocked: 'این قانون با ایمنی قابل اعمال نیست. فهرست را تازه‌سازی و دوباره تلاش کنید.', largeRuleSet: 'تعداد زیاد قوانین دیوارهٔ آتش ممکن است عملیات را کند کند. استفاده از iptables یا nftables توصیه می‌شود.', - scopeDefaultMismatch: 'ناحیهٔ پیش‌فرض سامانه {0} است؛ این صفحه فقط ناحیهٔ public را مدیریت می‌کند.', scopeMissing: 'محدودهٔ مدیریت‌شدهٔ {0} وجود ندارد و هنگام اعمال نخستین قانون به‌صورت امن ایجاد می‌شود.', - scopeUnmanagedActive: 'محدوده‌های فعال دیگری یافت شد: {0}. ‏1Panel قوانین آن‌ها را تغییر نمی‌دهد.', dockerRestart: 'عملیات دیواره آتش نیاز به راه‌اندازی مجدد سرویس Docker دارد', firewallHelper: 'دیواره آتش سیستم {0}', firewallNotStart: 'دیواره آتش سیستم در حال حاضر فعال نیست. ابتدا آن را فعال کنید.', @@ -4266,8 +4271,7 @@ const message = { dockerGuard: 'محافظت از پورت کانتینر', systemFirewall: 'فایروال میزبان', systemFirewallHelper: 'دسترسی پورت‌های میزبان و قوانین ورودی را کنترل می‌کند.', - forwardPolicyDropWarning: - 'سیاست پیش‌فرض FORWARD برای {0} برابر DROP است. ترافیک هدایت‌شده به میزبان‌های دیگر ممکن است مسدود شود، مگر اینکه قواعد iptables/ip6tables صریحاً آن را مجاز کنند. قواعد مجازکننده نسخه IP مربوطه را بررسی کنید.', + forwardPolicyDropWarning: 'این سرور هدایت ترافیک شبکه را محدود کرده است. ممکن است قوانین هدایت پورت کار نکنند.', forwardingHelper: 'قوانین انتقال پورت را مدیریت می‌کند.', dockerFirewallHelper: 'روش مدیریت محافظت از پورت کانتینر در 1Panel را انتخاب می‌کند.', dockerNftablesRequirement: 'Docker ≥ 29.0.0، آزمایشی', @@ -4276,6 +4280,7 @@ const message = { addressFamily: 'نسخه IP', portOrRange: 'پورت / بازه', batchLimit: 'در هر نوبت حداکثر {0} قانون پس از گسترش ایجاد یا وارد کنید.', + importDuplicatesRemoved: '{0} قانون تکراری، شامل موارد تکراری در فایل و قوانین موجود، حذف شد.', importLimit: 'حداکثر {0} قانون (پس از گسترش) وارد کنید؛ حجم فایل نباید از {1} کیلوبایت بیشتر باشد.', ruleSyncTitle: 'همگام‌سازی قوانین', ruleSyncAction: 'همگام‌سازی قوانین', @@ -4293,19 +4298,9 @@ const message = { ruleSyncBlocked: 'غیرقابل همگام‌سازی', ruleSyncReason: 'نتیجه بررسی', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'قانون از قبل با سیاست پایگاه داده مطابقت دارد.', - managedOrderDiffers: 'ترتیب قوانین مدیریت‌شده با ترتیب پایگاه داده متفاوت است.', - managedOnlyInTarget: 'قانون مدیریت‌شده فقط در فایروال مقصد وجود دارد.', - managedRuntimeCannotRemove: 'قانون فعال مدیریت‌شده را نمی‌توان با ایمنی حذف کرد.', missingFromTarget: 'این قانون در فایروال مقصد وجود ندارد.', - targetDiffers: 'قانون مقصد با سیاست پایگاه داده متفاوت است.', alreadyExistsInTarget: 'این قانون از قبل در فایروال مقصد وجود دارد.', onlyInTarget: 'این قانون فقط در فایروال مقصد وجود دارد.', - stale: 'وضعیت قانون فایروال قدیمی است. صفحه را تازه کنید و دوباره تلاش کنید.', - protectedRule: 'این قانون محافظت‌شدهٔ فایروال قابل تغییر نیست.', - dockerAcceptReadOnly: - 'این قانون ACCEPT فقط خواندنی است و هنگام همگام‌سازی سایر قوانین حفظ می‌شود. برای حذف آن، قانون را به‌صورت دستی از میزبان پاک کنید.', - cannotReconcile: 'قانون مقصد قابل همگام‌سازی نیست: {0}', }, ruleSyncPartial: 'همگام‌سازی پایان یافت: {0} موفق، {1} از قبل موجود و {2} ناموفق.', ruleSyncSuccess: 'همگام‌سازی پایان یافت: {0} موفق، {1} از قبل موجود و {2} حذف شد.', @@ -4315,17 +4310,19 @@ const message = { remove: 'در انتظار حذف', blocked: 'غیرقابل همگام‌سازی', }, + initializeFromFile: 'راه‌اندازی از فایل', + downloadRuleBackup: 'دریافت پشتیبان قوانین', + importFileHelper: + 'فایل‌های JSON تا 64 مگابایت به صورت دسته‌ای و به ترتیب فایل وارد می‌شوند. قوانینی که قابل تبدیل نباشند با خطا گزارش می‌شوند.', resetDirectRulesHelper: - 'زنجیره‌ها، قوانین اجرایی و فایل‌های پایدار فایروال سیستمی 1Panel از {0} حذف می‌شوند؛ سیاست‌های ذخیره‌شده پایگاه داده حفظ می‌شوند', + 'همه زنجیره‌های دیوار آتش میزبان 1Panel، قوانین فعال و فایل‌های ذخیره دائمی در {0} حذف می‌شوند.', resetWhitelistRulesHelper: - 'تنظیمات سفارشی فعال در {0} بازنشانی، پیش‌فرض‌های نصب بازیابی و {0} غیرفعال می‌شود؛ سیاست‌های پایگاه داده حفظ شده و دوباره قابل همگام‌سازی هستند.', + 'تنظیمات سفارشی {0} بازنشانی و پیش‌فرض‌ها بازیابی می‌شوند و دیوار آتش غیرفعال می‌شود.', cleanupForwardingBackendHelper: - 'قوانین زمان اجرای انتقال پورت 1Panel در {0} بازنشانی می‌شوند: همه قوانین و زنجیره‌های مرتبط حذف و داده‌های پایگاه داده حفظ می‌شوند', + 'تمام قوانین و زنجیره‌های انتقال 1Panel و فایل‌های بازیابی خودکار را از {0} حذف کنید. نسخه‌های پشتیبان JSON حفظ می‌شوند.', cleanupDockerBackendHelper: - 'قوانین زمان اجرای محافظت پورت Docker مربوط به 1Panel در {0} بازنشانی می‌شوند: همه قوانین و زنجیره‌های مرتبط حذف و داده‌های پایگاه داده حفظ می‌شوند', - cleanupBeforeBackendSwitch: - 'بک‌اند فعلی {0} هنوز شامل قوانین زمان اجرای 1Panel است. پیش از تغییر به {1} آن را بازنشانی کنید. بازنشانی فقط قوانین زمان اجرا را پاک می‌کند؛ سیاست‌های پایگاه داده حفظ می‌شوند و پس از تغییر می‌توان آن‌ها را دوباره راه‌اندازی یا همگام‌سازی کرد.', - cleanupAction: 'بازنشانی', + 'تمام قوانین و زنجیره‌های محافظت Docker در 1Panel و فایل‌های بازیابی خودکار را از {0} حذف کنید. نسخه‌های پشتیبان JSON حفظ می‌شوند.', + cleanupBeforeBackendSwitch: 'پیش از تغییر به {1}، {0} فعلی را بازنشانی و روش بازیابی قوانین را بررسی کنید.', backendSwitchNotice: 'توصیه می‌شود تنها یک روش مدیریت فایروال فعال باشد. اجرای هم‌زمان چند فایروال ممکن است باعث تداخل قوانین، ناهماهنگی وضعیت یا اختلال در دسترسی به پورت‌های کانتینر شود.', switchBackendHelper: 'به {0} تغییر داده شود؟', @@ -4374,8 +4371,7 @@ const message = { portDetails: 'جزئیات پورت', orphanEndpoints: 'نقاط پایانی بدون ارتباط', orphanPolicies: 'قوانین بدون ارتباط', - orphanPoliciesHelper: - '{0} قانون زیر به پورت Docker متصل نیستند. با شناسایی پورت منطبق، به‌طور خودکار متصل می‌شوند.', + orphanPoliciesHelper: 'این {0} قانون سیستم پورت منتشرشده منطبق در Docker ندارند.', composeOrApp: 'Compose / برنامه', sources: 'منابع', deniedSources: 'منابع ردشده', @@ -4384,6 +4380,8 @@ const message = { protectionMode: 'حالت محافظت', denySources: 'رد منابع مشخص‌شده', allowSources: 'فقط اجازه به منابع مشخص‌شده', + acceptSources: 'پذیرش مستقیم منابع مشخص‌شده', + acceptAll: 'پذیرش مستقیم تمام ترافیک', denyAll: 'رد همه دسترسی‌ها', dockerGuardMixedFamilyHelper: 'هر دو IPv4 و IPv6 انتخاب شده‌اند. سیاست‌های مبتنی بر مبدأ را جداگانه تنظیم کنید؛ رد همه دسترسی‌ها را می‌توان مستقیماً اعمال کرد.', @@ -4426,7 +4424,7 @@ const message = { 'قطع اتصال - در صورت قطع اتصال، همه قوانین دیواره آتش اضافه شده بی‌اثر می‌شوند. با احتیاط ادامه دهید. تأیید می‌کنید؟', portWhiteList: 'لیست سفید پورت', whitelistConfigHelper: - 'فهرست مجاز فقط تنظیمات را ذخیره می‌کند و از قوانین مجاز منطبق محافظت می‌کند. قوانین مفقود هنگام شروع، راه‌اندازی مجدد، مقداردهی اولیه یا همگام‌سازی اضافه می‌شوند. ویرایش یا حذف موارد فهرست مجاز، قوانین موجود را حذف نمی‌کند.', + 'با ذخیره فهرست مجاز، قوانین اجازهٔ مفقود اضافه و قوانین منطبق محافظت می‌شوند. ویرایش یا حذف موارد فهرست، قوانین موجود را حذف نمی‌کند.', whitelistDeleteConfirm: 'فقط این ورودی فهرست سفید حذف می‌شود. قوانین موجود فایروال باقی می‌مانند. ادامه می‌دهید؟', portWhiteListHelper: @@ -4444,7 +4442,7 @@ const message = { whitelistServicePortsHelper: 'یک پورت (1-65535) وارد کنید. این کار پورت شنود سرویس را تغییر نمی‌دهد. تغییر پورت در تنظیمات سرویس، این ورودی فهرست سفید را نیز به‌روزرسانی می‌کند.', whitelistSourcesHelper: - 'آدرس‌های IP یا محدوده‌های CIDR را با ویرگول یا خط جدید جدا کنید. برای مجاز کردن همه مبدأهای IPv4 و IPv6، این قسمت را خالی بگذارید.', + 'آدرس‌های IP یا محدوده‌های CIDR را با ویرگول یا خط جدید جدا کنید. برای مجاز کردن {0}، این قسمت را خالی بگذارید.', destinationPortPlaceholder: 'مثلاً: 80، 80,443 یا 8080-8089', deleteRuleConfirm: '{0} قانون حذف خواهند شد. ادامه می‌دهید؟', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 8afc283aa..18e16424b 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -4236,39 +4236,42 @@ const message = { useEAB: 'EAB認証を使用', }, firewall: { + ipv6Support: 'IPv6 サポート', + ipv6SupportHelper: + 'IPv6 ファイアウォールルールの管理と適用を制御します。サーバーの IPv6 ネットワーク機能には影響しません。', + ipv6DisableContainerHelper: 'コンテナの IPv6 ポート情報を非表示にします。', + ipv6DisableWhitelistHelper: '許可リスト内の IPv6 ルールを削除し、それ以外の既存の IPv6 ルールは保持します。', + ipv6DisableRulesHelper: '1Panel が管理する iptables、nftables の IPv6 ルールを無効にします。', + ipv6DisableNetworkHelper: 'サーバーの IPv6 ネットワーク機能は無効になりません。', + ipv6DisableEffectHelper: '対象の IPv6 アクセス制限とポート転送は適用されなくなります。', + ipv6Disabled: 'IPv6 サポートは無効です。先にファイアウォール設定で有効にしてください。', + familyRepair: '修復', + familyIncomplete: '{0} のチェーンが不完全です', + resetWithBackup: 'ルールをバックアップしてリセット', + resetOnly: 'リセットのみ', + resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時に復元できます。', + resetOnlyHelper: '新しいバックアップを作成せずに現在のルールを消去します。既存のバックアップは保持されます。', + backupDirectoryHelper: '{0} 内の有効なルールバックアップを表示します。既定では最新のファイルが選択されます。', + restoreAllRules: '初期化してルールを復元', + importRuleBackup: 'バックアップからインポート', + initializeOnly: '初期化のみ', + backupRuleCount: 'ルール数', + noRuleBackup: '復元可能なルールバックアップがありません', + ruleBackupLoadFailed: + 'ルールのバックアップを読み込めませんでした。再試行するか、バックアップを復元せずに初期化してください。', create: 'ルールを作成します', edit: 'ルールを編集します', quickJump: 'クイックアクセス', used: '使用済み', unUsed: '未使用', - managed: 'パネル作成', - managedHelper: '1Panel が作成・管理するルールです。編集または削除できます。', - adopted: '外部ルール管理', - adoptedHelper: 'システムに既に存在し、現在は 1Panel が管理しているルールです。', - external: '外部ルール', - externalHelper: - 'システムに存在しますが、まだ 1Panel の管理対象ではありません。必要に応じて管理対象にできます。', - protected: 'システム保護', - protectedHelper: '組み込みルールとホワイトリストに完全一致するルールは編集・削除できません。', - stateShort: { managed: 'パネル', adopted: '管理済', external: '外部', protected: '保護', drifted: '差異' }, + whitelist: '許可リスト', + builtinRuleProtected: 'システム内蔵の保護ルールです。編集や削除はできません。', + whitelistRuleProtected: + '許可リストのルールは順序と説明のみ変更できます。その他の変更は「設定 → ポート許可リスト」で行ってください。', ruleTargetRequired: 'IP アドレスまたはポートを1つ以上入力してください', - resolution_adopt: '管理対象にする', - plan_duplicate_rules: - '条件とアクションが同じルールが重複しているため、管理対象に追加できません。重複ルールを手動で削除してから再試行してください。', - adoptRuleConfirm: '管理対象にすると、1Panel がこの既存ルールの保守と削除を行えるようになります。続行しますか?', - plan_exact_rule_conflict: '一致条件が同じで、許可・拒否の動作が逆のルールが存在します。', - allRulesAlreadyExist: '確認した {0} 件のルールはすべて既に存在します。新しく作成するルールはありません。', - plan_managed_rule_drifted: - '管理対象ルールが実際のファイアウォールと一致しません。先に不整合を解消してください。', - plan_opaque_rule_in_target_scope: '対象範囲に安全に解析できないルールがあります。操作を停止しました。', - plan_runtime_permanent_mismatch: '実行中と永続設定のファイアウォール構成が異なります。先に同期してください。', - plan_protected_rule: 'このルールは保護されており、管理対象化、変更、削除はできません。', - plan_blocked: 'このルールは安全に適用できません。ルールを更新して再試行してください。', largeRuleSet: 'ファイアウォールルールが多すぎると操作が遅くなる可能性があります。iptables または nftables の使用を推奨します。', - scopeDefaultMismatch: 'システムの既定 zone は {0} です。このページでは public zone のみ管理します。', scopeMissing: '管理対象スコープ {0} がありません。最初のルール適用時に安全に作成されます。', - scopeUnmanagedActive: '他の有効な範囲が見つかりました:{0}。1Panel はそのルールを変更しません。', dockerRestart: 'ファイアウォール操作にはDockerサービスの再起動が必要です', firewallHelper: '{0}システムファイアウォール', firewallNotStart: '現在、システムファイアウォールは有効になっていません。最初に有効にします。', @@ -4292,7 +4295,7 @@ const message = { systemFirewall: 'ホストファイアウォール', systemFirewallHelper: 'ホストのポートアクセスと受信ルールを管理します。', forwardPolicyDropWarning: - '{0} の FORWARD のデフォルトポリシーは DROP です。iptables/ip6tables のルールで明示的に許可されていない他のホストへの転送トラフィックは、遮断される可能性があります。該当する IP バージョンの許可ルールを確認してください。', + 'このサーバーではネットワーク転送が制限されているため、ポート転送ルールが機能しない可能性があります。', forwardingHelper: 'ポート転送ルールを管理します。', dockerFirewallHelper: '1Panel のコンテナポート保護の管理方法を選択します。', dockerNftablesRequirement: 'Docker ≥ 29.0.0、実験的', @@ -4301,6 +4304,7 @@ const message = { addressFamily: 'IP バージョン', portOrRange: 'ポート / 範囲', batchLimit: '一度に作成またはインポートできるルールは、展開後の件数で最大 {0} 件です。', + importDuplicatesRemoved: 'ファイル内の重複と既存ルールを含む、{0} 件の重複ルールを除外しました。', importLimit: '最大 {0} 件のルール(展開後の件数)をインポートできます。ファイルサイズの上限は {1} KB です。', ruleSyncTitle: 'ルールを同期', ruleSyncAction: 'ルールを同期', @@ -4318,19 +4322,9 @@ const message = { ruleSyncBlocked: '同期不可', ruleSyncReason: '確認結果', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'ルールは既にデータベースポリシーと一致しています。', - managedOrderDiffers: '管理対象ルールの順序がデータベースの順序と異なります。', - managedOnlyInTarget: '管理対象ルールは対象ファイアウォールにのみ存在します。', - managedRuntimeCannotRemove: '管理対象の実行中ルールを安全に削除できません。', missingFromTarget: '対象ファイアウォールにこのルールがありません。', - targetDiffers: '対象ルールがデータベースポリシーと異なります。', alreadyExistsInTarget: '対象ファイアウォールにこのルールは既に存在します。', onlyInTarget: 'このルールは対象ファイアウォールにのみ存在します。', - stale: 'ファイアウォールルールの状態が古くなっています。更新して再試行してください。', - protectedRule: '保護されたファイアウォールルールは変更できません。', - dockerAcceptReadOnly: - 'この ACCEPT ルールは読み取り専用で、他のルールを同期しても保持されます。削除する場合は、ホスト上で手動で削除してください。', - cannotReconcile: '対象ルールを同期できません:{0}', }, ruleSyncPartial: '同期完了:成功 {0} 件、存在済み {1} 件、失敗 {2} 件。', ruleSyncSuccess: '同期完了:成功 {0} 件、存在済み {1} 件、削除 {2} 件。', @@ -4340,17 +4334,18 @@ const message = { remove: '削除予定', blocked: '同期不可', }, + initializeFromFile: 'ファイルから初期化', + downloadRuleBackup: 'ルールのバックアップをダウンロード', + importFileHelper: + '最大 64 MB の JSON ファイルをファイル順に分割してインポートします。変換できないルールはエラーとして報告します。', resetDirectRulesHelper: - '{0} から 1Panel システムファイアウォールのチェーン、実行ルール、永続化ファイルを削除し、データベースポリシーは保持します', - resetWhitelistRulesHelper: - '{0} の有効なカスタム設定をリセットし、インストール時の既定値に戻して無効化します。データベースポリシーは保持され、後で再同期できます。', + '{0} のすべての 1Panel ホストファイアウォールチェーン、実行中のルール、永続化ファイルを削除します。', + resetWhitelistRulesHelper: '{0} のカスタム設定をリセットし、既定の状態に戻して無効にします。', cleanupForwardingBackendHelper: - '{0} の 1Panel ポート転送ランタイムルールをリセットします。関連するルールとチェーンをすべて削除し、データベースデータを保持します', + '{0} の 1Panel 転送ルール、チェーン、自動復元ファイルをすべて削除します。既存の JSON バックアップは保持されます。', cleanupDockerBackendHelper: - '{0} の 1Panel Docker ポート保護ランタイムルールをリセットします。関連するルールとチェーンをすべて削除し、データベースデータを保持します', - cleanupBeforeBackendSwitch: - '現在の {0} バックエンドには 1Panel の実行時ルールが残っています。{1} に切り替える前にリセットしてください。リセットでは実行時ルールのみが削除され、データベースポリシーは保持されます。切り替え後に再初期化または同期できます。', - cleanupAction: 'リセット', + '{0} の 1Panel Docker 保護ルール、チェーン、自動復元ファイルをすべて削除します。既存の JSON バックアップは保持されます。', + cleanupBeforeBackendSwitch: '{1} に切り替える前に現在の {0} をリセットし、ルールの復元方法を確認してください。', backendSwitchNotice: 'ファイアウォールの管理方式は 1 つだけ有効にすることを推奨します。複数のファイアウォールを同時に実行すると、ルールの競合、状態の不一致、コンテナーポートへのアクセス異常が発生する可能性があります。', switchBackendHelper: '{0} に切り替えますか?', @@ -4396,8 +4391,7 @@ const message = { portDetails: 'ポート詳細', orphanEndpoints: '未関連付けエンドポイント', orphanPolicies: '未関連付けルール', - orphanPoliciesHelper: - '以下の {0} 件のルールは Docker ポートにバインドされていません。一致するポートが検出されると自動的にバインドされます。', + orphanPoliciesHelper: 'これら {0} 件のシステムルールに一致する Docker 公開ポートはありません。', composeOrApp: 'Compose / アプリ', sources: '送信元', deniedSources: '拒否する送信元', @@ -4406,6 +4400,8 @@ const message = { protectionMode: '保護モード', denySources: '指定した送信元を拒否', allowSources: '指定した送信元のみ許可', + acceptSources: '指定した送信元を直接許可', + acceptAll: 'すべての通信を直接許可', denyAll: 'すべてのアクセスを拒否', dockerGuardMixedFamilyHelper: 'IPv4 と IPv6 の両方が選択されています。送信元指定は個別に設定してください。「すべて拒否」はそのまま適用できます。', @@ -4449,7 +4445,7 @@ const message = { 'アンバインド - アンバインドすると、追加されたすべてのファイアウォールルールが無効になります。注意して操作してください。確認しますか?', portWhiteList: 'ポートホワイトリスト', whitelistConfigHelper: - '許可リストは設定の保存と一致する許可ルールの保護のみを行います。不足するルールは起動、再起動、初期化、同期時に追加されます。許可リストを編集・削除しても既存のルールは削除されません。', + '許可リストの保存時に不足する許可ルールを追加し、一致するルールを保護します。許可リストの編集や削除では既存のルールは削除されません。', whitelistDeleteConfirm: 'このホワイトリスト項目のみを削除します。既存のファイアウォールルールは保持されます。続行しますか?', portWhiteListHelper: @@ -4467,7 +4463,7 @@ const message = { whitelistServicePortsHelper: 'ポートを1つ入力してください(1-65535)。サービスの待受ポートは変更されません。サービス設定でポートを変更すると、このホワイトリスト項目も更新されます。', whitelistSourcesHelper: - 'IP アドレスまたは CIDR をカンマか改行で区切って入力してください。 空欄の場合、すべての IPv4 および IPv6 の接続元を許可します。', + 'IP アドレスまたは CIDR をカンマか改行で区切って入力してください。空欄の場合、{0} を許可します。', destinationPortPlaceholder: '例: 80、80,443、8080-8089', deleteRuleConfirm: '{0} 個のルールを削除します。続行しますか?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 8fe601640..fcb31974b 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -4159,39 +4159,41 @@ const message = { useEAB: 'EAB 인증 사용', }, firewall: { + ipv6Support: 'IPv6 지원', + ipv6SupportHelper: + 'IPv6 방화벽 규칙의 관리 및 적용을 제어하며 서버의 IPv6 네트워크 기능에는 영향을 주지 않습니다.', + ipv6DisableContainerHelper: '컨테이너의 IPv6 포트 정보를 숨깁니다.', + ipv6DisableWhitelistHelper: '허용 목록의 IPv6 규칙을 제거하고 나머지 기존 IPv6 규칙은 유지합니다.', + ipv6DisableRulesHelper: '1Panel이 관리하는 iptables 및 nftables의 IPv6 규칙을 비활성화합니다.', + ipv6DisableNetworkHelper: '서버의 IPv6 네트워크 기능은 비활성화되지 않습니다.', + ipv6DisableEffectHelper: '해당 IPv6 접근 제한과 포트 포워딩은 더 이상 적용되지 않습니다.', + ipv6Disabled: 'IPv6 지원이 비활성화되어 있습니다. 먼저 방화벽 설정에서 활성화하세요.', + familyRepair: '복구', + familyIncomplete: '{0} 체인이 불완전합니다', + resetWithBackup: '규칙 백업 후 초기화', + resetOnly: '초기화만 수행', + resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 시 복원할 수 있습니다.', + resetOnlyHelper: '새 백업 없이 현재 규칙을 삭제합니다. 기존 백업은 유지됩니다.', + backupDirectoryHelper: '{0}의 유효한 규칙 백업을 표시합니다. 기본적으로 최신 파일이 선택됩니다.', + restoreAllRules: '초기화 및 규칙 복원', + importRuleBackup: '백업에서 가져오기', + initializeOnly: '초기화만 수행', + backupRuleCount: '규칙 수', + noRuleBackup: '복원할 수 있는 규칙 백업이 없습니다', + ruleBackupLoadFailed: '규칙 백업을 불러오지 못했습니다. 다시 시도하거나 백업을 복원하지 않고 초기화하세요.', create: '규칙 만들기', edit: '규칙 수정', quickJump: '빠른 이동', used: '사용됨', unUsed: '사용 안 함', - managed: '패널 생성', - managedHelper: '1Panel에서 생성하고 관리하는 규칙으로 편집하거나 삭제할 수 있습니다.', - adopted: '외부 규칙 관리', - adoptedHelper: '기존 시스템 규칙을 현재 1Panel에서 관리하고 있습니다.', - external: '외부 규칙', - externalHelper: - '시스템에 존재하지만 아직 1Panel에서 관리하지 않는 규칙이며 필요할 때 관리 대상으로 전환할 수 있습니다.', - protected: '시스템 보호', - protectedHelper: '기본 제공 규칙과 화이트리스트에 정확히 일치하는 규칙은 편집하거나 삭제할 수 없습니다.', - stateShort: { managed: '패널', adopted: '관리', external: '외부', protected: '보호', drifted: '불일치' }, + whitelist: '허용 목록', + builtinRuleProtected: '시스템 기본 보호 규칙입니다. 편집하거나 삭제할 수 없습니다.', + whitelistRuleProtected: + '허용 목록 규칙은 순서와 설명만 변경할 수 있습니다. 다른 변경은 설정 → 포트 허용 목록에서 진행하세요.', ruleTargetRequired: 'IP 주소 또는 포트를 하나 이상 입력하세요', - resolution_adopt: '관리 대상으로 전환', - plan_duplicate_rules: - '조건과 동작이 동일한 중복 규칙은 관리 대상으로 추가할 수 없습니다. 중복 규칙을 수동으로 삭제한 후 다시 시도하세요.', - adoptRuleConfirm: - '관리 대상으로 전환하면 1Panel이 이 기존 규칙을 유지하고 삭제할 수 있습니다. 계속하시겠습니까?', - plan_exact_rule_conflict: '일치 조건이 같지만 허용 또는 거부 동작이 반대인 규칙이 이미 있습니다.', - allRulesAlreadyExist: '확인한 규칙 {0}개가 모두 이미 존재합니다. 새로 생성할 규칙이 없습니다.', - plan_managed_rule_drifted: '관리 규칙이 실제 방화벽과 일치하지 않습니다. 먼저 불일치를 해결하세요.', - plan_opaque_rule_in_target_scope: '대상 범위에 안전하게 분석할 수 없는 규칙이 있어 작업을 중지했습니다.', - plan_runtime_permanent_mismatch: '실행 중 구성과 영구 방화벽 구성이 다릅니다. 먼저 동기화하세요.', - plan_protected_rule: '이 규칙은 보호되어 관리 전환, 변경 또는 삭제할 수 없습니다.', - plan_blocked: '이 규칙을 안전하게 적용할 수 없습니다. 목록을 새로 고친 후 다시 시도하세요.', largeRuleSet: '방화벽 규칙이 너무 많으면 작업이 느려질 수 있습니다. iptables 또는 nftables 사용을 권장합니다.', - scopeDefaultMismatch: '시스템 기본 zone은 {0}이며 이 페이지는 public zone만 관리합니다.', scopeMissing: '관리 범위 {0}이(가) 없으며 첫 규칙을 적용할 때 안전하게 생성됩니다.', - scopeUnmanagedActive: '다른 활성 범위가 감지되었습니다: {0}. 1Panel은 해당 규칙을 변경하지 않습니다.', dockerRestart: '방화벽 작업에는 Docker 서비스 재시작이 필요합니다', firewallHelper: '{0} 시스템 방화벽', firewallNotStart: '현재 시스템 방화벽이 활성화되지 않았습니다. 먼저 활성화하세요.', @@ -4213,8 +4215,7 @@ const message = { dockerGuard: '컨테이너 포트 보호', systemFirewall: '호스트 방화벽', systemFirewallHelper: '호스트 포트 접근과 인바운드 규칙을 관리합니다.', - forwardPolicyDropWarning: - '{0}의 FORWARD 기본 정책은 DROP입니다. iptables/ip6tables 규칙에서 명시적으로 허용하지 않은 다른 호스트로의 전달 트래픽은 차단될 수 있습니다. 해당 IP 버전의 허용 규칙을 확인하세요.', + forwardPolicyDropWarning: '현재 서버에서 네트워크 전달이 제한되어 포트 전달 규칙이 작동하지 않을 수 있습니다.', forwardingHelper: '포트 포워딩 규칙을 관리합니다.', dockerFirewallHelper: '1Panel 컨테이너 포트 보호 관리 방식을 선택합니다.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, 실험적', @@ -4223,6 +4224,7 @@ const message = { addressFamily: 'IP 버전', portOrRange: '포트 / 범위', batchLimit: '한 번에 생성하거나 가져올 수 있는 규칙은 확장 후 기준으로 최대 {0}개입니다.', + importDuplicatesRemoved: '파일 내 중복 및 이미 존재하는 규칙을 포함하여 중복 규칙 {0}개를 제거했습니다.', importLimit: '최대 {0}개의 규칙(확장 후 기준)을 가져올 수 있으며, 파일 크기는 {1} KB를 초과할 수 없습니다.', ruleSyncTitle: '규칙 동기화', ruleSyncAction: '규칙 동기화', @@ -4240,19 +4242,9 @@ const message = { ruleSyncBlocked: '동기화 불가', ruleSyncReason: '검사 결과', ruleSyncReasonDetail: { - matchesDatabasePolicy: '규칙이 이미 데이터베이스 정책과 일치합니다.', - managedOrderDiffers: '관리 규칙 순서가 데이터베이스 순서와 다릅니다.', - managedOnlyInTarget: '관리 규칙이 대상 방화벽에만 존재합니다.', - managedRuntimeCannotRemove: '관리 중인 런타임 규칙을 안전하게 삭제할 수 없습니다.', missingFromTarget: '대상 방화벽에 이 규칙이 없습니다.', - targetDiffers: '대상 규칙이 데이터베이스 정책과 다릅니다.', alreadyExistsInTarget: '대상 방화벽에 이 규칙이 이미 존재합니다.', onlyInTarget: '이 규칙은 대상 방화벽에만 존재합니다.', - stale: '방화벽 규칙 상태가 오래되었습니다. 새로 고친 후 다시 시도하세요.', - protectedRule: '보호된 방화벽 규칙은 수정할 수 없습니다.', - dockerAcceptReadOnly: - '이 ACCEPT 규칙은 읽기 전용이며 다른 규칙을 동기화할 때 그대로 유지됩니다. 제거하려면 호스트에서 수동으로 삭제하세요.', - cannotReconcile: '대상 규칙을 동기화할 수 없습니다: {0}', }, ruleSyncPartial: '동기화 완료: 성공 {0}개, 이미 존재 {1}개, 실패 {2}개.', ruleSyncSuccess: '동기화 완료: 성공 {0}개, 이미 존재 {1}개, 삭제 {2}개.', @@ -4262,17 +4254,17 @@ const message = { remove: '삭제 예정', blocked: '동기화 불가', }, - resetDirectRulesHelper: - '{0}에서 1Panel 시스템 방화벽 체인, 실행 규칙 및 영구 파일을 삭제하고 데이터베이스 정책은 유지합니다', - resetWhitelistRulesHelper: - '{0}의 활성 사용자 설정을 재설정하고 설치 기본값으로 복원한 후 비활성화합니다. 데이터베이스 정책은 유지되며 나중에 다시 동기화할 수 있습니다.', + initializeFromFile: '파일에서 초기화', + downloadRuleBackup: '규칙 백업 다운로드', + importFileHelper: + '최대 64 MB의 JSON 파일을 파일 순서대로 나누어 가져옵니다. 변환할 수 없는 규칙은 오류로 보고됩니다.', + resetDirectRulesHelper: '{0}의 모든 1Panel 호스트 방화벽 체인, 실행 중인 규칙 및 영구 저장 파일을 삭제합니다.', + resetWhitelistRulesHelper: '{0}의 사용자 설정을 초기화하고 기본 상태로 복원한 후 비활성화합니다.', cleanupForwardingBackendHelper: - '{0}의 1Panel 포트 전달 런타임 규칙을 재설정합니다. 관련 규칙과 체인을 모두 삭제하고 데이터베이스 데이터는 유지합니다', + '{0}의 모든 1Panel 전달 규칙, 체인 및 자동 복구 파일을 삭제합니다. 기존 JSON 백업은 유지됩니다.', cleanupDockerBackendHelper: - '{0}의 1Panel Docker 포트 보호 런타임 규칙을 재설정합니다. 관련 규칙과 체인을 모두 삭제하고 데이터베이스 데이터는 유지합니다', - cleanupBeforeBackendSwitch: - '현재 {0} 백엔드에 1Panel 런타임 규칙이 남아 있습니다. {1}(으)로 전환하기 전에 먼저 재설정하세요. 재설정은 런타임 규칙만 정리하며 데이터베이스 정책은 유지됩니다. 전환 후 다시 초기화하거나 동기화할 수 있습니다.', - cleanupAction: '재설정', + '{0}의 모든 1Panel Docker 보호 규칙, 체인 및 자동 복구 파일을 삭제합니다. 기존 JSON 백업은 유지됩니다.', + cleanupBeforeBackendSwitch: '{1}(으)로 전환하기 전에 현재 {0}을(를) 초기화하고 규칙 복원 방법을 확인하세요.', backendSwitchNotice: '하나의 방화벽 관리 방식만 활성화하는 것이 좋습니다. 여러 방화벽을 동시에 실행하면 규칙 충돌, 상태 불일치 또는 컨테이너 포트 접근 오류가 발생할 수 있습니다.', switchBackendHelper: '{0}(으)로 전환하시겠습니까?', @@ -4319,8 +4311,7 @@ const message = { portDetails: '포트 상세 정보', orphanEndpoints: '연결되지 않은 엔드포인트', orphanPolicies: '연결되지 않은 규칙', - orphanPoliciesHelper: - '다음 {0}개 규칙은 Docker 포트에 바인딩되지 않았습니다. 일치하는 포트가 감지되면 자동으로 바인딩됩니다.', + orphanPoliciesHelper: '이 시스템 규칙 {0}개와 일치하는 Docker 게시 포트가 없습니다.', composeOrApp: 'Compose / 앱', sources: '소스', deniedSources: '거부된 소스', @@ -4329,6 +4320,8 @@ const message = { protectionMode: '보호 모드', denySources: '지정한 소스 거부', allowSources: '지정한 소스만 허용', + acceptSources: '지정된 소스를 직접 허용', + acceptAll: '모든 트래픽을 직접 허용', denyAll: '모든 접근 거부', dockerGuardMixedFamilyHelper: 'IPv4와 IPv6가 모두 선택되었습니다. 출발지 지정 정책은 각각 설정해야 하며, 모든 접근 거부는 바로 적용할 수 있습니다.', @@ -4370,7 +4363,7 @@ const message = { '바인딩 해제 - 바인딩 해제 시 추가된 모든 방화벽 규칙이 무효화됩니다. 주의하여 진행하세요. 확인하시겠습니까?', portWhiteList: '포트 화이트리스트', whitelistConfigHelper: - '허용 목록은 설정만 저장하고 일치하는 허용 규칙을 보호합니다. 누락된 규칙은 시작, 재시작, 초기화 또는 동기화 시 추가됩니다. 허용 목록을 편집하거나 삭제해도 기존 규칙은 삭제되지 않습니다.', + '허용 목록을 저장하면 누락된 허용 규칙을 추가하고 일치하는 규칙을 보호합니다. 허용 목록 항목을 수정하거나 삭제해도 기존 규칙은 삭제되지 않습니다.', whitelistDeleteConfirm: '이 화이트리스트 항목만 삭제됩니다. 기존 방화벽 규칙은 유지됩니다. 계속하시겠습니까?', portWhiteListHelper: '단일 포트(예: 80) 또는 포트 범위(예: 8000-8100)를 입력하세요. 포트 번호는 1~65535여야 합니다.', @@ -4387,7 +4380,7 @@ const message = { whitelistServicePortsHelper: '포트 하나를 입력하세요(1-65535). 서비스의 수신 포트는 변경되지 않습니다. 서비스 설정에서 포트를 변경하면 이 화이트리스트 항목도 업데이트됩니다.', whitelistSourcesHelper: - 'IP 주소 또는 CIDR 대역을 쉼표나 줄바꿈으로 구분하세요. 비워 두면 모든 IPv4 및 IPv6 출발지를 허용합니다.', + 'IP 주소 또는 CIDR 대역을 쉼표나 줄바꿈으로 구분하세요. 비워 두면 {0}을(를) 허용합니다.', destinationPortPlaceholder: '예: 80, 80,443 또는 8080-8089', deleteRuleConfirm: '{0}개의 규칙을 삭제합니다. 계속하시겠습니까?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 41f6189a0..4b2f04ffc 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -4128,36 +4128,39 @@ const message = { useEAB: 'ໃຊ້ການຢືນຢັນຕົວຕົນ EAB', }, firewall: { + ipv6Support: 'ຮອງຮັບ IPv6', + ipv6SupportHelper: 'ຄວບຄຸມການຈັດການ ແລະ ການນຳໃຊ້ກົດໄຟວໍ IPv6 ໂດຍບໍ່ກະທົບຕໍ່ເຄືອຂ່າຍ IPv6 ຂອງເຊີບເວີ.', + ipv6DisableContainerHelper: 'ເຊື່ອງຂໍ້ມູນພອດ IPv6 ຂອງຄອນເທນເນີ.', + ipv6DisableWhitelistHelper: 'ລຶບກົດ IPv6 ອອກຈາກລາຍການອະນຸຍາດ ແລະ ເກັບກົດ IPv6 ອື່ນທີ່ມີຢູ່ໄວ້.', + ipv6DisableRulesHelper: 'ປິດໃຊ້ງານກົດ IPv6 ໃນ iptables ແລະ nftables ທີ່ 1Panel ຈັດການ.', + ipv6DisableNetworkHelper: 'ການດຳເນີນການນີ້ບໍ່ໄດ້ປິດເຄືອຂ່າຍ IPv6 ຂອງເຊີບເວີ.', + ipv6DisableEffectHelper: 'ຂໍ້ຈຳກັດການເຂົ້າເຖິງ IPv6 ແລະ ການສົ່ງຕໍ່ພອດທີ່ກ່ຽວຂ້ອງຈະບໍ່ມີຜົນອີກຕໍ່ໄປ.', + ipv6Disabled: 'ການຮອງຮັບ IPv6 ຖືກປິດ. ກະລຸນາເປີດໃນການຕັ້ງຄ່າໄຟວໍກ່ອນ.', + familyRepair: 'ສ້ອມແປງ', + familyIncomplete: 'ຊຸດກົດ {0} ບໍ່ຄົບຖ້ວນ', + resetWithBackup: 'ສຳຮອງກົດແລ້ວຣີເຊັດ', + resetOnly: 'ຣີເຊັດເທົ່ານັ້ນ', + resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ.', + resetOnlyHelper: 'ລຶບກົດປັດຈຸບັນໂດຍບໍ່ສ້າງໄຟລ໌ສຳຮອງໃໝ່. ໄຟລ໌ສຳຮອງເກົ່າຍັງຄົງຢູ່.', + backupDirectoryHelper: 'ສະແດງໄຟລ໌ສຳຮອງກົດທີ່ໃຊ້ໄດ້ໃນ {0}. ເລືອກໄຟລ໌ຫຼ້າສຸດເປັນຄ່າເລີ່ມຕົ້ນ.', + restoreAllRules: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ', + importRuleBackup: 'ນຳເຂົ້າຈາກຂໍ້ມູນສຳຮອງ', + initializeOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ', + backupRuleCount: 'ຈຳນວນກົດ', + noRuleBackup: 'ບໍ່ພົບໄຟລ໌ສຳຮອງກົດທີ່ກູ້ຄືນໄດ້', + ruleBackupLoadFailed: 'ໂຫຼດລາຍການໄຟລ໌ສຳຮອງກົດບໍ່ສຳເລັດ. ກະລຸນາລອງໃໝ່ ຫຼືເລີ່ມຕົ້ນໂດຍບໍ່ກູ້ຄືນໄຟລ໌ສຳຮອງ.', create: 'ສ້າງກົດລະບຽບ', edit: 'ແກ້ໄຂກົດລະບຽບ', quickJump: 'ເຂົ້າເຖິງດ່ວນ', used: 'ໃຊ້ແລ້ວ', unUsed: 'ບໍ່ໄດ້ໃຊ້', - managed: 'ສ້າງໂດຍແຜງ', - managedHelper: 'ສ້າງ ແລະ ດູແລໂດຍ 1Panel ສາມາດແກ້ໄຂ ຫຼື ລຶບໄດ້.', - adopted: 'ກົດພາຍນອກທີ່ຈັດການ', - adoptedHelper: 'ກົດທີ່ມີຢູ່ໃນລະບົບ ແລະ ຕອນນີ້ດູແລໂດຍ 1Panel.', - external: 'ກົດພາຍນອກ', - externalHelper: 'ກົດທີ່ມີຢູ່ໃນລະບົບແຕ່ 1Panel ຍັງບໍ່ໄດ້ຈັດການ ແລະ ສາມາດຮັບເຂົ້າຈັດການໄດ້ເມື່ອຈຳເປັນ.', - protected: 'ລະບົບປົກປ້ອງ', - protectedHelper: 'ກົດທີ່ມີໃນລະບົບ ແລະ ກົດທີ່ກົງກັບບັນຊີຂາວທຸກປະການ ບໍ່ສາມາດແກ້ໄຂ ຫຼື ລຶບໄດ້.', - stateShort: { managed: 'ແຜງ', adopted: 'ຈັດການ', external: 'ພາຍນອກ', protected: 'ປົກປ້ອງ', drifted: 'ບໍ່ກົງ' }, + whitelist: 'ລາຍການອະນຸຍາດ', + builtinRuleProtected: 'ກົດປ້ອງກັນທີ່ມີມາພ້ອມລະບົບ ບໍ່ສາມາດແກ້ໄຂ ຫຼືລຶບໄດ້.', + whitelistRuleProtected: + 'ກົດໃນລາຍການອະນຸຍາດປ່ຽນໄດ້ພຽງລຳດັບ ແລະ ຄຳອະທິບາຍ. ການປ່ຽນແປງອື່ນໆ ໃຫ້ໄປທີ່ ການຕັ້ງຄ່າ → ລາຍການພອດທີ່ອະນຸຍາດ.', ruleTargetRequired: 'ກະລຸນາປ້ອນ IP ຫຼື ພອດຢ່າງໜ້ອຍໜຶ່ງລາຍການ', - resolution_adopt: 'ຮັບເຂົ້າຈັດການ', - plan_duplicate_rules: - 'ບໍ່ສາມາດນຳກົດທີ່ມີເງື່ອນໄຂ ແລະ ການກະທຳຄືກັນເຂົ້າມາຈັດການໄດ້. ກະລຸນາລຶບກົດທີ່ຊ້ຳກັນດ້ວຍຕົນເອງ ແລ້ວລອງອີກຄັ້ງ.', - adoptRuleConfirm: 'ຫຼັງຈາກຮັບເຂົ້າແລ້ວ 1Panel ຈະສາມາດຮັກສາແລະລຶບກົດນີ້ໄດ້. ສືບຕໍ່ບໍ?', - plan_exact_rule_conflict: 'ມີກົດທີ່ມີເງື່ອນໄຂຄືກັນ ແຕ່ການອະນຸຍາດຫຼືປະຕິເສດກົງກັນຂ້າມ.', - allRulesAlreadyExist: 'ກົດທັງໝົດ {0} ລາຍການທີ່ກວດສອບມີຢູ່ແລ້ວ. ບໍ່ມີກົດໃໝ່ທີ່ຕ້ອງສ້າງ.', - plan_managed_rule_drifted: 'ກົດທີ່ຈັດການບໍ່ກົງກັບໄຟວໍທີ່ໃຊ້ງານ. ກະລຸນາແກ້ໄຂກ່ອນ.', - plan_opaque_rule_in_target_scope: 'ມີກົດໃນຂອບເຂດທີ່ບໍ່ສາມາດວິເຄາະໄດ້ຢ່າງປອດໄພ. ການດຳເນີນງານຖືກຢຸດ.', - plan_runtime_permanent_mismatch: 'ຄ່າໄຟວໍທີ່ໃຊ້ງານແລະຄ່າຖາວອນບໍ່ກົງກັນ. ກະລຸນາຊິງກ່ອນ.', - plan_protected_rule: 'ກົດນີ້ຖືກປົກປ້ອງ ແລະບໍ່ສາມາດຮັບເຂົ້າ, ແກ້ໄຂ ຫຼືລຶບໄດ້.', - plan_blocked: 'ບໍ່ສາມາດນຳໃຊ້ກົດນີ້ໄດ້ຢ່າງປອດໄພ. ກະລຸນາໂຫຼດໃໝ່ແລ້ວລອງອີກຄັ້ງ.', largeRuleSet: 'ກົດໄຟວໍທີ່ຫຼາຍເກີນໄປອາດເຮັດໃຫ້ການດຳເນີນງານຊ້າລົງ. ແນະນຳໃຫ້ໃຊ້ iptables ຫຼື nftables.', - scopeDefaultMismatch: 'zone ຄ່າເລີ່ມຕົ້ນແມ່ນ {0}; ໜ້ານີ້ຈັດການສະເພາະ public zone.', scopeMissing: 'ບໍ່ພົບຂອບເຂດຈັດການ {0}; ລະບົບຈະສ້າງຢ່າງປອດໄພເມື່ອນຳໃຊ້ກົດທຳອິດ.', - scopeUnmanagedActive: 'ພົບຂອບເຂດອື່ນທີ່ໃຊ້ງານ: {0}. 1Panel ຈະບໍ່ແກ້ໄຂກົດເຫຼົ່ານັ້ນ.', dockerRestart: 'ການຈັດການໄຟວໍຕ້ອງມີການເລີ່ມຕົ້ນບໍລິການ Docker ໃໝ່', firewallHelper: 'ໄຟວໍລະບົບ {0}', firewallNotStart: 'ໄຟວໍລະບົບຍັງບໍ່ໄດ້ເປີດໃຊ້. ກະລຸນາເປີດກ່ອນ.', @@ -4179,8 +4182,7 @@ const message = { dockerGuard: 'ການປ້ອງກັນພອດຄອນເທນເນີ', systemFirewall: 'ໄຟວໍໂຮສ', systemFirewallHelper: 'ຄວບຄຸມການເຂົ້າເຖິງພອດໂຮສ ແລະ ກົດຂາເຂົ້າ.', - forwardPolicyDropWarning: - 'ນະໂຍບາຍ FORWARD ເລີ່ມຕົ້ນສຳລັບ {0} ແມ່ນ DROP. ການຈະລາຈອນທີ່ສົ່ງຕໍ່ໄປຍັງໂຮສອື່ນອາດຖືກບລັອກ ຖ້າບໍ່ມີກົດ iptables/ip6tables ອະນຸຍາດຢ່າງຊັດເຈນ. ກະລຸນາກວດສອບກົດອະນຸຍາດສຳລັບເວີຊັນ IP ທີ່ກ່ຽວຂ້ອງ.', + forwardPolicyDropWarning: 'ເຊີບເວີນີ້ຈຳກັດການສົ່ງຕໍ່ເຄືອຂ່າຍ. ກົດການສົ່ງຕໍ່ພອດອາດບໍ່ເຮັດວຽກ.', forwardingHelper: 'ຈັດການກົດການສົ່ງຕໍ່ພອດ.', dockerFirewallHelper: 'ເລືອກວິທີທີ່ 1Panel ຈັດການການປ້ອງກັນພອດຄອນເທນເນີ.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, ທົດລອງ', @@ -4189,6 +4191,7 @@ const message = { addressFamily: 'ລຸ້ນ IP', portOrRange: 'ພອດ / ຊ່ວງ', batchLimit: 'ແຕ່ລະຄັ້ງສາມາດສ້າງ ຫຼື ນຳເຂົ້າໄດ້ສູງສຸດ {0} ກົດ ໂດຍນັບຫຼັງຈາກຂະຫຍາຍແລ້ວ.', + importDuplicatesRemoved: 'ໄດ້ລຶບກົດທີ່ຊ້ຳກັນ {0} ລາຍການ ລວມທັງກົດທີ່ຊ້ຳໃນໄຟລ໌ ແລະກົດທີ່ມີຢູ່ແລ້ວ.', importLimit: 'ນຳເຂົ້າໄດ້ສູງສຸດ {0} ກົດ (ນັບຫຼັງຈາກຂະຫຍາຍແລ້ວ), ຂະໜາດໄຟລ໌ບໍ່ເກີນ {1} KB.', ruleSyncTitle: 'ຊິງກົດ', ruleSyncAction: 'ຊິງກົດ', @@ -4205,19 +4208,9 @@ const message = { ruleSyncBlocked: 'ຊິງບໍ່ໄດ້', ruleSyncReason: 'ຜົນການກວດສອບ', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'ກົດນີ້ກົງກັບນະໂຍບາຍໃນຖານຂໍ້ມູນແລ້ວ.', - managedOrderDiffers: 'ລຳດັບກົດທີ່ຈັດການແຕກຕ່າງຈາກລຳດັບໃນຖານຂໍ້ມູນ.', - managedOnlyInTarget: 'ກົດທີ່ຈັດການມີຢູ່ສະເພາະໃນໄຟວໍເປົ້າໝາຍ.', - managedRuntimeCannotRemove: 'ບໍ່ສາມາດລຶບກົດທີ່ກຳລັງໃຊ້ງານໄດ້ຢ່າງປອດໄພ.', missingFromTarget: 'ບໍ່ມີກົດນີ້ໃນໄຟວໍເປົ້າໝາຍ.', - targetDiffers: 'ກົດເປົ້າໝາຍແຕກຕ່າງຈາກນະໂຍບາຍໃນຖານຂໍ້ມູນ.', alreadyExistsInTarget: 'ກົດນີ້ມີຢູ່ແລ້ວໃນໄຟວໍເປົ້າໝາຍ.', onlyInTarget: 'ກົດນີ້ມີຢູ່ສະເພາະໃນໄຟວໍເປົ້າໝາຍ.', - stale: 'ສະຖານະກົດໄຟວໍເກົ່າແລ້ວ. ກະລຸນາໂຫຼດໃໝ່ແລ້ວລອງອີກຄັ້ງ.', - protectedRule: 'ກົດໄຟວໍທີ່ຖືກປົກປ້ອງນີ້ບໍ່ສາມາດແກ້ໄຂໄດ້.', - dockerAcceptReadOnly: - 'ກົດ ACCEPT ນີ້ເປັນແບບອ່ານຢ່າງດຽວ ແລະຈະຖືກເກັບໄວ້ເມື່ອຊິງກົດອື່ນ. ຫາກຕ້ອງການລຶບ ໃຫ້ລຶບດ້ວຍຕົນເອງໃນໂຮສ.', - cannotReconcile: 'ບໍ່ສາມາດຊິງກົດເປົ້າໝາຍ: {0}', }, ruleSyncPartial: 'ຊິງສຳເລັດ: ສຳເລັດ {0}, ມີແລ້ວ {1}, ລົ້ມເຫຼວ {2}.', ruleSyncSuccess: 'ຊິງສຳເລັດ: ສຳເລັດ {0}, ມີແລ້ວ {1}, ລຶບ {2}.', @@ -4227,17 +4220,17 @@ const message = { remove: 'ຈະລຶບ', blocked: 'ຊິງບໍ່ໄດ້', }, - resetDirectRulesHelper: - 'ລຶບ chain, ກົດທີ່ກຳລັງໃຊ້ ແລະ ໄຟລ໌ຖາວອນຂອງໄຟວໍລະບົບ 1Panel ຈາກ {0}; ນະໂຍບາຍໃນຖານຂໍ້ມູນຈະຖືກເກັບໄວ້', - resetWhitelistRulesHelper: - 'ຣີເຊັດການຕັ້ງຄ່າກຳນົດເອງທີ່ກຳລັງໃຊ້ໃນ {0}, ຄືນຄ່າເລີ່ມຕົ້ນ ແລະປິດ {0}; ນະໂຍບາຍໃນຖານຂໍ້ມູນຈະຖືກເກັບໄວ້ ແລະຊິງຄືນໄດ້.', + initializeFromFile: 'ເລີ່ມຕົ້ນຈາກໄຟລ໌', + downloadRuleBackup: 'ດາວໂຫຼດໄຟລ໌ສຳຮອງກົດ', + importFileHelper: + 'ນຳເຂົ້າໄຟລ໌ JSON ຂະໜາດບໍ່ເກີນ 64 MB ເປັນຊຸດຕາມລຳດັບໃນໄຟລ໌. ກົດທີ່ແປງບໍ່ໄດ້ຈະລາຍງານເປັນຂໍ້ຜິດພາດ.', + resetDirectRulesHelper: 'ລຶບເຊນໄຟວໍໂຮສ 1Panel, ກົດທີ່ໃຊ້ງານ ແລະ ໄຟລ໌ຖາວອນທັງໝົດໃນ {0}.', + resetWhitelistRulesHelper: 'ຣີເຊັດການຕັ້ງຄ່າຂອງ {0}, ກັບຄືນຄ່າເລີ່ມຕົ້ນ ແລະ ປິດໃຊ້ງານ.', cleanupForwardingBackendHelper: - 'ຣີເຊັດກົດ runtime ສຳລັບການສົ່ງຕໍ່ພອດ 1Panel ໃນ {0}: ລຶບກົດ ແລະ chain ທີ່ກ່ຽວຂ້ອງທັງໝົດ ແລະເກັບຂໍ້ມູນໃນຖານຂໍ້ມູນ', + 'ລຶບກົດສົ່ງຕໍ່, ຕ່ອງໂສ້ ແລະ ໄຟລ໌ກູ້ຄືນອັດຕະໂນມັດຂອງ 1Panel ທັງໝົດຈາກ {0}. ເກັບຂໍ້ມູນສຳຮອງ JSON ເດີມໄວ້.', cleanupDockerBackendHelper: - 'ຣີເຊັດກົດ runtime ປ້ອງກັນພອດ Docker ຂອງ 1Panel ໃນ {0}: ລຶບກົດ ແລະ chain ທີ່ກ່ຽວຂ້ອງທັງໝົດ ແລະເກັບຂໍ້ມູນໃນຖານຂໍ້ມູນ', - cleanupBeforeBackendSwitch: - 'ແບັກເອັນປັດຈຸບັນ {0} ຍັງມີກົດ runtime ຂອງ 1Panel. ກະລຸນາຣີເຊັດກ່ອນສະຫຼັບໄປ {1}. ການຣີເຊັດຈະລ້າງສະເພາະກົດ runtime; ນະໂຍບາຍໃນຖານຂໍ້ມູນຈະຖືກເກັບໄວ້ ແລະສາມາດເລີ່ມ ຫຼືຊິງຫຼັງສະຫຼັບ.', - cleanupAction: 'ຣີເຊັດ', + 'ລຶບກົດປ້ອງກັນ Docker, ຕ່ອງໂສ້ ແລະ ໄຟລ໌ກູ້ຄືນອັດຕະໂນມັດຂອງ 1Panel ທັງໝົດຈາກ {0}. ເກັບຂໍ້ມູນສຳຮອງ JSON ເດີມໄວ້.', + cleanupBeforeBackendSwitch: 'ກ່ອນປ່ຽນໄປ {1}, ໃຫ້ຣີເຊັດ {0} ປັດຈຸບັນ ແລະກວດສອບວິທີກູ້ຄືນກົດ.', backendSwitchNotice: 'ແນະນຳໃຫ້ເປີດໃຊ້ວິທີຈັດການໄຟວໍພຽງແບບດຽວ. ການໃຊ້ຫຼາຍໄຟວໍພ້ອມກັນອາດເຮັດໃຫ້ກົດຂັດແຍ່ງ, ສະຖານະບໍ່ກົງກັນ ຫຼື ການເຂົ້າເຖິງພອດຄອນເທນເນີຜິດປົກກະຕິ.', switchBackendHelper: 'ຢືນຢັນການສະຫຼັບເປັນ {0} ບໍ?', @@ -4281,8 +4274,7 @@ const message = { portDetails: 'ລາຍລະອຽດພອດ', orphanEndpoints: 'ຈຸດເຊື່ອມຕໍ່ທີ່ບໍ່ກ່ຽວຂ້ອງ', orphanPolicies: 'ກົດທີ່ບໍ່ໄດ້ເຊື່ອມໂຍງ', - orphanPoliciesHelper: - 'ກົດ {0} ລາຍການຕໍ່ໄປນີ້ຍັງບໍ່ໄດ້ຜູກກັບພອດ Docker. ກົດເຫຼົ່ານີ້ຈະຖືກຜູກອັດຕະໂນມັດເມື່ອກວດພົບພອດທີ່ກົງກັນ.', + orphanPoliciesHelper: 'ກົດລະບົບ {0} ລາຍການນີ້ບໍ່ມີພອດ Docker ທີ່ເຜີຍແຜ່ທີ່ກົງກັນ.', composeOrApp: 'Compose / ແອັບ', sources: 'ແຫຼ່ງ', deniedSources: 'ແຫຼ່ງທີ່ປະຕິເສດ', @@ -4291,6 +4283,8 @@ const message = { protectionMode: 'ໂໝດປ້ອງກັນ', denySources: 'ປະຕິເສດແຫຼ່ງທີ່ລະບຸ', allowSources: 'ອະນຸຍາດສະເພາະແຫຼ່ງທີ່ລະບຸ', + acceptSources: 'ອະນຸຍາດແຫຼ່ງທີ່ລະບຸໂດຍກົງ', + acceptAll: 'ອະນຸຍາດການເຂົ້າເຖິງທັງໝົດໂດຍກົງ', denyAll: 'ປະຕິເສດການເຂົ້າເຖິງທັງໝົດ', dockerGuardMixedFamilyHelper: 'ເລືອກທັງ IPv4 ແລະ IPv6 ແລ້ວ. ນະໂຍບາຍທີ່ລະບຸແຫຼ່ງທີ່ມາຕ້ອງຕັ້ງຄ່າແຍກກັນ; ການປະຕິເສດທັງໝົດສາມາດນຳໃຊ້ໄດ້ໂດຍກົງ.', @@ -4330,7 +4324,7 @@ const message = { 'ຍົກເລີກການຜູກມັດ - ເມື່ອຍົກເລີກ, ກົດລະບຽບໄຟວໍທັງໝົດຈະບໍ່ມີຜົນ. ກະລຸນາເຮັດດ້ວຍຄວາມລະມັດລະວັງ. ຢືນຢັນ?', portWhiteList: 'ພອດທີ່ອະນຸຍາດ', whitelistConfigHelper: - 'ລາຍການອະນຸຍາດບັນທຶກສະເພາະການຕັ້ງຄ່າ ແລະ ປົກປ້ອງກົດອະນຸຍາດທີ່ກົງກັນ. ກົດທີ່ຂາດຈະຖືກເພີ່ມເມື່ອເລີ່ມ, ເລີ່ມໃໝ່, ຕັ້ງຄ່າເບື້ອງຕົ້ນ ຫຼື ຊິງຂໍ້ມູນ. ການແກ້ໄຂ ຫຼື ລຶບລາຍການອະນຸຍາດຈະບໍ່ລຶບກົດທີ່ມີຢູ່.', + 'ການບັນທຶກລາຍຊື່ອະນຸຍາດຈະເພີ່ມກົດອະນຸຍາດທີ່ຂາດ ແລະປ້ອງກັນກົດທີ່ກົງກັນ. ການແກ້ໄຂ ຫຼືລຶບລາຍການຈະບໍ່ລຶບກົດທີ່ມີຢູ່.', whitelistDeleteConfirm: 'ຈະລຶບສະເພາະລາຍການບັນຊີຂາວນີ້. ກົດໄຟວໍທີ່ມີຢູ່ຈະຖືກເກັບໄວ້. ສືບຕໍ່ບໍ?', portWhiteListHelper: 'ປ້ອນພອດດຽວ (ເຊັ່ນ 80) ຫຼື ຊ່ວງພອດ (ເຊັ່ນ 8000-8100). ໝາຍເລກພອດຕ້ອງຢູ່ລະຫວ່າງ 1 ຫາ 65535.', chain: 'Chain', @@ -4345,8 +4339,7 @@ const message = { sourceAddressPlaceholder: 'ຕົວຢ່າງ: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 ຫຼື 2001:db8::/64', whitelistServicePortsHelper: 'ປ້ອນພອດດຽວ (1-65535). ການຕັ້ງຄ່ານີ້ຈະບໍ່ປ່ຽນພອດຮັບຟັງຂອງບໍລິການ. ເມື່ອປ່ຽນພອດໃນການຕັ້ງຄ່າບໍລິການ, ລາຍການບັນຊີຂາວນີ້ຈະຖືກອັບເດດດ້ວຍ.', - whitelistSourcesHelper: - 'ປ້ອນທີ່ຢູ່ IP ຫຼື CIDR ໂດຍແຍກດ້ວຍຈຸດຈ້ຳ ຫຼື ຂຶ້ນແຖວໃໝ່. ຖ້າປະໄວ້ຫວ່າງ ຈະອະນຸຍາດທຸກແຫຼ່ງທີ່ມາ IPv4 ແລະ IPv6.', + whitelistSourcesHelper: 'ປ້ອນທີ່ຢູ່ IP ຫຼື CIDR ໂດຍແຍກດ້ວຍຈຸດຈ້ຳ ຫຼື ຂຶ້ນແຖວໃໝ່. ຖ້າປະໄວ້ຫວ່າງ ຈະອະນຸຍາດ {0}.', destinationPortPlaceholder: 'ຕົວຢ່າງ: 80, 80,443 ຫຼື 8080-8089', deleteRuleConfirm: 'ຈະລຶບ {0} ກົດລະບຽບ. ຕ້ອງການຕໍ່ຫຼືບໍ່?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index d1d801f7e..6d94adac9 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -4314,49 +4314,43 @@ const message = { useEAB: 'Gunakan pengesahan EAB', }, firewall: { + ipv6Support: 'Sokongan IPv6', + ipv6SupportHelper: + 'Mengawal pengurusan dan penguatkuasaan peraturan tembok api IPv6 tanpa menjejaskan rangkaian IPv6 pelayan.', + ipv6DisableContainerHelper: 'Sembunyikan maklumat port IPv6 bekas.', + ipv6DisableWhitelistHelper: + 'Buang peraturan IPv6 daripada senarai dibenarkan dan kekalkan semua peraturan IPv6 sedia ada yang lain.', + ipv6DisableRulesHelper: 'Nyahdayakan peraturan IPv6 iptables dan nftables yang diuruskan oleh 1Panel.', + ipv6DisableNetworkHelper: 'Ini tidak menyahdayakan rangkaian IPv6 pelayan.', + ipv6DisableEffectHelper: 'Sekatan akses IPv6 dan pemajuan port yang terlibat tidak lagi berkuat kuasa.', + ipv6Disabled: 'Sokongan IPv6 dinyahdayakan. Dayakannya dalam tetapan tembok api dahulu.', + familyRepair: 'Baiki', + familyIncomplete: 'Rantaian {0} tidak lengkap', + resetWithBackup: 'Sandarkan peraturan dan tetapkan semula', + resetOnly: 'Tetapkan semula sahaja', + resetWithBackupHelper: 'Simpan peraturan modul semasa sebagai JSON untuk dipulihkan semasa pemulaan.', + resetOnlyHelper: 'Padam peraturan semasa tanpa sandaran baharu. Sandaran sedia ada dikekalkan.', + backupDirectoryHelper: + 'Sandaran peraturan yang sah dalam {0} disenaraikan di bawah. Fail terkini dipilih secara lalai.', + restoreAllRules: 'Mulakan dan pulihkan peraturan', + importRuleBackup: 'Import daripada sandaran', + initializeOnly: 'Mulakan sahaja', + backupRuleCount: 'Bilangan peraturan', + noRuleBackup: 'Tiada sandaran peraturan yang boleh dipulihkan', + ruleBackupLoadFailed: 'Gagal memuatkan sandaran peraturan. Cuba lagi atau mulakan tanpa memulihkan sandaran.', create: 'Buat peraturan', edit: 'Edit peraturan', quickJump: 'Akses pantas', used: 'Digunakan', unUsed: 'Tidak Digunakan', - managed: 'Dicipta panel', - managedHelper: 'Dicipta dan diselenggara oleh 1Panel. Peraturan ini boleh diedit atau dipadam.', - adopted: 'Luaran terurus', - adoptedHelper: 'Peraturan sistem sedia ada yang kini diselenggara oleh 1Panel.', - external: 'Peraturan luaran', - externalHelper: - 'Peraturan sistem sedia ada yang belum diurus oleh 1Panel. Ia boleh diambil alih apabila diperlukan.', - protected: 'Dilindungi sistem', - protectedHelper: - 'Peraturan terbina dalam dan peraturan yang sepadan tepat dengan senarai putih tidak boleh diedit atau dipadam.', - stateShort: { - managed: 'Panel', - adopted: 'Terurus', - external: 'Luaran', - protected: 'Dilindungi', - drifted: 'Berbeza', - }, + whitelist: 'Senarai dibenarkan', + builtinRuleProtected: 'Peraturan perlindungan terbina dalam sistem. Ia tidak boleh diedit atau dipadam.', + whitelistRuleProtected: + 'Peraturan senarai dibenarkan hanya membenarkan perubahan susunan dan penerangan. Untuk perubahan lain, pergi ke Tetapan → Senarai port yang dibenarkan.', ruleTargetRequired: 'Masukkan sekurang-kurangnya satu alamat IP atau port', - 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.', - adoptRuleConfirm: - 'Selepas diambil alih, 1Panel boleh menyelenggara dan memadam peraturan sedia ada ini. Teruskan?', - 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.', - plan_managed_rule_drifted: - 'Peraturan terurus tidak sepadan dengan tembok api aktif. Selesaikan perbezaan dahulu.', - plan_opaque_rule_in_target_scope: - 'Skop sasaran mengandungi peraturan yang tidak dapat dihuraikan dengan selamat. Operasi dihentikan.', - plan_runtime_permanent_mismatch: 'Konfigurasi tembok api aktif dan kekal berbeza. Segerakkan dahulu.', - plan_protected_rule: 'Peraturan ini dilindungi dan tidak boleh diambil alih, diubah atau dipadam.', - plan_blocked: 'Peraturan ini tidak dapat digunakan dengan selamat. Muat semula senarai dan cuba lagi.', largeRuleSet: 'Terlalu banyak peraturan tembok api boleh melambatkan operasi. Penggunaan iptables atau nftables disyorkan.', - scopeDefaultMismatch: 'Zon lalai sistem ialah {0}; halaman ini hanya mengurus zon public.', scopeMissing: 'Skop terurus {0} tiada dan akan dicipta dengan selamat apabila peraturan pertama digunakan.', - scopeUnmanagedActive: 'Skop aktif lain dikesan: {0}. 1Panel tidak akan mengubah peraturannya.', dockerRestart: 'Operasi firewall memerlukan memulakan semula perkhidmatan Docker', firewallHelper: '{0} firewall sistem', firewallNotStart: 'Firewall sistem belum diaktifkan. Aktifkannya dahulu.', @@ -4381,7 +4375,7 @@ const message = { systemFirewall: 'Tembok api hos', systemFirewallHelper: 'Mengawal akses port hos dan peraturan masuk.', forwardPolicyDropWarning: - 'Dasar FORWARD lalai untuk {0} ialah DROP. Trafik yang dimajukan ke hos lain mungkin disekat melainkan dibenarkan secara jelas oleh peraturan iptables/ip6tables. Semak peraturan kebenaran untuk versi IP yang berkenaan.', + 'Pelayan ini mengehadkan pemajuan rangkaian. Peraturan pemajuan port mungkin tidak berfungsi.', forwardingHelper: 'Mengurus peraturan pemajuan port.', dockerFirewallHelper: 'Memilih cara 1Panel mengurus perlindungan port bekas.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, percubaan', @@ -4390,6 +4384,8 @@ const message = { addressFamily: 'Versi IP', portOrRange: 'Port / julat', batchLimit: 'Cipta atau import maksimum {0} peraturan setiap kelompok selepas pengembangan.', + importDuplicatesRemoved: + '{0} peraturan pendua telah dibuang, termasuk pendua dalam fail dan peraturan yang sudah wujud.', importLimit: 'Import sehingga {0} peraturan (dikira selepas pengembangan), dengan saiz fail tidak melebihi {1} KB.', ruleSyncTitle: 'Segerakkan peraturan', @@ -4408,19 +4404,9 @@ const message = { ruleSyncBlocked: 'Tidak tersedia', ruleSyncReason: 'Hasil semakan', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'Peraturan sudah sepadan dengan dasar pangkalan data.', - 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.', missingFromTarget: 'Peraturan tiada dalam tembok api sasaran.', - targetDiffers: 'Peraturan sasaran berbeza daripada dasar pangkalan data.', alreadyExistsInTarget: 'Peraturan sudah wujud dalam tembok api sasaran.', onlyInTarget: 'Peraturan hanya wujud dalam tembok api sasaran.', - stale: 'Keadaan peraturan tembok api sudah lapuk. Muat semula dan cuba lagi.', - protectedRule: 'Peraturan tembok api yang dilindungi ini tidak boleh diubah.', - dockerAcceptReadOnly: - 'Peraturan ACCEPT ini adalah baca sahaja dan akan dikekalkan semasa peraturan lain disegerakkan. Untuk membuangnya, padam secara manual pada hos.', - cannotReconcile: 'Peraturan sasaran tidak dapat disegerakkan: {0}', }, ruleSyncPartial: 'Penyegerakan selesai: {0} berjaya, {1} sudah wujud dan {2} gagal.', ruleSyncSuccess: 'Penyegerakan selesai: {0} berjaya, {1} sudah wujud dan {2} dipadam.', @@ -4430,17 +4416,19 @@ const message = { remove: 'Untuk dipadam', blocked: 'Tidak tersedia', }, - resetDirectRulesHelper: - 'Padam rantaian, peraturan masa jalan dan fail berterusan tembok api sistem 1Panel daripada {0}; dasar pangkalan data yang disimpan dikekalkan', + initializeFromFile: 'Mulakan daripada fail', + downloadRuleBackup: 'Muat turun sandaran peraturan', + importFileHelper: + 'Import fail JSON sehingga 64 MB secara kelompok mengikut susunan fail. Peraturan yang tidak dapat ditukar akan dilaporkan sebagai ralat.', + resetDirectRulesHelper: 'Padam semua rantai tembok api hos 1Panel, peraturan aktif dan fail kekal dalam {0}.', resetWhitelistRulesHelper: - 'Tetapkan semula konfigurasi tersuai aktif dalam {0}, pulihkan tetapan pemasangan asal dan nyahdayakan {0}; dasar pangkalan data dikekalkan dan boleh disegerakkan semula.', + 'Tetapkan semula konfigurasi tersuai {0}, pulihkan tetapan lalai dan nyahaktifkannya.', cleanupForwardingBackendHelper: - 'Tetapkan semula peraturan masa jalan pemajuan port 1Panel dalam {0}: padam semua peraturan dan rantaian berkaitan sambil mengekalkan data pangkalan data', + 'Padam semua peraturan pemajuan, rantaian dan fail pemulihan automatik 1Panel daripada {0}. Sandaran JSON sedia ada dikekalkan.', cleanupDockerBackendHelper: - 'Tetapkan semula peraturan masa jalan perlindungan port Docker 1Panel dalam {0}: padam semua peraturan dan rantaian berkaitan sambil mengekalkan data pangkalan data', + 'Padam semua peraturan perlindungan Docker, rantaian dan fail pemulihan automatik 1Panel daripada {0}. Sandaran JSON sedia ada dikekalkan.', cleanupBeforeBackendSwitch: - 'Bahagian belakang semasa {0} masih mengandungi peraturan masa jalan 1Panel. Tetapkan semula sebelum bertukar kepada {1}. Penetapan semula hanya membersihkan peraturan masa jalan; dasar pangkalan data dikekalkan dan boleh dimulakan atau disegerakkan selepas penukaran.', - cleanupAction: 'Tetapkan semula', + 'Tetapkan semula {0} semasa sebelum beralih kepada {1} dan sahkan cara peraturannya akan dipulihkan.', backendSwitchNotice: 'Disyorkan untuk mengaktifkan hanya satu kaedah pengurusan tembok api. Menjalankan berbilang tembok api serentak boleh menyebabkan konflik peraturan, status tidak konsisten atau masalah akses port kontena.', switchBackendHelper: 'Tukar kepada {0}?', @@ -4486,8 +4474,7 @@ const message = { portDetails: 'Butiran port', orphanEndpoints: 'Titik akhir tidak berkaitan', orphanPolicies: 'Peraturan tidak berkaitan', - orphanPoliciesHelper: - '{0} peraturan berikut belum terikat pada port Docker. Peraturan akan diikat secara automatik apabila port sepadan dikesan.', + orphanPoliciesHelper: '{0} peraturan sistem ini tiada port Docker terbitan yang sepadan.', composeOrApp: 'Compose / Aplikasi', sources: 'Sumber', deniedSources: 'Sumber ditolak', @@ -4496,6 +4483,8 @@ const message = { protectionMode: 'Mod perlindungan', denySources: 'Tolak sumber tertentu', allowSources: 'Benarkan sumber tertentu sahaja', + acceptSources: 'Terima sumber yang ditentukan secara langsung', + acceptAll: 'Terima semua trafik secara langsung', denyAll: 'Tolak semua akses', dockerGuardMixedFamilyHelper: 'IPv4 dan IPv6 dipilih. Konfigurasikan dasar berasaskan sumber secara berasingan; tolak semua boleh digunakan terus.', @@ -4538,7 +4527,7 @@ const message = { 'Nyahikat - Apabila tidak terikat, semua peraturan firewall yang ditambah akan menjadi tidak sah. Teruskan dengan berhati-hati. Sahkan?', portWhiteList: 'Senarai putih port', whitelistConfigHelper: - 'Senarai dibenarkan hanya menyimpan konfigurasi dan melindungi peraturan kebenaran yang sepadan. Peraturan yang tiada ditambah semasa mula, mula semula, pemulaan atau penyegerakan. Mengedit atau memadam entri tidak membuang peraturan sedia ada.', + 'Menyimpan senarai dibenarkan menambah peraturan kebenaran yang tiada dan melindungi peraturan yang sepadan. Mengedit atau memadam entri senarai tidak memadam peraturan sedia ada.', whitelistDeleteConfirm: 'Hanya entri senarai putih ini akan dipadam. Peraturan tembok api sedia ada akan dikekalkan. Teruskan?', portWhiteListHelper: @@ -4556,7 +4545,7 @@ const message = { whitelistServicePortsHelper: 'Masukkan satu port (1-65535). Ini tidak mengubah port pendengaran perkhidmatan. Menukar port dalam tetapan perkhidmatan turut mengemas kini entri senarai putih ini.', whitelistSourcesHelper: - 'Masukkan alamat IP atau julat CIDR, dipisahkan dengan koma atau baris baharu. Biarkan kosong untuk membenarkan semua sumber IPv4 dan IPv6.', + 'Masukkan alamat IP atau julat CIDR, dipisahkan dengan koma atau baris baharu. Biarkan kosong untuk membenarkan {0}.', destinationPortPlaceholder: 'contoh: 80, 80,443 atau 8080-8089', deleteRuleConfirm: 'Akan memadam {0} peraturan. Teruskan?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 2f13c8ef9..5122713f5 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -4334,48 +4334,45 @@ const message = { useEAB: 'Usar autenticação EAB', }, firewall: { + ipv6Support: 'Suporte a IPv6', + ipv6SupportHelper: + 'Controla o gerenciamento e a aplicação das regras de firewall IPv6 sem afetar a rede IPv6 do servidor.', + ipv6DisableContainerHelper: 'Ocultar as informações de portas IPv6 dos contêineres.', + ipv6DisableWhitelistHelper: + 'Remover as regras IPv6 da lista de permissões e manter as demais regras IPv6 existentes.', + ipv6DisableRulesHelper: 'Desativar as regras IPv6 de iptables e nftables gerenciadas pelo 1Panel.', + ipv6DisableNetworkHelper: 'Isso não desativa a rede IPv6 do servidor.', + ipv6DisableEffectHelper: + 'As restrições de acesso IPv6 e o encaminhamento de portas afetados deixarão de ser aplicados.', + ipv6Disabled: 'O suporte a IPv6 está desativado. Ative-o primeiro nas configurações do firewall.', + familyRepair: 'Reparar', + familyIncomplete: 'As cadeias de {0} estão incompletas', + resetWithBackup: 'Salvar regras e redefinir', + resetOnly: 'Apenas redefinir', + resetWithBackupHelper: 'Salve as regras do módulo atual em JSON para restaurá-las durante a inicialização.', + resetOnlyHelper: 'Limpar as regras atuais sem criar um novo backup. Os backups existentes são mantidos.', + backupDirectoryHelper: + 'Os backups válidos de regras em {0} são listados abaixo. O mais recente é selecionado por padrão.', + restoreAllRules: 'Inicializar e restaurar regras', + importRuleBackup: 'Importar do backup', + initializeOnly: 'Apenas inicializar', + backupRuleCount: 'Número de regras', + noRuleBackup: 'Nenhum backup de regras disponível para restauração', + ruleBackupLoadFailed: + 'Não foi possível carregar os backups das regras. Tente novamente ou inicialize sem restaurar um backup.', create: 'Criar regra', edit: 'Editar regra', quickJump: 'Acesso rápido', used: 'Usado', unUsed: 'Não usado', - managed: 'Criada pelo painel', - managedHelper: 'Criada e mantida pelo 1Panel. Pode ser editada ou excluída.', - adopted: 'Externa gerenciada', - adoptedHelper: 'Uma regra existente do sistema que agora é mantida pelo 1Panel.', - external: 'Regra externa', - externalHelper: - 'Regra existente do sistema que ainda não é gerenciada pelo 1Panel. Pode ser adotada quando necessário.', - protected: 'Protegida pelo sistema', - protectedHelper: - 'As regras integradas e as que correspondem exatamente à lista de permissões não podem ser editadas nem excluídas.', - stateShort: { - managed: 'Painel', - adopted: 'Gerenciada', - external: 'Externa', - protected: 'Protegida', - drifted: 'Divergente', - }, + whitelist: 'Lista de permissões', + builtinRuleProtected: 'Regra de proteção integrada do sistema. Não pode ser editada nem excluída.', + whitelistRuleProtected: + 'Regras da lista de permissões permitem alterar apenas a ordem e a descrição. Para outras alterações, acesse Configurações → Lista de portas permitidas.', ruleTargetRequired: 'Informe pelo menos um endereço IP ou uma porta', - 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.', - adoptRuleConfirm: 'Depois disso, o 1Panel poderá manter e excluir esta regra existente. Continuar?', - 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.', - plan_managed_rule_drifted: - 'A regra gerenciada não corresponde ao firewall ativo. Resolva a divergência primeiro.', - plan_opaque_rule_in_target_scope: - 'O escopo contém uma regra que não pode ser analisada com segurança. A operação foi interrompida.', - plan_runtime_permanent_mismatch: 'As configurações ativa e permanente são diferentes. Sincronize-as primeiro.', - plan_protected_rule: 'Esta regra está protegida e não pode ser assumida, alterada ou excluída.', - plan_blocked: 'A regra não pode ser aplicada com segurança. Atualize a lista e tente novamente.', largeRuleSet: 'Regras de firewall em excesso podem tornar as operações lentas. Recomendamos usar iptables ou nftables.', - scopeDefaultMismatch: 'A zona padrão do sistema é {0}; esta página gerencia apenas a zona public.', scopeMissing: 'O escopo gerenciado {0} não existe e será criado com segurança ao aplicar a primeira regra.', - scopeUnmanagedActive: 'Outros escopos ativos foram detectados: {0}. O 1Panel não modificará suas regras.', dockerRestart: 'Operações de firewall exigem reinicialização do serviço Docker', firewallHelper: 'Firewall do sistema {0}', firewallNotStart: 'O firewall do sistema não está habilitado atualmente. Habilite-o primeiro.', @@ -4399,7 +4396,7 @@ const message = { systemFirewall: 'Firewall do host', systemFirewallHelper: 'Controla o acesso às portas do host e as regras de entrada.', forwardPolicyDropWarning: - 'A política padrão de FORWARD para {0} é DROP. O tráfego encaminhado para outros hosts pode ser bloqueado se não for permitido explicitamente pelas regras do iptables/ip6tables. Verifique as regras de permissão da versão IP correspondente.', + 'Este servidor restringe o encaminhamento de rede. As regras de encaminhamento de portas podem não funcionar.', forwardingHelper: 'Gerencia regras de encaminhamento de portas.', dockerFirewallHelper: 'Seleciona como o 1Panel gerencia a proteção de portas de contêineres.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, experimental', @@ -4408,6 +4405,8 @@ const message = { addressFamily: 'Versão do IP', portOrRange: 'Porta / intervalo', batchLimit: 'Crie ou importe no máximo {0} regras por lote, após a expansão.', + importDuplicatesRemoved: + 'Foram removidas {0} regras duplicadas, incluindo repetições no arquivo e regras já existentes.', importLimit: 'Importe até {0} regras (contadas após a expansão), com um arquivo de no máximo {1} KB.', ruleSyncTitle: 'Sincronizar regras', ruleSyncAction: 'Sincronizar regras', @@ -4425,19 +4424,9 @@ const message = { ruleSyncBlocked: 'Indisponíveis', ruleSyncReason: 'Resultado da verificação', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'A regra já corresponde à política do banco de dados.', - 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.', 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.', onlyInTarget: 'A regra existe apenas no firewall de destino.', - stale: 'O estado da regra está desatualizado. Atualize e tente novamente.', - protectedRule: 'Esta regra protegida do firewall não pode ser modificada.', - dockerAcceptReadOnly: - 'Esta regra ACCEPT é somente leitura e será preservada durante a sincronização das outras regras. Para removê-la, exclua-a manualmente no host.', - cannotReconcile: 'Não foi possível sincronizar a regra de destino: {0}', }, ruleSyncPartial: 'Sincronização concluída: {0} com sucesso, {1} já existiam e {2} falharam.', ruleSyncSuccess: 'Sincronização concluída: {0} com sucesso, {1} já existiam e {2} removidas.', @@ -4447,17 +4436,20 @@ const message = { remove: 'A remover', blocked: 'Indisponível', }, + initializeFromFile: 'Inicializar pelo arquivo', + downloadRuleBackup: 'Baixar backup das regras', + importFileHelper: + 'Importe arquivos JSON de até 64 MB em lotes, na ordem do arquivo. Regras que não puderem ser convertidas serão informadas como erros.', resetDirectRulesHelper: - 'Exclua de {0} as cadeias, regras em execução e arquivos persistentes do firewall do 1Panel; as políticas salvas no banco são mantidas', + 'Excluir todas as cadeias do firewall do host do 1Panel, regras ativas e arquivos persistentes de {0}.', resetWhitelistRulesHelper: - 'Redefina a configuração personalizada ativa em {0}, restaure os padrões de instalação e desative {0}; as políticas do banco são mantidas e podem ser sincronizadas novamente.', + 'Redefinir as configurações personalizadas de {0}, restaurar os padrões e desativá-lo.', cleanupForwardingBackendHelper: - 'Redefina as regras de execução de encaminhamento de portas do 1Panel em {0}: exclua todas as regras e cadeias relacionadas e mantenha os dados do banco de dados', + 'Exclua de {0} todas as regras e cadeias de encaminhamento do 1Panel e os arquivos de recuperação automática. Os backups JSON existentes são mantidos.', cleanupDockerBackendHelper: - 'Redefina as regras de execução de proteção de portas Docker do 1Panel em {0}: exclua todas as regras e cadeias relacionadas e mantenha os dados do banco de dados', + 'Exclua de {0} todas as regras e cadeias de proteção Docker do 1Panel e os arquivos de recuperação automática. Os backups JSON existentes são mantidos.', cleanupBeforeBackendSwitch: - 'O backend atual {0} ainda contém regras de execução do 1Panel. Redefina-o antes de mudar para {1}. A redefinição remove apenas as regras de execução; as políticas do banco são mantidas e podem ser inicializadas ou sincronizadas após a troca.', - cleanupAction: 'Redefinir', + 'Redefina o {0} atual antes de mudar para {1} e confirme como as regras serão restauradas.', backendSwitchNotice: 'Recomenda-se habilitar apenas uma forma de gerenciamento de firewall. Executar vários firewalls ao mesmo tempo pode causar conflitos de regras, estados inconsistentes ou falhas de acesso às portas dos contêineres.', switchBackendHelper: 'Mudar para {0}?', @@ -4505,8 +4497,7 @@ const message = { portDetails: 'Detalhes da porta', orphanEndpoints: 'Endpoints não associados', orphanPolicies: 'Regras não associadas', - orphanPoliciesHelper: - 'As {0} regra(s) a seguir não estão vinculadas a uma porta Docker. Elas serão vinculadas automaticamente quando uma porta correspondente for detectada.', + orphanPoliciesHelper: 'Estas {0} regras do sistema não têm uma porta publicada do Docker correspondente.', composeOrApp: 'Compose / Aplicativo', sources: 'Origens', deniedSources: 'Origens negadas', @@ -4515,6 +4506,8 @@ const message = { protectionMode: 'Modo de proteção', denySources: 'Negar origens especificadas', allowSources: 'Permitir somente origens especificadas', + acceptSources: 'Aceitar diretamente as origens especificadas', + acceptAll: 'Aceitar diretamente todo o tráfego', denyAll: 'Negar todo o acesso', dockerGuardMixedFamilyHelper: 'IPv4 e IPv6 estão selecionados. Configure as políticas baseadas em origem separadamente; bloquear tudo pode ser aplicado diretamente.', @@ -4560,7 +4553,7 @@ const message = { 'Desvincular - Quando desvinculado, todas as regras de firewall adicionadas se tornarão inválidas. Prossiga com cautela. Confirmar?', portWhiteList: 'Lista branca de portas', whitelistConfigHelper: - 'A lista de permissões apenas salva a configuração e protege as regras de permissão correspondentes. As regras ausentes são adicionadas ao iniciar, reiniciar, inicializar ou sincronizar. Editar ou excluir entradas não remove as regras existentes.', + 'Salvar a lista de permissões adiciona as regras de permissão ausentes e protege as correspondentes. Editar ou excluir entradas da lista não remove regras existentes.', whitelistDeleteConfirm: 'Apenas esta entrada da lista de permissões será excluída. As regras existentes do firewall serão mantidas. Continuar?', portWhiteListHelper: @@ -4578,7 +4571,7 @@ const message = { whitelistServicePortsHelper: 'Informe uma única porta (1-65535). Isso não altera a porta de escuta do serviço. Alterar a porta nas configurações do serviço também atualiza esta entrada da lista de permissões.', whitelistSourcesHelper: - 'Insira endereços IP ou intervalos CIDR separados por vírgulas ou quebras de linha. Deixe em branco para permitir todas as origens IPv4 e IPv6.', + 'Insira endereços IP ou intervalos CIDR separados por vírgulas ou quebras de linha. Deixe em branco para permitir {0}.', destinationPortPlaceholder: 'por exemplo: 80, 80,443 ou 8080-8089', deleteRuleConfirm: 'Excluirá {0} regras. Continuar?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index ebe69a2d2..5aa93c5d9 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -4300,48 +4300,45 @@ const message = { useEAB: 'Использовать аутентификацию EAB', }, firewall: { + ipv6Support: 'Поддержка IPv6', + ipv6SupportHelper: + 'Управляет настройкой и применением правил межсетевого экрана IPv6, не влияя на работу сети IPv6 на сервере.', + ipv6DisableContainerHelper: 'Скрыть информацию о портах IPv6 контейнеров.', + ipv6DisableWhitelistHelper: + 'Удалить правила IPv6 из белого списка, сохранив остальные существующие правила IPv6.', + ipv6DisableRulesHelper: 'Отключить правила IPv6 в iptables и nftables, управляемые 1Panel.', + ipv6DisableNetworkHelper: 'Это действие не отключает сеть IPv6 на сервере.', + ipv6DisableEffectHelper: + 'Соответствующие ограничения доступа IPv6 и перенаправление портов перестанут действовать.', + ipv6Disabled: 'Поддержка IPv6 отключена. Сначала включите её в настройках межсетевого экрана.', + familyRepair: 'Восстановить', + familyIncomplete: 'Цепочки {0} неполны', + resetWithBackup: 'Сохранить правила и сбросить', + resetOnly: 'Только сбросить', + resetWithBackupHelper: 'Сохраните правила текущего модуля в JSON для восстановления при инициализации.', + resetOnlyHelper: 'Удалить текущие правила без создания новой копии. Существующие копии сохраняются.', + backupDirectoryHelper: + 'Ниже перечислены действительные резервные копии правил из {0}. По умолчанию выбрана самая новая.', + restoreAllRules: 'Инициализировать и восстановить правила', + importRuleBackup: 'Импортировать из резервной копии', + initializeOnly: 'Только инициализировать', + backupRuleCount: 'Число правил', + noRuleBackup: 'Резервные копии правил для восстановления не найдены', + ruleBackupLoadFailed: + 'Не удалось загрузить резервные копии правил. Повторите попытку или выполните инициализацию без восстановления.', create: 'Создать правило', edit: 'Редактировать правило', quickJump: 'Быстрый доступ', used: 'Используется', unUsed: 'Не используется', - managed: 'Создано в панели', - managedHelper: 'Создано и поддерживается 1Panel. Правило можно изменить или удалить.', - adopted: 'Внешнее под управлением', - adoptedHelper: 'Существующее системное правило, которое теперь поддерживается 1Panel.', - external: 'Внешнее правило', - externalHelper: - 'Существующее системное правило, которое пока не управляется 1Panel. При необходимости его можно принять под управление.', - protected: 'Защищено системой', - protectedHelper: - 'Встроенные правила и правила, точно совпадающие с белым списком, нельзя изменять или удалять.', - stateShort: { - managed: 'Панель', - adopted: 'Принято', - external: 'Внешнее', - protected: 'Защита', - drifted: 'Различие', - }, + whitelist: 'Белый список', + builtinRuleProtected: 'Встроенное защитное правило системы. Его нельзя изменить или удалить.', + whitelistRuleProtected: + 'У правил белого списка можно изменить только порядок и описание. Для других изменений перейдите в «Настройки → Белый список портов».', ruleTargetRequired: 'Укажите хотя бы один IP-адрес или порт', - resolution_adopt: 'Взять под управление', - plan_duplicate_rules: - 'Нельзя принять под управление правила с одинаковыми условиями и действиями. Удалите дубликаты вручную и повторите попытку.', - adoptRuleConfirm: 'После этого 1Panel сможет обслуживать и удалять существующее правило. Продолжить?', - plan_exact_rule_conflict: - 'Уже существует правило с теми же условиями, но с противоположным действием разрешения или запрета.', - allRulesAlreadyExist: 'Все проверенные правила ({0}) уже существуют. Новых правил для создания нет.', - plan_managed_rule_drifted: - 'Управляемое правило не совпадает с активным межсетевым экраном. Сначала устраните расхождение.', - plan_opaque_rule_in_target_scope: - 'В целевой области есть правило, которое нельзя безопасно разобрать. Операция остановлена.', - plan_runtime_permanent_mismatch: 'Активная и постоянная конфигурации различаются. Сначала синхронизируйте их.', - plan_protected_rule: 'Правило защищено: его нельзя взять под управление, изменить или удалить.', - plan_blocked: 'Правило нельзя безопасно применить. Обновите список и повторите попытку.', largeRuleSet: 'Слишком большое количество правил брандмауэра может замедлить операции. Рекомендуется использовать iptables или nftables.', - scopeDefaultMismatch: 'Системная зона по умолчанию — {0}; на этой странице управляется только зона public.', scopeMissing: 'Управляемая область {0} отсутствует и будет безопасно создана при применении первого правила.', - scopeUnmanagedActive: 'Обнаружены другие активные области: {0}. 1Panel не будет изменять их правила.', dockerRestart: 'Операции с брандмауэром требуют перезапуска службы Docker', firewallHelper: '{0} межсетевой экран', firewallNotStart: 'Межсетевой экран в настоящее время не включен. Сначала включите его.', @@ -4365,7 +4362,7 @@ const message = { systemFirewall: 'Брандмауэр хоста', systemFirewallHelper: 'Управляет доступом к портам хоста и входящими правилами.', forwardPolicyDropWarning: - 'Политика FORWARD по умолчанию для {0} — DROP. Трафик, пересылаемый на другие узлы, может блокироваться, если он явно не разрешён правилами iptables/ip6tables. Проверьте разрешающие правила для соответствующей версии IP.', + 'На этом сервере ограничена пересылка сетевого трафика. Правила перенаправления портов могут не работать.', forwardingHelper: 'Управляет правилами перенаправления портов.', dockerFirewallHelper: 'Выбирает способ управления защитой портов контейнеров в 1Panel.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, экспериментально', @@ -4374,6 +4371,7 @@ const message = { addressFamily: 'Версия IP', portOrRange: 'Порт / диапазон', batchLimit: 'За один раз можно создать или импортировать не более {0} правил после развёртывания.', + importDuplicatesRemoved: 'Удалено дубликатов правил: {0}, включая повторы в файле и уже существующие правила.', importLimit: 'Можно импортировать до {0} правил (после развёртывания). Размер файла не должен превышать {1} КБ.', ruleSyncTitle: 'Синхронизация правил', @@ -4392,19 +4390,9 @@ const message = { ruleSyncBlocked: 'Недоступно', ruleSyncReason: 'Результат проверки', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'Правило уже соответствует политике базы данных.', - managedOrderDiffers: 'Порядок управляемых правил отличается от порядка в базе данных.', - managedOnlyInTarget: 'Управляемое правило существует только в целевом брандмауэре.', - managedRuntimeCannotRemove: 'Активное управляемое правило нельзя безопасно удалить.', missingFromTarget: 'Правило отсутствует в целевом брандмауэре.', - targetDiffers: 'Целевое правило отличается от политики базы данных.', alreadyExistsInTarget: 'Правило уже существует в целевом брандмауэре.', onlyInTarget: 'Правило существует только в целевом брандмауэре.', - stale: 'Состояние правила устарело. Обновите данные и повторите попытку.', - protectedRule: 'Это защищённое правило брандмауэра нельзя изменить.', - dockerAcceptReadOnly: - 'Это правило ACCEPT доступно только для чтения и будет сохранено при синхронизации остальных правил. Чтобы удалить его, сделайте это вручную на хосте.', - cannotReconcile: 'Не удалось синхронизировать целевое правило: {0}', }, ruleSyncPartial: 'Синхронизация завершена: успешно — {0}, уже существовало — {1}, ошибок — {2}.', ruleSyncSuccess: 'Синхронизация завершена: успешно — {0}, уже существовало — {1}, удалено — {2}.', @@ -4414,17 +4402,20 @@ const message = { remove: 'К удалению', blocked: 'Недоступно', }, + initializeFromFile: 'Инициализировать из файла', + downloadRuleBackup: 'Скачать резервную копию правил', + importFileHelper: + 'Импорт JSON-файлов до 64 МБ выполняется пакетами в порядке файла. Для правил, которые нельзя преобразовать, выводятся ошибки.', resetDirectRulesHelper: - 'Удалить из {0} системные цепочки, действующие правила и постоянные файлы 1Panel; политики в базе данных сохраняются', + 'Удалить все цепочки межсетевого экрана хоста 1Panel, действующие правила и файлы сохранения в {0}.', resetWhitelistRulesHelper: - 'Сбросить активную пользовательскую конфигурацию {0}, восстановить исходные настройки и отключить {0}; политики базы данных сохраняются и могут быть синхронизированы повторно.', + 'Сбросить пользовательские настройки {0}, восстановить значения по умолчанию и отключить его.', cleanupForwardingBackendHelper: - 'Сбросить правила времени выполнения переадресации портов 1Panel в {0}: удалить все связанные правила и цепочки, сохранив данные в базе данных', + 'Удалить все правила и цепочки перенаправления 1Panel и файлы автоматического восстановления из {0}. Существующие резервные копии JSON сохраняются.', cleanupDockerBackendHelper: - 'Сбросить правила времени выполнения защиты портов Docker 1Panel в {0}: удалить все связанные правила и цепочки, сохранив данные в базе данных', + 'Удалить все правила и цепочки защиты Docker в 1Panel и файлы автоматического восстановления из {0}. Существующие резервные копии JSON сохраняются.', cleanupBeforeBackendSwitch: - 'Текущий бэкенд {0} всё ещё содержит правила времени выполнения 1Panel. Сбросьте его перед переключением на {1}. Сброс удаляет только правила времени выполнения; политики базы данных сохраняются и после переключения могут быть повторно инициализированы или синхронизированы.', - cleanupAction: 'Сбросить', + 'Перед переключением на {1} сбросьте текущий {0} и проверьте способ восстановления правил.', backendSwitchNotice: 'Рекомендуется использовать только один способ управления брандмауэром. Одновременная работа нескольких брандмауэров может привести к конфликтам правил, несогласованному состоянию или проблемам доступа к портам контейнеров.', switchBackendHelper: 'Переключиться на {0}?', @@ -4472,8 +4463,7 @@ const message = { portDetails: 'Сведения о порте', orphanEndpoints: 'Несвязанные конечные точки', orphanPolicies: 'Несвязанные правила', - orphanPoliciesHelper: - 'Следующие правила ({0}) не привязаны к порту Docker. Они будут привязаны автоматически при обнаружении подходящего порта.', + orphanPoliciesHelper: 'У этих системных правил ({0}) нет соответствующих опубликованных портов Docker.', composeOrApp: 'Compose / Приложение', sources: 'Источники', deniedSources: 'Запрещенные источники', @@ -4482,6 +4472,8 @@ const message = { protectionMode: 'Режим защиты', denySources: 'Запретить указанные источники', allowSources: 'Разрешить только указанные источники', + acceptSources: 'Напрямую разрешить указанные источники', + acceptAll: 'Напрямую разрешить весь трафик', denyAll: 'Запретить любой доступ', dockerGuardMixedFamilyHelper: 'Выбраны IPv4 и IPv6. Правила с указанными источниками настройте отдельно; запретить любой доступ можно сразу.', @@ -4526,7 +4518,7 @@ const message = { 'Отвязать - При отвязке все добавленные правила брандмауэра станут недействительными. Действуйте осторожно. Подтвердить?', portWhiteList: 'Белый список портов', whitelistConfigHelper: - 'Список разрешений только сохраняет настройки и защищает совпадающие разрешающие правила. Недостающие правила добавляются при запуске, перезапуске, инициализации или синхронизации. Изменение или удаление записей списка не удаляет существующие правила.', + 'При сохранении списка разрешений добавляются недостающие разрешающие правила и защищаются совпадающие правила. Изменение или удаление записей списка не удаляет существующие правила.', whitelistDeleteConfirm: 'Будет удалена только эта запись белого списка. Существующие правила брандмауэра сохранятся. Продолжить?', portWhiteListHelper: @@ -4544,7 +4536,7 @@ const message = { whitelistServicePortsHelper: 'Введите один порт (1-65535). Порт прослушивания службы не изменится. Изменение порта в настройках службы также обновит эту запись белого списка.', whitelistSourcesHelper: - 'Введите IP-адреса или подсети CIDR через запятую или с новой строки. Оставьте поле пустым, чтобы разрешить все источники IPv4 и IPv6.', + 'Введите IP-адреса или подсети CIDR через запятую или с новой строки. Оставьте поле пустым, чтобы разрешить {0}.', destinationPortPlaceholder: 'например: 80, 80,443 или 8080-8089', deleteRuleConfirm: 'Удалит {0} правил. Продолжить?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 4ed21517b..f2505ffbb 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -4320,45 +4320,43 @@ const message = { useEAB: 'EAB kimlik doğrulamasını kullan', }, firewall: { + ipv6Support: 'IPv6 desteği', + ipv6SupportHelper: + 'Sunucunun IPv6 ağ işlevlerini etkilemeden IPv6 güvenlik duvarı kurallarının yönetimini ve uygulanmasını kontrol eder.', + ipv6DisableContainerHelper: 'Konteynerlerin IPv6 port bilgilerini gizleyin.', + ipv6DisableWhitelistHelper: + 'İzin listesindeki IPv6 kurallarını kaldırın ve mevcut diğer IPv6 kurallarını koruyun.', + ipv6DisableRulesHelper: '1Panel tarafından yönetilen iptables ve nftables IPv6 kurallarını devre dışı bırakın.', + ipv6DisableNetworkHelper: 'Bu işlem sunucunun IPv6 ağ işlevlerini kapatmaz.', + ipv6DisableEffectHelper: 'İlgili IPv6 erişim kısıtlamaları ve port yönlendirmeleri artık uygulanmaz.', + ipv6Disabled: 'IPv6 desteği devre dışı. Önce güvenlik duvarı ayarlarından etkinleştirin.', + familyRepair: 'Onar', + familyIncomplete: '{0} zincirleri eksik', + resetWithBackup: 'Kuralları yedekle ve sıfırla', + resetOnly: 'Yalnızca sıfırla', + resetWithBackupHelper: 'Geçerli modül kurallarını başlatma sırasında geri yüklemek için JSON olarak kaydedin.', + resetOnlyHelper: 'Yeni yedek oluşturmadan mevcut kuralları temizleyin. Önceki yedekler korunur.', + backupDirectoryHelper: + '{0} içindeki geçerli kural yedekleri listelenir. Varsayılan olarak en yeni dosya seçilir.', + restoreAllRules: 'Başlat ve kuralları geri yükle', + importRuleBackup: 'Yedekten içe aktar', + initializeOnly: 'Yalnızca başlat', + backupRuleCount: 'Kural sayısı', + noRuleBackup: 'Geri yüklenebilir kural yedeği bulunamadı', + ruleBackupLoadFailed: 'Kural yedekleri yüklenemedi. Yeniden deneyin veya yedeği geri yüklemeden başlatın.', create: 'Kural oluştur', edit: 'Kuralı düzenle', quickJump: 'Hızlı erişim', used: 'Kullanıldı', unUsed: 'Kullanılmadı', - managed: 'Panel tarafından oluşturuldu', - managedHelper: '1Panel tarafından oluşturulur ve yönetilir. Düzenlenebilir veya silinebilir.', - adopted: 'Harici yönetilen', - adoptedHelper: 'Önceden sistemde bulunan ve artık 1Panel tarafından yönetilen bir kuraldır.', - external: 'Harici kural', - externalHelper: - 'Sistemde bulunan ancak henüz 1Panel tarafından yönetilmeyen bir kuraldır. Gerektiğinde yönetime alınabilir.', - protected: 'Sistem korumalı', - protectedHelper: 'Yerleşik kurallar ve beyaz listeyle tam olarak eşleşen kurallar düzenlenemez veya silinemez.', - stateShort: { - managed: 'Panel', - adopted: 'Yönetilen', - external: 'Harici', - protected: 'Korumalı', - drifted: 'Farklı', - }, + whitelist: 'İzin listesi', + builtinRuleProtected: 'Sistemin yerleşik koruma kuralıdır. Düzenlenemez veya silinemez.', + whitelistRuleProtected: + 'İzin listesi kurallarında yalnızca sıra ve açıklama değiştirilebilir. Diğer değişiklikler için Ayarlar → İzin verilen bağlantı noktaları bölümüne gidin.', ruleTargetRequired: 'En az bir IP adresi veya port girin', - 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.', - adoptRuleConfirm: 'Devraldıktan sonra 1Panel bu mevcut kuralı yönetebilir ve silebilir. Devam edilsin mi?', - 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.', - 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.', - plan_protected_rule: 'Bu kural korumalıdır; yönetimi devralınamaz, değiştirilemez veya silinemez.', - plan_blocked: 'Bu kural güvenle uygulanamıyor. Listeyi yenileyip tekrar deneyin.', largeRuleSet: 'Çok fazla güvenlik duvarı kuralı işlemleri yavaşlatabilir. iptables veya nftables kullanılması önerilir.', - scopeDefaultMismatch: 'Sistemin varsayılan zone değeri {0}; bu sayfa yalnızca public zone alanını yönetir.', scopeMissing: 'Yönetilen {0} kapsamı eksik ve ilk kural uygulanırken güvenli şekilde oluşturulacak.', - scopeUnmanagedActive: 'Başka etkin kapsamlar algılandı: {0}. 1Panel bunların kurallarını değiştirmez.', dockerRestart: 'Güvenlik duvarı işlemleri Docker hizmetinin yeniden başlatılmasını gerektirir', firewallHelper: '{0} sistem güvenlik duvarı', firewallNotStart: 'Sistem güvenlik duvarı şu anda etkin değil. Önce etkinleştirin.', @@ -4381,8 +4379,7 @@ const message = { dockerGuard: 'Konteyner portu koruması', systemFirewall: 'Ana makine güvenlik duvarı', systemFirewallHelper: 'Ana makine port erişimini ve gelen kuralları yönetir.', - forwardPolicyDropWarning: - '{0} için varsayılan FORWARD ilkesi DROP olarak ayarlanmış. Diğer ana bilgisayarlara yönlendirilen trafik, iptables/ip6tables kurallarıyla açıkça izin verilmediği sürece engellenebilir. İlgili IP sürümünün izin kurallarını kontrol edin.', + forwardPolicyDropWarning: 'Bu sunucu ağ yönlendirmesini kısıtlıyor. Port yönlendirme kuralları çalışmayabilir.', forwardingHelper: 'Port yönlendirme kurallarını yönetir.', dockerFirewallHelper: '1Panel konteyner portu korumasının nasıl yönetileceğini seçer.', dockerNftablesRequirement: 'Docker ≥ 29.0.0, deneysel', @@ -4391,6 +4388,8 @@ const message = { addressFamily: 'IP sürümü', portOrRange: 'Port / aralık', batchLimit: 'Genişletme sonrasında bir defada en fazla {0} kural oluşturabilir veya içe aktarabilirsiniz.', + importDuplicatesRemoved: + 'Dosya içindeki tekrarlar ve zaten mevcut kurallar dahil {0} yinelenen kural kaldırıldı.', importLimit: 'En fazla {0} kural (genişletme sonrası sayıya göre) içe aktarılabilir; dosya boyutu {1} KB değerini aşmamalıdır.', ruleSyncTitle: 'Kuralları eşitle', @@ -4409,19 +4408,9 @@ const message = { ruleSyncBlocked: 'Kullanılamaz', ruleSyncReason: 'Kontrol sonucu', ruleSyncReasonDetail: { - matchesDatabasePolicy: 'Kural zaten veritabanı ilkesiyle eşleşiyor.', - 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.', missingFromTarget: 'Kural hedef güvenlik duvarında bulunmuyor.', - targetDiffers: 'Hedef kural veritabanı ilkesinden farklı.', alreadyExistsInTarget: 'Kural hedef güvenlik duvarında zaten mevcut.', onlyInTarget: 'Kural yalnızca hedef güvenlik duvarında mevcut.', - stale: 'Güvenlik duvarı kuralının durumu güncel değil. Yenileyip tekrar deneyin.', - protectedRule: 'Bu korumalı güvenlik duvarı kuralı değiştirilemez.', - dockerAcceptReadOnly: - 'Bu ACCEPT kuralı salt okunurdur ve diğer kurallar eşitlenirken korunur. Kaldırmak için ana makinede manuel olarak silin.', - cannotReconcile: 'Hedef kural eşitlenemiyor: {0}', }, ruleSyncPartial: 'Eşitleme tamamlandı: {0} başarılı, {1} zaten mevcut, {2} başarısız.', ruleSyncSuccess: 'Eşitleme tamamlandı: {0} başarılı, {1} zaten mevcut, {2} silindi.', @@ -4431,17 +4420,20 @@ const message = { remove: 'Silinecek', blocked: 'Kullanılamaz', }, + initializeFromFile: 'Dosyadan başlat', + downloadRuleBackup: 'Kural yedeğini indir', + importFileHelper: + '64 MB boyutuna kadar JSON dosyalarını dosya sırasıyla toplu olarak içe aktarın. Dönüştürülemeyen kurallar hata olarak bildirilir.', resetDirectRulesHelper: - '{0} içindeki 1Panel sistem güvenlik duvarı zincirlerini, çalışma zamanı kurallarını ve kalıcı dosyaları silin; veritabanı ilkelerini koruyun', + '{0} içindeki tüm 1Panel ana makine güvenlik duvarı zincirlerini, etkin kuralları ve kalıcı dosyaları silin.', resetWhitelistRulesHelper: - '{0} içindeki etkin özel yapılandırmayı sıfırlayın, kurulum varsayılanlarını geri yükleyin ve {0} öğesini devre dışı bırakın; veritabanı ilkeleri korunur ve yeniden eşitlenebilir.', + '{0} özel yapılandırmasını sıfırlayın, varsayılanları geri yükleyin ve devre dışı bırakın.', cleanupForwardingBackendHelper: - '{0} içindeki 1Panel bağlantı noktası yönlendirme çalışma zamanı kurallarını sıfırlayın: ilgili tüm kuralları ve zincirleri silip veritabanı verilerini koruyun', + '{0} üzerindeki tüm 1Panel yönlendirme kurallarını, zincirlerini ve otomatik kurtarma dosyalarını silin. Mevcut JSON yedekleri korunur.', cleanupDockerBackendHelper: - '{0} içindeki 1Panel Docker bağlantı noktası koruma çalışma zamanı kurallarını sıfırlayın: ilgili tüm kuralları ve zincirleri silip veritabanı verilerini koruyun', + '{0} üzerindeki tüm 1Panel Docker koruma kurallarını, zincirlerini ve otomatik kurtarma dosyalarını silin. Mevcut JSON yedekleri korunur.', cleanupBeforeBackendSwitch: - 'Mevcut {0} arka ucu hâlâ 1Panel çalışma zamanı kuralları içeriyor. {1} arka ucuna geçmeden önce sıfırlayın. Sıfırlama yalnızca çalışma zamanı kurallarını temizler; veritabanı ilkeleri korunur ve geçişten sonra yeniden başlatılabilir veya eşitlenebilir.', - cleanupAction: 'Sıfırla', + '{1} sistemine geçmeden önce mevcut {0} sistemini sıfırlayın ve kuralların nasıl geri yükleneceğini doğrulayın.', backendSwitchNotice: 'Yalnızca bir güvenlik duvarı yönetim yönteminin etkinleştirilmesi önerilir. Birden fazla güvenlik duvarının aynı anda çalışması kural çakışmalarına, tutarsız durumlara veya konteyner bağlantı noktası erişim sorunlarına yol açabilir.', switchBackendHelper: '{0} arka ucuna geçilsin mi?', @@ -4490,8 +4482,7 @@ const message = { portDetails: 'Port ayrıntıları', orphanEndpoints: 'İlişkilendirilmemiş uç noktalar', orphanPolicies: 'İlişkilendirilmemiş kurallar', - orphanPoliciesHelper: - 'Aşağıdaki {0} kural bir Docker portuna bağlı değil. Eşleşen bir port algılandığında otomatik olarak bağlanır.', + orphanPoliciesHelper: 'Bu {0} sistem kuralıyla eşleşen yayımlanmış bir Docker bağlantı noktası yok.', composeOrApp: 'Compose / Uygulama', sources: 'Kaynaklar', deniedSources: 'Reddedilen kaynaklar', @@ -4500,6 +4491,8 @@ const message = { protectionMode: 'Koruma modu', denySources: 'Belirtilen kaynakları reddet', allowSources: 'Yalnızca belirtilen kaynaklara izin ver', + acceptSources: 'Belirtilen kaynakları doğrudan kabul et', + acceptAll: 'Tüm trafiği doğrudan kabul et', denyAll: 'Tüm erişimi reddet', dockerGuardMixedFamilyHelper: 'IPv4 ve IPv6 birlikte seçildi. Kaynak tabanlı ilkeleri ayrı ayrı yapılandırın; tüm erişimi reddet doğrudan uygulanabilir.', @@ -4542,7 +4535,7 @@ const message = { 'Bağlantıyı Kaldır - Bağlantı kaldırıldığında, eklenen tüm güvenlik duvarı kuralları geçersiz olacaktır. Dikkatli ilerleyin. Onaylıyor musunuz?', portWhiteList: 'Port beyaz listesi', whitelistConfigHelper: - 'İzin listesi yalnızca yapılandırmayı kaydeder ve eşleşen izin kurallarını korur. Eksik kurallar başlatma, yeniden başlatma, ilk yapılandırma veya eşitleme sırasında eklenir. İzin listesi girdilerini düzenlemek veya silmek mevcut kuralları kaldırmaz.', + 'İzin listesini kaydetmek eksik izin kurallarını ekler ve eşleşen kuralları korur. Liste girdilerini düzenlemek veya silmek mevcut kuralları kaldırmaz.', whitelistDeleteConfirm: 'Yalnızca bu beyaz liste kaydı silinecek. Mevcut güvenlik duvarı kuralları korunacak. Devam edilsin mi?', portWhiteListHelper: @@ -4560,7 +4553,7 @@ const message = { whitelistServicePortsHelper: 'Tek bir port girin (1-65535). Bu işlem hizmetin dinleme portunu değiştirmez. Hizmet ayarlarında port değiştirildiğinde bu beyaz liste kaydı da güncellenir.', whitelistSourcesHelper: - 'IP adreslerini veya CIDR aralıklarını virgül ya da yeni satırla ayırın. Tüm IPv4 ve IPv6 kaynaklarına izin vermek için boş bırakın.', + 'IP adreslerini veya CIDR aralıklarını virgül ya da yeni satırla ayırın. {0} kaynaklarına izin vermek için boş bırakın.', destinationPortPlaceholder: 'örn. 80, 80,443 veya 8080-8089', deleteRuleConfirm: '{0} kural silinecek. Devam etmek istiyor musunuz?', deleteUsedRuleConfirm: diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index b9981f735..427affb2b 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -3967,41 +3967,38 @@ const message = { useEAB: '使用 EAB 認證', }, firewall: { + ipv6Support: 'IPv6 支援', + ipv6SupportHelper: '控制 IPv6 防火牆規則的管理與生效,不影響伺服器的 IPv6 網路功能。', + ipv6DisableContainerHelper: '隱藏容器 IPv6 連接埠資訊。', + ipv6DisableWhitelistHelper: '移除白名單中的 IPv6 規則,其餘既有 IPv6 規則保留。', + ipv6DisableRulesHelper: '停用 1Panel 管理的 iptables、nftables IPv6 規則。', + ipv6DisableNetworkHelper: '不會關閉伺服器的 IPv6 網路功能。', + ipv6DisableEffectHelper: '停用後,相關 IPv6 存取限制和連接埠轉送將不再生效。', + ipv6Disabled: 'IPv6 支援已關閉,請先在防火牆設定中開啟。', + familyRepair: '修復', + familyIncomplete: '{0} 鏈不完整', + resetWithBackup: '備份規則後重置', + resetOnly: '僅重置', + resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化時選擇備份還原。', + resetOnlyHelper: '不建立新備份,直接清理目前規則;保留既有歷史備份。', + backupDirectoryHelper: '自動讀取 {0} 下的有效規則備份,預設選擇最新檔案。', + restoreAllRules: '初始化並還原規則', + importRuleBackup: '從備份匯入', + initializeOnly: '僅初始化', + backupRuleCount: '規則數量', + noRuleBackup: '未找到可還原的規則備份', + ruleBackupLoadFailed: '備份清單載入失敗,請重試。您仍可選擇僅初始化。', create: '建立規則', edit: '編輯規則', quickJump: '快速跳轉', used: '已使用', unUsed: '未使用', - managed: '面板建立', - managedHelper: '由 1Panel 建立並管理。', - adopted: '外部納管', - adoptedHelper: '系統原有規則,現由 1Panel 管理。', - external: '外部規則', - externalHelper: '尚未納入 1Panel 管理。', - protected: '系統保護', - protectedHelper: '系統內建或與白名單完全相符的規則,不可編輯或刪除。', - stateShort: { - managed: '面板建立', - adopted: '外部納管', - external: '外部規則', - protected: '系統保護', - drifted: '異常', - }, + whitelist: '白名單', + builtinRuleProtected: '系統內建保護規則,無法編輯或刪除。', + whitelistRuleProtected: '白名單規則僅支援調整順序和描述,其他修改請前往「設定 → 埠白名單」。', ruleTargetRequired: '請至少填寫一個 IP 位址或連接埠', - resolution_adopt: '納管', - plan_duplicate_rules: '存在比對條件和動作相同的重複規則,無法納管。請手動刪除重複規則後重試。', - adoptRuleConfirm: '納入管理後,1Panel 將負責維護及刪除這條現有規則,是否繼續?', - plan_exact_rule_conflict: '已存在比對條件相同、但允許與拒絕動作相反的規則。', - allRulesAlreadyExist: '本次檢查的 {0} 條規則均已存在,沒有需要建立的新規則。', - plan_managed_rule_drifted: '1Panel 管理記錄與系統實際規則不一致,請檢查系統規則。', - plan_opaque_rule_in_target_scope: '目標範圍內有無法安全解析的規則,本次操作已停止。', - plan_runtime_permanent_mismatch: '防火牆執行設定與永久設定不一致,請先完成同步。', - plan_protected_rule: '此規則受到保護,不能認領、修改或刪除。', - plan_blocked: '目前規則無法安全套用,請重新整理規則後再試。', largeRuleSet: '防火牆規則過多可能導致操作卡頓,建議使用 iptables 或 nftables。', - scopeDefaultMismatch: '系統預設 zone 為 {0},目前頁面僅管理 public zone。', scopeMissing: '託管範圍 {0} 尚未建立,套用第一條規則時將以安全方式建立。', - scopeUnmanagedActive: '偵測到其他使用中範圍:{0},1Panel 不會修改其中的規則。', dockerRestart: '防火牆操作需要重新啟動 Docker 服務', firewallHelper: '{0}系統防火牆', firewallNotStart: '尚未啟用系統防火牆,請先啟用', @@ -4023,8 +4020,7 @@ const message = { dockerGuard: '容器連接埠防護', systemFirewall: '主機防火牆', systemFirewallHelper: '用於管理主機連接埠存取和入站規則。', - forwardPolicyDropWarning: - '{0} 的 FORWARD 預設策略為 DROP,未被 iptables/ip6tables 規則明確允許的跨主機轉送流量可能遭到阻擋,請檢查對應位址族的允許規則。', + forwardPolicyDropWarning: '目前伺服器限制了網路轉送,轉送規則可能無法生效。', forwardingHelper: '用於管理連接埠轉發規則。', dockerFirewallHelper: '用於選擇 1Panel 容器連接埠防護的管理方式。', dockerNftablesRequirement: 'Docker ≥ 29.0.0,實驗性', @@ -4033,6 +4029,7 @@ const message = { addressFamily: 'IP 版本', portOrRange: '連接埠 / 範圍', batchLimit: '每次最多建立或匯入 {0} 條規則(依展開後的數量計算)。', + importDuplicatesRemoved: '已精簡 {0} 條重複規則(含檔案內重複和已存在的規則)。', importLimit: '最多匯入 {0} 條規則(依展開後的數量計算),檔案大小不得超過 {1} KB。', ruleSyncTitle: '同步規則', ruleSyncAction: '同步規則', @@ -4050,19 +4047,9 @@ const message = { ruleSyncBlocked: '不可同步', ruleSyncReason: '檢查結果', ruleSyncReasonDetail: { - matchesDatabasePolicy: '此規則已與資料庫策略一致。', - managedOrderDiffers: '受管規則順序與資料庫順序不一致。', - managedOnlyInTarget: '此受管規則僅存在於目標防火牆中。', - managedRuntimeCannotRemove: '無法安全刪除此受管執行中規則。', missingFromTarget: '目標防火牆中缺少此規則。', - targetDiffers: '目標規則與資料庫策略不一致。', alreadyExistsInTarget: '目標防火牆中已存在此規則。', onlyInTarget: '此規則僅存在於目標防火牆中。', - stale: '防火牆規則狀態已過期,請重新整理後再試。', - protectedRule: '此防火牆規則受保護,無法修改。', - dockerAcceptReadOnly: - '此 ACCEPT 規則為唯讀、不可同步;同步其他規則時將保留此規則。如需移除,請在主機上手動刪除。', - cannotReconcile: '無法同步目標規則:{0}', }, ruleSyncPartial: '同步完成:成功 {0} 條,已存在 {1} 條,失敗 {2} 條。', ruleSyncSuccess: '同步完成:成功 {0} 條,已存在 {1} 條,刪除 {2} 條。', @@ -4072,16 +4059,15 @@ const message = { remove: '待刪除', blocked: '不可同步', }, - resetDirectRulesHelper: '刪除 {0} 中全部 1Panel 系統防火牆鏈、執行規則及持久化檔案,保留資料庫策略', - resetWhitelistRulesHelper: - '重設 {0} 中目前生效的自訂防火牆設定,恢復安裝預設狀態並停用 {0};資料庫策略會保留,之後可重新同步。', - cleanupForwardingBackendHelper: - '重設 {0} 中的 1Panel 連接埠轉送執行時規則:刪除全部相關規則及規則鏈,僅保留資料庫資料', + initializeFromFile: '從檔案初始化', + downloadRuleBackup: '下載規則備份', + importFileHelper: '支援 JSON 檔案(最大 64 MB),依檔案順序分批匯入。無法轉換的規則會回報錯誤。', + resetDirectRulesHelper: '刪除 {0} 的全部 1Panel 主機防火牆鏈、執行規則及持久化檔案。', + resetWhitelistRulesHelper: '重置 {0} 的自訂設定,還原預設狀態並停用。', + cleanupForwardingBackendHelper: '刪除 {0} 的全部 1Panel 轉送規則、規則鏈及自動還原檔案;保留歷史 JSON 備份。', cleanupDockerBackendHelper: - '重設 {0} 中的 1Panel Docker 連接埠防護執行時規則:刪除全部相關規則及規則鏈,僅保留資料庫資料', - cleanupBeforeBackendSwitch: - '目前後端 {0} 仍存在 1Panel 執行時規則,請先重設該後端,再切換到 {1}。重設僅清理執行時規則,資料庫策略會保留,切換後可重新初始化或同步。', - cleanupAction: '重設', + '刪除 {0} 的全部 1Panel Docker 防護規則、規則鏈及自動還原檔案;保留歷史 JSON 備份。', + cleanupBeforeBackendSwitch: '切換至 {1} 前,請先重設目前的 {0},並確認規則的還原方式。', backendSwitchNotice: '建議僅啟用一種防火牆管理方式。同時執行多個防火牆可能導致規則衝突、狀態不一致或容器連接埠存取異常。', switchBackendHelper: '確認切換為 {0}?', @@ -4120,7 +4106,7 @@ const message = { portDetails: '連接埠詳情', orphanEndpoints: '未關聯端點', orphanPolicies: '未關聯規則', - orphanPoliciesHelper: '以下 {0} 條規則尚未綁定 Docker 連接埠,偵測到匹配連接埠後將自動綁定。', + orphanPoliciesHelper: '以下 {0} 條系統規則沒有相符的 Docker 發布連接埠。', composeOrApp: 'Compose / 應用程式', sources: '來源', deniedSources: '禁止來源', @@ -4129,6 +4115,8 @@ const message = { protectionMode: '防護模式', denySources: '禁止指定來源', allowSources: '僅允許指定來源', + acceptSources: '直接放行指定來源', + acceptAll: '直接放行所有存取', denyAll: '禁止所有存取', dockerGuardMixedFamilyHelper: '已同時選擇 IPv4 和 IPv6。指定來源需分別設定,禁止所有可直接套用。', dockerGuardInconsistentConfigHelper: '所選規則設定不一致,請重新設定;提交後將統一覆寫,空白描述將統一清除。', @@ -4164,8 +4152,7 @@ const message = { bindHelper: '綁定 僅當狀態為綁定時,防火牆規則才能生效,是否確認?', unbindHelper: '解除綁定 解除綁定時,已新增的所有防火牆規則將失效,請謹慎操作,是否確認?', portWhiteList: '埠白名單', - whitelistConfigHelper: - '白名單僅儲存設定並保護相符的放行規則。缺少的規則將於啟動、重新啟動、初始化或同步時補齊;編輯或刪除白名單不會刪除現有規則。', + whitelistConfigHelper: '儲存白名單時補齊缺少的允許規則並保護符合的規則。編輯或刪除白名單設定不會刪除既有規則。', whitelistDeleteConfirm: '僅刪除此白名單設定,現有防火牆規則會保留。是否繼續?', portWhiteListHelper: '支援單一連接埠(如 80)或連接埠範圍(如 8000-8100),連接埠取值為 1-65535。', chain: '鏈', @@ -4180,7 +4167,7 @@ const message = { sourceAddressPlaceholder: '例如:172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64', whitelistServicePortsHelper: '手動填寫一個連接埠(1-65535),不會修改服務監聽連接埠。在服務設定中修改連接埠時,會同步更新此白名單設定。', - whitelistSourcesHelper: '支援 IP 或 CIDR,多個以逗號或換行分隔。留空預設允許所有 IPv4 和 IPv6 來源。', + whitelistSourcesHelper: '支援 IP 或 CIDR,多個以逗號或換行分隔。留空預設允許 {0}。', destinationPortPlaceholder: '例如:80、80,443 或 8080-8089', deleteRuleConfirm: '將刪除 {0} 條規則,是否繼續?', deleteUsedRuleConfirm: '該規則涵蓋由 {0} 提供的監聽服務。刪除後可能導致相關服務無法存取,是否繼續?', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 3c5c34204..ff28bcf0a 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -4012,44 +4012,39 @@ const message = { useEAB: '使用 EAB 认证', }, firewall: { + ipv6Support: 'IPv6 支持', + ipv6SupportHelper: '控制 IPv6 防火墙规则的管理与生效,不影响服务器的 IPv6 网络功能。', + ipv6DisableContainerHelper: '隐藏容器 IPv6 端口信息。', + ipv6DisableWhitelistHelper: '移除白名单中的 IPv6 规则,其余已有 IPv6 规则保留。', + ipv6DisableRulesHelper: '停用 1Panel 管理的 iptables、nftables IPv6 规则。', + ipv6DisableNetworkHelper: '不会关闭服务器的 IPv6 网络功能。', + ipv6DisableEffectHelper: '停用后,相关 IPv6 访问限制和端口转发将不再生效。', + ipv6Disabled: 'IPv6 支持已关闭,请先在防火墙设置中开启。', + familyRepair: '修复', + familyIncomplete: '{0} 链不完整', + resetWithBackup: '备份规则后重置', + resetOnly: '仅重置', + resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化时选择备份恢复。', + resetOnlyHelper: '不生成新备份,直接清理当前规则;已有历史备份保留。', + backupDirectoryHelper: '自动读取 {0} 下的有效规则备份,默认选择最新文件。', + restoreAllRules: '初始化并恢复规则', + importRuleBackup: '从备份导入', + initializeOnly: '仅初始化', + backupRuleCount: '规则数量', + noRuleBackup: '未找到可恢复的规则备份', + ruleBackupLoadFailed: '备份列表加载失败,请重试。您仍可选择仅初始化。', create: '创建规则', edit: '编辑规则', quickJump: '快速跳转', used: '已使用', unUsed: '未使用', - managed: '面板创建', - managedHelper: '由 1Panel 创建并管理。', - adopted: '外部纳管', - adoptedHelper: '系统原有规则,现由 1Panel 管理。', - external: '外部规则', - externalHelper: '尚未纳入 1Panel 管理。', - protected: '系统保护', - protectedHelper: '系统内置或与白名单完全匹配的规则,不可编辑或删除。', - stateShort: { - managed: '面板创建', - adopted: '外部纳管', - external: '外部规则', - protected: '系统保护', - drifted: '异常', - }, + whitelist: '白名单', + builtinRuleProtected: '系统内置保护规则,无法编辑或删除。', + whitelistRuleProtected: '白名单规则仅支持调整顺序和描述,其他修改请前往「设置 → 端口白名单」。', ruleTargetRequired: '请至少填写一个 IP 地址或端口', - resolution_adopt: '纳管', - plan_duplicate_rules: '存在匹配条件和动作相同的重复规则,无法纳管。请手动删除重复规则后重试。', - adoptRuleConfirm: '纳入管理后,1Panel 将负责维护和删除这条现有规则,是否继续?', - plan_exact_rule_conflict: '已存在匹配条件相同、但允许与拒绝动作相反的规则。', - allRulesAlreadyExist: '本次检查的 {0} 条规则均已存在,没有需要创建的新规则。', - plan_managed_rule_drifted: - '这条规则在 1Panel 中的记录与防火墙实际状态不一致,请先刷新规则;如仍异常,请重新同步。', - plan_opaque_rule_in_target_scope: - '当前防火墙中存在 1Panel 暂不支持的规则。为避免影响现有访问,本次未创建。请返回规则列表,查看内容无法完整显示的“外部规则”,并在系统防火墙中处理后重试。', - plan_runtime_permanent_mismatch: '当前生效的防火墙规则与重启后保留的规则不一致,请先同步后重试。', - plan_protected_rule: '该规则受到保护,不能认领、修改或删除。', - plan_blocked: '暂时无法处理这条规则,本次操作未生效。请刷新后重试;如仍失败,请检查规则状态。', largeRuleSet: '防火墙规则过多可能导致操作卡顿,建议使用 iptables 或 nftables。', - scopeDefaultMismatch: '系统默认 zone 为 {0},当前页面仅管理 public zone。', scopeMissing: '托管范围 {0} 尚未创建,应用首条规则时将以安全方式创建。', - scopeUnmanagedActive: '检测到其他活动范围:{0},1Panel 不会修改其中的规则。', dockerRestart: '防火墙操作需要重启 Docker 服务', firewallHelper: '{0}系统防火墙', firewallNotStart: '当前未开启系统防火墙,请先开启!', @@ -4071,8 +4066,7 @@ const message = { dockerGuard: '容器端口防护', systemFirewall: '主机防火墙', systemFirewallHelper: '用于管理主机端口访问和入站规则。', - forwardPolicyDropWarning: - '{0} 的 FORWARD 默认策略为 DROP,未被 iptables/ip6tables 规则明确放行的跨主机转发流量可能被阻断,请检查对应地址族的放行规则。', + forwardPolicyDropWarning: '当前服务器限制了网络转发,转发规则可能无法生效。', forwardingHelper: '用于管理端口转发规则。', dockerFirewallHelper: '用于选择 1Panel 容器端口防护的管理方式。', dockerNftablesRequirement: 'Docker ≥ 29.0.0,实验性', @@ -4081,6 +4075,7 @@ const message = { addressFamily: 'IP 版本', portOrRange: '端口 / 范围', batchLimit: '每次最多创建或导入 {0} 条规则(按展开后的数量计算)。', + importDuplicatesRemoved: '已精简 {0} 条重复规则(含文件内重复和已存在的规则)。', importLimit: '最多导入 {0} 条规则(按展开后的数量计算),文件大小不超过 {1} KB。', ruleSyncTitle: '同步规则', ruleSyncAction: '同步规则', @@ -4098,19 +4093,9 @@ const message = { ruleSyncBlocked: '不可同步', ruleSyncReason: '检查结果', ruleSyncReasonDetail: { - matchesDatabasePolicy: '该规则已与 1Panel 保存的设置一致。', - managedOrderDiffers: '防火墙中的规则顺序与 1Panel 保存的顺序不一致,本次将调整。', - managedOnlyInTarget: '该规则仍存在于防火墙中,但 1Panel 已不再保存,本次将删除。', - managedRuntimeCannotRemove: '该规则仅在当前运行中生效,1Panel 无法确认删除后的影响,因此本次不会删除。', missingFromTarget: '防火墙中缺少该规则,本次将补充。', - targetDiffers: '防火墙中的规则与 1Panel 保存的设置不一致,本次将修复。', alreadyExistsInTarget: '防火墙中已存在相同规则,无需重复添加。', onlyInTarget: '该规则只存在于当前防火墙中。', - stale: '检查后防火墙状态已发生变化,请刷新后重试。', - protectedRule: '该防火墙规则受到保护,不能修改。', - dockerAcceptReadOnly: - '该 ACCEPT 规则为只读、不可同步;同步其他规则时会保留该规则。如需移除,请在主机上手动删除。', - cannotReconcile: '该规则无法自动同步:{0}', }, ruleSyncPartial: '同步完成:成功 {0} 条,已存在 {1} 条,失败 {2} 条。', ruleSyncSuccess: '同步完成:成功 {0} 条,已存在 {1} 条,删除 {2} 条。', @@ -4120,16 +4105,15 @@ const message = { remove: '待删除', blocked: '不可同步', }, - resetDirectRulesHelper: '删除 {0} 中全部 1Panel 系统防火墙链、运行规则及持久化文件,保留数据库策略', - resetWhitelistRulesHelper: - '重置 {0} 中当前生效的自定义防火墙配置,恢复安装默认状态并停用 {0};数据库策略会保留,之后可以重新同步。', - cleanupForwardingBackendHelper: - '重置 {0} 中的 1Panel 端口转发运行时规则:删除全部相关规则及规则链,仅保留数据库数据', + initializeFromFile: '从文件初始化', + downloadRuleBackup: '下载规则备份', + importFileHelper: '支持 JSON 文件(最大 64 MB),按文件顺序分批导入。无法转换的规则会报告错误。', + resetDirectRulesHelper: '删除 {0} 的全部 1Panel 主机防火墙链、运行规则及持久化文件。', + resetWhitelistRulesHelper: '重置 {0} 的自定义配置,恢复默认状态并停用。', + cleanupForwardingBackendHelper: '删除 {0} 的全部 1Panel 转发规则、规则链及自动恢复文件;历史 JSON 备份保留。', cleanupDockerBackendHelper: - '重置 {0} 中的 1Panel Docker 端口防护运行时规则:删除全部相关规则及规则链,仅保留数据库数据', - cleanupBeforeBackendSwitch: - '当前后端 {0} 仍存在 1Panel 运行时规则,请先重置该后端,再切换到 {1}。重置仅清理运行时规则,数据库策略会保留,切换后可以重新初始化或同步。', - cleanupAction: '重置', + '删除 {0} 的全部 1Panel Docker 防护规则、规则链及自动恢复文件;历史 JSON 备份保留。', + cleanupBeforeBackendSwitch: '切换到 {1} 前,请先重置当前 {0},并确认对应的规则恢复方式。', backendSwitchNotice: '建议仅启用一种防火墙管理方式。多个防火墙同时运行可能导致规则冲突、状态不一致或容器端口访问异常。', switchBackendHelper: '确认切换为 {0}?', @@ -4167,7 +4151,7 @@ const message = { portDetails: '端口详情', orphanEndpoints: '未关联端点', orphanPolicies: '未关联规则', - orphanPoliciesHelper: '以下 {0} 条规则尚未绑定 Docker 端口,检测到匹配端口后将自动绑定。', + orphanPoliciesHelper: '以下 {0} 条系统规则没有匹配的 Docker 发布端口。', composeOrApp: 'Compose / 应用', sources: '来源', deniedSources: '禁止来源', @@ -4176,6 +4160,8 @@ const message = { protectionMode: '防护模式', denySources: '禁止指定来源', allowSources: '仅允许指定来源', + acceptSources: '直接放行指定来源', + acceptAll: '直接放行所有访问', denyAll: '禁止所有访问', dockerGuardMixedFamilyHelper: '已同时选择 IPv4 和 IPv6。指定来源需分别设置,禁止所有可直接应用。', dockerGuardInconsistentConfigHelper: '所选规则配置不一致,请重新设置;提交后将统一覆盖,空描述将统一清空。', @@ -4212,8 +4198,7 @@ const message = { bindHelper: '绑定 仅当状态为绑定时,防火墙规则才能生效,是否确认?', unbindHelper: '解绑 解除绑定时,已添加的所有防火墙规则将失效,请谨慎操作,是否确认?', portWhiteList: '端口白名单', - whitelistConfigHelper: - '白名单仅保存配置并保护匹配的放行规则。缺失规则将在启动、重启、初始化或同步时补齐;编辑或删除白名单不会删除已有规则。', + whitelistConfigHelper: '保存白名单时补齐缺失的放行规则并保护匹配规则。编辑或删除白名单配置不会删除已有规则。', whitelistDeleteConfirm: '仅删除这条白名单配置,已有防火墙规则会保留。是否继续?', portWhiteListHelper: '支持单个端口(如 80)或端口范围(如 8000-8100),端口取值为 1-65535。', chain: '链', @@ -4228,7 +4213,7 @@ const message = { sourceAddressPlaceholder: '例如:172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64', whitelistServicePortsHelper: '手动填写一个端口(1-65535),不会修改服务监听端口。在服务设置中修改端口时,会同步更新此白名单配置。', - whitelistSourcesHelper: '支持 IP 或 CIDR,多个以逗号或换行分隔。留空默认允许所有 IPv4 和 IPv6 来源。', + whitelistSourcesHelper: '支持 IP 或 CIDR,多个以逗号或换行分隔。留空默认允许 {0}。', destinationPortPlaceholder: '例如:80、80,443 或 8080-8089', deleteRuleConfirm: '将删除 {0} 条规则,是否继续?', deleteUsedRuleConfirm: '该规则覆盖了由 {0} 提供的监听服务。删除后可能导致相关服务无法访问,是否继续?', diff --git a/frontend/src/views/host/firewall/components/family-issues.vue b/frontend/src/views/host/firewall/components/family-issues.vue new file mode 100644 index 000000000..aecc72fde --- /dev/null +++ b/frontend/src/views/host/firewall/components/family-issues.vue @@ -0,0 +1,141 @@ + + + + + + diff --git a/frontend/src/views/host/firewall/components/rule-dialog.scss b/frontend/src/views/host/firewall/components/rule-dialog.scss new file mode 100644 index 000000000..f05f8d227 --- /dev/null +++ b/frontend/src/views/host/firewall/components/rule-dialog.scss @@ -0,0 +1,151 @@ +.firewall-rule-dialog.el-dialog { + width: min(720px, calc(100vw - 32px)); + + .el-dialog__body { + padding-top: 8px; + } + + .el-dialog__footer { + padding-top: 20px; + } + + .el-alert__title { + line-height: 1.6; + overflow-wrap: anywhere; + } + + .firewall-rule-dialog__content { + display: flex; + flex-direction: column; + gap: 20px; + min-height: 100px; + } + + .firewall-rule-dialog__modes { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + } + + .firewall-rule-dialog__choices, + .firewall-rule-dialog__files { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .el-radio.is-bordered { + height: auto; + min-height: 48px; + margin: 0; + padding: 16px; + border-radius: 8px; + white-space: normal; + + &.is-checked { + background: var(--el-color-primary-light-9); + } + + &:not(.is-disabled):hover { + border-color: var(--el-color-primary); + } + + .el-radio__label { + min-width: 0; + width: 100%; + padding-left: 12px; + line-height: 1.5; + } + } + + .firewall-rule-dialog__choice-title { + display: block; + font-weight: 500; + } + + .firewall-rule-dialog__choice-description { + display: block; + margin-top: 6px; + color: var(--el-text-color-secondary); + font-size: 12px; + line-height: 1.7; + } + + .firewall-rule-dialog__confirmation { + display: flex; + flex-direction: column; + gap: 10px; + border-top: 1px solid var(--el-border-color-lighter); + padding-top: 20px; + + code { + display: inline-block; + padding: 2px 6px; + border-radius: 4px; + background: var(--el-fill-color); + color: var(--el-color-danger); + } + } + + .firewall-rule-dialog__directory { + display: flex; + align-items: flex-start; + gap: 8px; + color: var(--el-text-color-secondary); + font-size: 12px; + line-height: 1.7; + overflow-wrap: anywhere; + + .el-icon { + flex-shrink: 0; + margin-top: 3px; + font-size: 16px; + } + } + + .firewall-rule-dialog__files { + flex-wrap: nowrap; + max-height: 320px; + overflow-y: auto; + padding: 2px; + + > .el-radio { + flex-shrink: 0; + width: 100%; + } + } + + .firewall-rule-dialog__file-name { + display: flex; + align-items: flex-start; + gap: 8px; + overflow-wrap: anywhere; + font-weight: 500; + + .el-icon { + flex-shrink: 0; + margin-top: 3px; + } + } + + .firewall-rule-dialog__file-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 16px; + margin-top: 8px; + color: var(--el-text-color-secondary); + font-size: 12px; + } + + .el-empty { + padding: 12px 0; + } + + @media (max-width: 600px) { + .firewall-rule-dialog__modes { + grid-template-columns: 1fr; + } + } +} diff --git a/frontend/src/views/host/firewall/components/rule-initialize.vue b/frontend/src/views/host/firewall/components/rule-initialize.vue new file mode 100644 index 000000000..fd73ce445 --- /dev/null +++ b/frontend/src/views/host/firewall/components/rule-initialize.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/frontend/src/views/host/firewall/components/rule-reset.vue b/frontend/src/views/host/firewall/components/rule-reset.vue new file mode 100644 index 000000000..1a908c9d3 --- /dev/null +++ b/frontend/src/views/host/firewall/components/rule-reset.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/src/views/host/firewall/docker/detail/index.vue b/frontend/src/views/host/firewall/docker/detail/index.vue index df82f39aa..937e13d2e 100644 --- a/frontend/src/views/host/firewall/docker/detail/index.vue +++ b/frontend/src/views/host/firewall/docker/detail/index.vue @@ -95,12 +95,6 @@
-
- {{ $t('commons.table.description') }} - - {{ group.endpoint.description }} - -
{{ $t('commons.table.message') }} @@ -123,7 +117,7 @@ :title="$t('firewall.dockerGuardMixedFamilyHelper')" /> - - -