mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 08:00:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88a78f7b26 | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 | ||
|
|
2701af9054 | ||
|
|
4954067736 | ||
|
|
b962144e7d | ||
|
|
8f1be42b6b | ||
|
|
c6969f231b | ||
|
|
3571fb8d86 |
+79
-138
@@ -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)
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
||||
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||
var req request.RuntimeOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.EditRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
+86
-121
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
+4
-117
@@ -4,11 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -20,12 +17,10 @@ import (
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
type AlertService struct{}
|
||||
@@ -358,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
}
|
||||
|
||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||
var disks []dto.DiskDTO
|
||||
excludes := map[string]struct{}{
|
||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
||||
infos := loadDiskInfo(true)
|
||||
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||
for _, item := range infos {
|
||||
disks = append(disks, dto.DiskDTO(item))
|
||||
}
|
||||
stdout, err := executeDiskCommand()
|
||||
if err != nil {
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
lines := strings.Split(stdout, "\n")
|
||||
var mounts []dto.AlertDiskInfo
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
mountPoint := strings.Join(fields[6:], " ")
|
||||
if shouldExclude(fields, mountPoint, excludes) {
|
||||
continue
|
||||
}
|
||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
)
|
||||
wg.Add(len(mounts))
|
||||
for i := 0; i < len(mounts); i++ {
|
||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
var itemData dto.DiskDTO
|
||||
itemData.Path = mount.Mount
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
default:
|
||||
state, err := disk.Usage(mount.Mount)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
return
|
||||
}
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(time.After(5*time.Second), mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
sort.Slice(disks, func(i, j int) bool {
|
||||
return disks[i].Path < disks[j].Path
|
||||
})
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func executeDiskCommand() (string, error) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
||||
}
|
||||
if err != nil {
|
||||
return stdout, err
|
||||
}
|
||||
var lines []string
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(fields[2], "K") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(mountPoint, "docker") {
|
||||
return true
|
||||
}
|
||||
_, excluded := excludes[mountPoint]
|
||||
return excluded
|
||||
}
|
||||
|
||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||
var (
|
||||
opts []repo.DBOption
|
||||
|
||||
@@ -1066,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||
return err
|
||||
}
|
||||
var errMsgs []string
|
||||
for _, item := range diskList {
|
||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
||||
errMsgs = append(errMsgs, errMsg)
|
||||
global.LOG.Errorf("%s", errMsg)
|
||||
if item.Total == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(errMsgs) > 0 {
|
||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
||||
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processSingleDisk(alert dto.AlertDTO) error {
|
||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
||||
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s", err.Error())
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||
return err
|
||||
}
|
||||
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||
commonTotal := float64(alert.Count)
|
||||
if alert.Cycle == 1 {
|
||||
commonTotal *= 1024 * 1024 * 1024
|
||||
}
|
||||
if usedTotal < commonTotal {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
params := createAlertDiskParams(path, usedStr)
|
||||
sender := NewAlertSender(alert, alert.Project)
|
||||
sender.ResourceSend(path, params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||
|
||||
@@ -242,7 +242,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
||||
currentInfo.SwapMemoryUsed = swapInfo.Used
|
||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||
|
||||
currentInfo.DiskData = loadDiskInfo()
|
||||
currentInfo.DiskData = loadDiskInfo(false)
|
||||
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||
|
||||
if ioOption == "all" {
|
||||
@@ -455,26 +455,9 @@ type diskInfo struct {
|
||||
Device string
|
||||
}
|
||||
|
||||
func loadDiskInfo() []dto.DiskInfo {
|
||||
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
|
||||
var datas []dto.DiskInfo
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
stdout, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return datas
|
||||
}
|
||||
}
|
||||
stdout := loadDiskMounts()
|
||||
lines := strings.Split(stdout, "\n")
|
||||
|
||||
var mounts []diskInfo
|
||||
@@ -522,43 +505,22 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(mount.Mount, false)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
case result := <-resultCh:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
|
||||
return
|
||||
}
|
||||
itemData.Total = result.state.Total
|
||||
itemData.Free = result.state.Free
|
||||
itemData.Used = result.state.Used
|
||||
itemData.UsedPercent = result.state.UsedPercent
|
||||
itemData.InodesTotal = result.state.InodesTotal
|
||||
itemData.InodesUsed = result.state.InodesUsed
|
||||
itemData.InodesFree = result.state.InodesFree
|
||||
itemData.InodesUsedPercent = result.state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
} else {
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
}
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
}(mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -569,6 +531,69 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
return datas
|
||||
}
|
||||
|
||||
var diskMountsMu sync.Mutex
|
||||
|
||||
func loadDiskMounts() string {
|
||||
if !diskMountsMu.TryLock() {
|
||||
return ""
|
||||
}
|
||||
resultCh := make(chan string, 1)
|
||||
go func() {
|
||||
var stdout string
|
||||
defer func() {
|
||||
diskMountsMu.Unlock()
|
||||
resultCh <- stdout
|
||||
}()
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
output, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
output, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stdout = output
|
||||
}()
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case stdout := <-resultCh:
|
||||
return stdout
|
||||
case <-timer.C:
|
||||
global.LOG.Error("load disk mounts timed out; df collection is still running")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(path, forceRefresh)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
|
||||
case result := <-resultCh:
|
||||
return result.state, result.err
|
||||
}
|
||||
}
|
||||
|
||||
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
ok, client := accelerator.New()
|
||||
if !ok {
|
||||
|
||||
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
||||
if baseInfo.SwapMemoryTotal != 0 {
|
||||
baseInfo.SwapDetails = loadSwap()
|
||||
}
|
||||
disks := loadDiskInfo()
|
||||
disks := loadDiskInfo(false)
|
||||
for _, item := range disks {
|
||||
baseInfo.MaxSize += item.Free
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1310,7 +1310,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
||||
}
|
||||
|
||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||
return loadDiskInfo()
|
||||
return loadDiskInfo(false)
|
||||
}
|
||||
|
||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||
|
||||
+482
-878
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+1537
-2859
File diff suppressed because it is too large
Load Diff
+155
-388
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+72
-11
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const sshPath = "/etc/ssh/sshd_config"
|
||||
@@ -438,15 +440,33 @@ func (u *SSHService) SyncRootCert() error {
|
||||
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
|
||||
continue
|
||||
}
|
||||
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
|
||||
publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubItem)
|
||||
if err != nil || len(bytes.TrimSpace(rest)) != 0 {
|
||||
continue
|
||||
}
|
||||
cert.EncryptionMode = loadSSHKeyType(publicKey)
|
||||
if cert.EncryptionMode == "" {
|
||||
continue
|
||||
}
|
||||
rootCerts = append(rootCerts, cert)
|
||||
}
|
||||
return hostRepo.SyncCert(rootCerts)
|
||||
}
|
||||
|
||||
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
||||
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
switch req.Mode {
|
||||
case "generate":
|
||||
switch req.EncryptionMode {
|
||||
case "rsa", "ed25519", "ecdsa", "dsa":
|
||||
default:
|
||||
return buserr.WithName("ErrNotSupportType", req.EncryptionMode)
|
||||
}
|
||||
case "input", "import":
|
||||
if err := validateSSHKeyPair(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return buserr.WithName("ErrNotSupportType", req.Mode)
|
||||
}
|
||||
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
|
||||
if certItem.ID != 0 {
|
||||
@@ -543,6 +563,9 @@ func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
||||
}
|
||||
|
||||
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
|
||||
if err := validateSSHKeyPair(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current user failed, err: %v", err)
|
||||
@@ -1681,22 +1704,60 @@ func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
|
||||
return itemDate
|
||||
}
|
||||
|
||||
func loadEncryptioMode(content string) string {
|
||||
if strings.HasPrefix(content, "ssh-rsa") {
|
||||
func loadSSHKeyType(publicKey ssh.PublicKey) string {
|
||||
switch publicKey.Type() {
|
||||
case ssh.KeyAlgoRSA:
|
||||
return "rsa"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ed25519") {
|
||||
case ssh.KeyAlgoED25519:
|
||||
return "ed25519"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ecdsa") {
|
||||
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
|
||||
return "ecdsa"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-dsa") {
|
||||
case ssh.KeyAlgoDSA:
|
||||
return "dsa"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateSSHKeyPair(req *dto.RootCertOperate) error {
|
||||
req.PublicKey = strings.TrimSpace(req.PublicKey)
|
||||
req.PrivateKey = strings.TrimSpace(req.PrivateKey)
|
||||
if req.PublicKey == "" || strings.ContainsAny(req.PublicKey, "\r\n") {
|
||||
return buserr.New("ErrSSHPublicKey")
|
||||
}
|
||||
publicKey, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(req.PublicKey))
|
||||
if err != nil || len(options) != 0 || len(bytes.TrimSpace(rest)) != 0 {
|
||||
return buserr.New("ErrSSHPublicKey")
|
||||
}
|
||||
req.EncryptionMode = loadSSHKeyType(publicKey)
|
||||
if req.EncryptionMode == "" {
|
||||
return buserr.WithName("ErrNotSupportType", publicKey.Type())
|
||||
}
|
||||
block, rest := pem.Decode([]byte(req.PrivateKey))
|
||||
if !strings.HasPrefix(req.PrivateKey, "-----BEGIN ") || block == nil || len(bytes.TrimSpace(rest)) != 0 {
|
||||
return buserr.New("ErrSSHPrivateKey")
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey([]byte(req.PrivateKey))
|
||||
var missingPassphrase *ssh.PassphraseMissingError
|
||||
if errors.As(err, &missingPassphrase) {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(req.PrivateKey), []byte(req.PassPhrase))
|
||||
} else if err == nil {
|
||||
req.PassPhrase = ""
|
||||
}
|
||||
if err != nil {
|
||||
return buserr.New("ErrSSHPrivateKey")
|
||||
}
|
||||
if !bytes.Equal(signer.PublicKey().Marshal(), publicKey.Marshal()) {
|
||||
return buserr.New("ErrSSHKeyMismatch")
|
||||
}
|
||||
req.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey)))
|
||||
if comment != "" {
|
||||
req.PublicKey += " " + comment
|
||||
}
|
||||
req.PublicKey += "\n"
|
||||
req.PrivateKey += "\n"
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLocalConn(newPort uint) error {
|
||||
conn, _ := settingRepo.GetValueByKey("LocalSSHConn")
|
||||
if len(conn) == 0 {
|
||||
|
||||
+10
-1
@@ -87,6 +87,7 @@ const (
|
||||
TaskScopeCronjob = "Cronjob"
|
||||
TaskScopeClam = "Clam"
|
||||
TaskScopeSystem = "System"
|
||||
TaskScopeVm = "VirtualMachine"
|
||||
TaskScopeFirewall = "Firewall"
|
||||
TaskScopeAppStore = "AppStore"
|
||||
TaskScopeSnapshot = "Snapshot"
|
||||
@@ -154,7 +155,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
|
||||
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(&SimpleFormatter{})
|
||||
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm)
|
||||
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
@@ -257,7 +258,15 @@ func (s *SubTask) Execute() error {
|
||||
case <-ctx.Done():
|
||||
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
|
||||
err = errors.New("timeout!")
|
||||
if s.RootTask.Task.Type == TaskScopeVm {
|
||||
<-done
|
||||
}
|
||||
case err = <-done:
|
||||
if s.RootTask.Task.Type == TaskScopeVm && ctx.Err() != nil {
|
||||
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
|
||||
err = errors.New("timeout!")
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
|
||||
if err.Error() == i18n.GetMsgByKey("ErrShutDown") {
|
||||
|
||||
@@ -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": [
|
||||
@@ -4039,12 +4034,21 @@
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
"ID"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "操作运行环境 [id]",
|
||||
"formatEN": "Operate runtime [id]"
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "ID",
|
||||
"isList": false,
|
||||
"db": "runtimes",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "操作运行环境 [name]",
|
||||
"formatEN": "Operate runtime [name]"
|
||||
},
|
||||
"/runtimes/php/config": {
|
||||
"bodyKeys": [
|
||||
@@ -4146,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": [
|
||||
@@ -4238,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": [
|
||||
@@ -4357,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": [
|
||||
@@ -4367,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": [
|
||||
@@ -4396,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": [
|
||||
@@ -4656,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": [
|
||||
@@ -5420,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": [
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+9
-17
@@ -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,7 +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"
|
||||
|
||||
|
||||
@@ -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,7 +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"
|
||||
|
||||
|
||||
+9
-17
@@ -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,7 +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 مطابقت ندارند"
|
||||
|
||||
|
||||
+9
-17
@@ -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,7 +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 公開鍵と秘密鍵が一致しません"
|
||||
|
||||
|
||||
+9
-17
@@ -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,7 +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 공개 키와 개인 키가 일치하지 않습니다"
|
||||
|
||||
|
||||
+9
-17
@@ -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,7 +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 ບໍ່ກົງກັນ"
|
||||
|
||||
|
||||
+9
-17
@@ -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,7 +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"
|
||||
|
||||
|
||||
@@ -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,7 +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"
|
||||
|
||||
|
||||
+9
-17
@@ -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,7 +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 не соответствуют друг другу"
|
||||
|
||||
|
||||
+9
-17
@@ -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,7 +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"
|
||||
|
||||
|
||||
@@ -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,7 +713,12 @@ 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 公鑰與私鑰不相符"
|
||||
|
||||
+9
-17
@@ -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,7 +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 公钥与私钥不匹配"
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -97,7 +97,7 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
|
||||
}()
|
||||
defer close(done)
|
||||
|
||||
client, err := sftp.NewClient(sshClient)
|
||||
client, err := sftp.NewClient(sshClient, sftp.UseConcurrentWrites(true))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -127,7 +127,15 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
// ReadFrom uses the pipelined write path of pkg/sftp. A plain io.Copy would
|
||||
// pick os.File.WriteTo (the source is an *os.File), which issues one
|
||||
// synchronous SFTP WRITE per packet and waits for the reply every time, so
|
||||
// the throughput is bounded by packetSize/RTT.
|
||||
written, err := dstFile.ReadFrom(srcFile)
|
||||
if err != nil {
|
||||
// A failed concurrent write may leave holes in the remote file, so cut
|
||||
// it back to the number of bytes actually written.
|
||||
_ = dstFile.Truncate(written)
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return false, ctxErr
|
||||
}
|
||||
|
||||
@@ -251,6 +251,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
|
||||
cmdItem.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
cmdItem.Cancel = func() error {
|
||||
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
|
||||
return cmdItem.Process.Kill()
|
||||
}
|
||||
cmds = append(cmds, cmdItem)
|
||||
}
|
||||
return cmds
|
||||
@@ -335,6 +339,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
if newContext != nil {
|
||||
cmd.Cancel = func() error {
|
||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
customWriter := &CustomWriter{taskItem: c.taskItem}
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -398,6 +408,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
if newContext != nil && newContext.Err() != nil {
|
||||
return "", c.pipeResultErr(newContext, err)
|
||||
}
|
||||
if err != nil {
|
||||
return handleErr(&stdout, &stderr, c.IgnoreExist1, err)
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+608
-224
File diff suppressed because it is too large
Load Diff
+608
-224
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||
@@ -4039,12 +4034,21 @@
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
"ID"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "操作运行环境 [id]",
|
||||
"formatEN": "Operate runtime [id]"
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "ID",
|
||||
"isList": false,
|
||||
"db": "runtimes",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "操作运行环境 [name]",
|
||||
"formatEN": "Operate runtime [name]"
|
||||
},
|
||||
"/runtimes/php/config": {
|
||||
"bodyKeys": [
|
||||
@@ -4146,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": [
|
||||
@@ -4238,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": [
|
||||
@@ -4357,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": [
|
||||
@@ -4367,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": [
|
||||
@@ -4396,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": [
|
||||
@@ -4656,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": [
|
||||
@@ -5420,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": [
|
||||
|
||||
@@ -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"}: {},
|
||||
|
||||
+15
-4
@@ -230,6 +230,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
|
||||
cmdItem.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
cmdItem.Cancel = func() error {
|
||||
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
|
||||
return cmdItem.Process.Kill()
|
||||
}
|
||||
cmds = append(cmds, cmdItem)
|
||||
}
|
||||
return cmds
|
||||
@@ -305,6 +309,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
if newContext != nil {
|
||||
cmd.Cancel = func() error {
|
||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
customWriter := &CustomWriter{taskItem: c.taskItem}
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -357,11 +367,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
if c.preserveErrorCause && newContext != nil && newContext.Err() != nil {
|
||||
if cmd.Process != nil && cmd.Process.Pid > 0 {
|
||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
if newContext != nil && newContext.Err() != nil {
|
||||
resultErr := c.pipeResultErr(newContext, err)
|
||||
if c.preserveErrorCause {
|
||||
resultErr = &commandError{message: resultErr.Error(), cause: newContext.Err()}
|
||||
}
|
||||
return "", newContext.Err()
|
||||
return "", resultErr
|
||||
}
|
||||
if err != nil {
|
||||
out, resultErr := handleErr(&stdout, &stderr, c.IgnoreExist1, err)
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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<Firewall.FilterChainOperationResult>('/hosts/firewall/forward/operate', request, {
|
||||
skipErrorMessage: true,
|
||||
});
|
||||
|
||||
export const enableForwarding = (taskID?: string) =>
|
||||
export const enableForwarding = (taskID?: string, backupFile?: string) =>
|
||||
http.post<Firewall.FilterChainOperationResult>(
|
||||
'/hosts/firewall/forward/enable',
|
||||
taskID ? { taskID } : {},
|
||||
{ taskID, backupFile },
|
||||
TimeoutEnum.T_60S,
|
||||
);
|
||||
|
||||
@@ -51,6 +51,9 @@ export const searchFirewallRules = (request: Firewall.InventoryRequest) => {
|
||||
return http.post<Firewall.Inventory>('/hosts/firewall/rules/search', request, TimeoutEnum.T_40S);
|
||||
};
|
||||
|
||||
export const listFirewallRuleBackups = (subsystem: Firewall.BackendSubsystem = 'system') =>
|
||||
http.get<Firewall.RuleBackups>('/hosts/firewall/rules/backups', { subsystem });
|
||||
|
||||
export const resetFirewallRules = (request: Firewall.ResetRequest) => {
|
||||
return http.post<Firewall.ResetResponse>('/hosts/firewall/rules/reset', request, TimeoutEnum.T_10M);
|
||||
};
|
||||
@@ -59,32 +62,16 @@ export const loadFirewallNativeDetail = (request: Firewall.NativeDetailRequest)
|
||||
return http.post<string>('/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<Firewall.CreateResponse>('/hosts/firewall/rules', request, TimeoutEnum.T_10M);
|
||||
};
|
||||
|
||||
export const previewFirewallRuleSync = (request: Firewall.RuleSyncRequest) => {
|
||||
return http.post<Firewall.RuleSyncPreview>('/hosts/firewall/rules/sync/preview', request, TimeoutEnum.T_3M);
|
||||
};
|
||||
|
||||
export const loadFirewallRuleSyncTask = () => {
|
||||
return http.get<Firewall.RuleSyncTask>('/hosts/firewall/rules/sync/task');
|
||||
};
|
||||
|
||||
export const syncFirewallRules = (request: Firewall.RuleSyncRequest) => {
|
||||
return http.post<Firewall.RuleSyncResult>('/hosts/firewall/rules/sync', request, TimeoutEnum.T_10M);
|
||||
};
|
||||
|
||||
export const deleteFirewallRules = (request: Firewall.DeleteRequest) => {
|
||||
return http.post<Firewall.DeleteResponse>('/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<Firewall.DockerGuardContainer[]>('/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<Firewall.FilterChainOperationResult>(
|
||||
'/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<Firewall.FilterChainOperationResult>('/hosts/firewall/family/operate', request, TimeoutEnum.T_60S);
|
||||
|
||||
export const operateFirewallIPv6 = (enabled: boolean) =>
|
||||
http.post<Firewall.FilterChainOperationResult>('/hosts/firewall/settings/ipv6', {
|
||||
status: enabled ? 'Enable' : 'Disable',
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<DialogPro v-model="submitVisible" :title="header" size="mini">
|
||||
<div>
|
||||
<span v-if="operationInfo" class="operation-info">{{ operationInfo }}</span>
|
||||
<slot />
|
||||
<div :style="{ 'margin-top': operationInfo ? '10px' : '0px' }">
|
||||
<span style="font-size: 12px">{{ $t('commons.msg.operateConfirm') }}</span>
|
||||
<span style="font-size: 12px; color: red; font-weight: 500">'{{ submitInputInfo }}'</span>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<fu-table
|
||||
v-if="currentViewMode === 'table'"
|
||||
v-bind="$attrs"
|
||||
:data="props.data"
|
||||
ref="tableRef"
|
||||
@select="handleSelect"
|
||||
@selection-change="handleSelectionChange"
|
||||
@@ -175,7 +176,7 @@ import { useResponsivePagination } from './useResponsivePagination';
|
||||
import { useTableSelection } from './useTableSelection';
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
const { isMobile, openMenuTabs } = useGlobalStore();
|
||||
const { isMobile, openMenuTabs, currentNode } = useGlobalStore();
|
||||
|
||||
defineOptions({ name: 'ComplexTable' });
|
||||
export type DropdownProps = FuTableOperationButton;
|
||||
@@ -184,6 +185,8 @@ type ViewMode = 'table' | 'card';
|
||||
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
data: { type: Array as PropType<any[]>, default: () => [] },
|
||||
selectionContext: { type: [String, Number, Array, Object, Function], default: undefined },
|
||||
paginationConfig: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -239,7 +242,7 @@ const selectionColumn = computed(() =>
|
||||
columnNodes.value.find((column) => (column.props as Record<string, any> | null)?.type === 'selection'),
|
||||
);
|
||||
const tableData = computed(() => {
|
||||
const data = attrs.data;
|
||||
const data = props.data;
|
||||
return Array.isArray(data) ? data : [];
|
||||
});
|
||||
const cardContentStyle = computed(() => {
|
||||
@@ -295,7 +298,7 @@ const {
|
||||
isDisabled: isRightButtonDisabled,
|
||||
click: rightButtonClick,
|
||||
} = useContextMenu(() => props.rightButtons);
|
||||
const getTableData = () => (Array.isArray(attrs.data) ? attrs.data : []);
|
||||
const getTableData = () => props.data || [];
|
||||
const isRowSelectable = (row: any) => {
|
||||
const selectable = (selectionColumn.value?.props as Record<string, any> | null)?.selectable;
|
||||
return typeof selectable === 'function' ? selectable(row) : true;
|
||||
@@ -304,7 +307,6 @@ const {
|
||||
selectedRows,
|
||||
isRowSelected,
|
||||
clearSelects,
|
||||
pruneSelection,
|
||||
toggleSelection,
|
||||
selectRow,
|
||||
syncTableSelection,
|
||||
@@ -313,7 +315,18 @@ const {
|
||||
handleRowClick,
|
||||
handleKeyDown: handleSelectionKeyDown,
|
||||
handleKeyUp,
|
||||
} = useTableSelection(tableRef, getTableData, (rows) => emit('update:selects', rows), isRowSelectable);
|
||||
} = useTableSelection(
|
||||
tableRef,
|
||||
getTableData,
|
||||
(rows) => emit('update:selects', rows),
|
||||
isRowSelectable,
|
||||
(row) => {
|
||||
const key = attrs.rowKey ?? attrs['row-key'];
|
||||
if (typeof key === 'function') return row == null ? undefined : key(row);
|
||||
if (typeof key === 'string') return key.split('.').reduce((value, part) => value?.[part], row);
|
||||
return row?.id;
|
||||
},
|
||||
);
|
||||
const toggleCardSelection = (row: any, selected: boolean) => {
|
||||
selectRow(row, selected);
|
||||
};
|
||||
@@ -446,7 +459,26 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
|
||||
flush: 'post',
|
||||
});
|
||||
|
||||
watch(tableData, pruneSelection);
|
||||
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
|
||||
flush: 'sync',
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
currentNode,
|
||||
() =>
|
||||
JSON.stringify(
|
||||
typeof props.selectionContext === 'function' ? props.selectionContext() : props.selectionContext,
|
||||
),
|
||||
],
|
||||
() => {
|
||||
clearSelects();
|
||||
if (props.paginationConfig) {
|
||||
props.paginationConfig.currentPage = 1;
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', calcHeight);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { shallowRef, ref, toRaw, type Ref } from 'vue';
|
||||
import { shallowRef, ref, toRaw, watch, type Ref } from 'vue';
|
||||
|
||||
type TableRef = { refElTable?: any } | undefined;
|
||||
|
||||
@@ -7,6 +7,7 @@ export const useTableSelection = (
|
||||
getTableData: () => any[],
|
||||
onSelectionChange: (rows: any[]) => void,
|
||||
isRowSelectable: (row: any) => boolean = () => true,
|
||||
getRowKey: (row: any) => unknown = (row) => row?.id,
|
||||
) => {
|
||||
const selectedRows = shallowRef<any[]>([]);
|
||||
const shiftPressed = ref(false);
|
||||
@@ -15,8 +16,13 @@ export const useTableSelection = (
|
||||
let isSyncingTableSelection = false;
|
||||
let skipNextSelectionChange = false;
|
||||
|
||||
// Element Plus can return reactive proxies for rows supplied as plain objects.
|
||||
const sameRow = (left: any, right: any) => toRaw(left) === toRaw(right);
|
||||
let isRefreshingData = false;
|
||||
const sameRow = (left: any, right: any) => {
|
||||
if (left == null || right == null) return left === right;
|
||||
const leftKey = getRowKey(left);
|
||||
const rightKey = getRowKey(right);
|
||||
return leftKey != null && rightKey != null ? leftKey === rightKey : toRaw(left) === toRaw(right);
|
||||
};
|
||||
const hasRow = (rows: any[], row: any) => rows.some((item) => sameRow(item, row));
|
||||
const isRowSelected = (row: any) => hasRow(selectedRows.value, row);
|
||||
const getTable = () => tableRef.value?.refElTable;
|
||||
@@ -39,8 +45,12 @@ export const useTableSelection = (
|
||||
const tableData = getTableData();
|
||||
const nextRows = selectedRows.value.filter((row) => hasRow(tableData, row) && isRowSelectable(row));
|
||||
const currentRows = table.getSelectionRows();
|
||||
currentRows.filter((row) => !hasRow(nextRows, row)).forEach((row) => table.toggleRowSelection(row, false));
|
||||
nextRows.filter((row) => !hasRow(currentRows, row)).forEach((row) => table.toggleRowSelection(row, true));
|
||||
currentRows
|
||||
.filter((row) => !nextRows.some((item) => toRaw(item) === toRaw(row)))
|
||||
.forEach((row) => table.toggleRowSelection(row, false));
|
||||
nextRows
|
||||
.filter((row) => !currentRows.some((item) => toRaw(item) === toRaw(row)))
|
||||
.forEach((row) => table.toggleRowSelection(row, true));
|
||||
} finally {
|
||||
isSyncingTableSelection = false;
|
||||
}
|
||||
@@ -73,7 +83,7 @@ export const useTableSelection = (
|
||||
return true;
|
||||
};
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
if (isSyncingTableSelection) {
|
||||
if (isSyncingTableSelection || isRefreshingData) {
|
||||
return;
|
||||
}
|
||||
if (skipNextSelectionChange) {
|
||||
@@ -104,16 +114,35 @@ export const useTableSelection = (
|
||||
rangeBaseRows.value = [];
|
||||
};
|
||||
const pruneSelection = () => {
|
||||
const nextRows = selectedRows.value.filter((row) => hasRow(getTableData(), row));
|
||||
if (nextRows.length !== selectedRows.value.length) {
|
||||
const data = getTableData();
|
||||
const nextRows = data.filter((row) => hasRow(selectedRows.value, row) && isRowSelectable(row));
|
||||
if (nextRows.length !== selectedRows.value.length || nextRows.some((row, i) => row !== selectedRows.value[i])) {
|
||||
setSelectedRows(nextRows);
|
||||
}
|
||||
if (lastSelectedRow.value && !hasRow(nextRows, lastSelectedRow.value)) {
|
||||
lastSelectedRow.value = null;
|
||||
rangeBaseRows.value = [];
|
||||
}
|
||||
syncTableSelection();
|
||||
lastSelectedRow.value = nextRows.find((row) => sameRow(row, lastSelectedRow.value)) ?? null;
|
||||
rangeBaseRows.value = lastSelectedRow.value ? nextRows.filter((row) => hasRow(rangeBaseRows.value, row)) : [];
|
||||
};
|
||||
// Guard before the child table reacts to replacement data and emits an empty selection.
|
||||
watch(
|
||||
getTableData,
|
||||
() => {
|
||||
isRefreshingData = true;
|
||||
skipNextSelectionChange = false;
|
||||
pruneSelection();
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
watch(
|
||||
getTableData,
|
||||
() => {
|
||||
try {
|
||||
syncTableSelection();
|
||||
} finally {
|
||||
isRefreshingData = false;
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
const toggleSelection = () => {
|
||||
const selectableRows = getTableData().filter(isRowSelectable);
|
||||
const allSelected = selectableRows.length > 0 && selectableRows.every(isRowSelected);
|
||||
@@ -155,7 +184,6 @@ export const useTableSelection = (
|
||||
selectedRows,
|
||||
isRowSelected,
|
||||
clearSelects,
|
||||
pruneSelection,
|
||||
toggleSelection,
|
||||
selectRow,
|
||||
syncTableSelection,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { watch, type Ref } from 'vue';
|
||||
import GlobalStore from '@/store/modules/global';
|
||||
|
||||
type SearchValue = string | number | undefined;
|
||||
type SearchFields = Record<string, Ref<SearchValue>>;
|
||||
|
||||
export const useSearchPersistence = (page: string, fields: SearchFields, scope?: () => SearchValue) => {
|
||||
const { currentNode } = storeToRefs(GlobalStore());
|
||||
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
|
||||
let initialized = false;
|
||||
let restoring = false;
|
||||
let activeKey: string | undefined;
|
||||
|
||||
watch(
|
||||
() => {
|
||||
const context = scope?.();
|
||||
if (scope && context === undefined) return undefined;
|
||||
return `1panel:search:v1:${JSON.stringify([currentNode.value, page, context ?? ''])}`;
|
||||
},
|
||||
(key) => {
|
||||
activeKey = key;
|
||||
let saved: Record<string, unknown> = {};
|
||||
try {
|
||||
const parsed = activeKey ? JSON.parse(localStorage.getItem(activeKey) || '{}') : {};
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = parsed;
|
||||
} catch {}
|
||||
restoring = true;
|
||||
for (const [name, field] of Object.entries(fields)) {
|
||||
const value = saved[name];
|
||||
const initial = defaults[name];
|
||||
let fallback = initial;
|
||||
if (initialized) {
|
||||
fallback = typeof initial === 'string' ? '' : undefined;
|
||||
}
|
||||
const valid =
|
||||
(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) &&
|
||||
(fallback === undefined || typeof value === typeof fallback);
|
||||
if (value === null) {
|
||||
field.value = typeof initial === 'string' ? '' : undefined;
|
||||
} else if (valid) {
|
||||
field.value = value as SearchValue;
|
||||
} else {
|
||||
field.value = fallback;
|
||||
}
|
||||
}
|
||||
restoring = false;
|
||||
initialized = true;
|
||||
},
|
||||
{ immediate: true, flush: 'sync' },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value ?? null])),
|
||||
(value) => {
|
||||
if (restoring || !activeKey) return;
|
||||
try {
|
||||
localStorage.setItem(activeKey, JSON.stringify(value));
|
||||
} catch {}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
};
|
||||
@@ -2888,6 +2888,9 @@ const message = {
|
||||
privateKey: 'Private Key',
|
||||
publicKey: 'Public Key',
|
||||
password: 'Password',
|
||||
existingPassPhrase: 'Existing private key passphrase',
|
||||
existingPassPhraseHelper:
|
||||
'Enter the original passphrase if the private key is encrypted; otherwise leave blank. This does not change the passphrase.',
|
||||
createMode: 'Creation Method',
|
||||
generate: 'Auto-generate',
|
||||
unSyncPass: 'Key password cannot be synchronized',
|
||||
@@ -4247,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.',
|
||||
@@ -4308,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',
|
||||
@@ -4317,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',
|
||||
@@ -4334,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.',
|
||||
@@ -4356,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}?',
|
||||
@@ -4414,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',
|
||||
@@ -4424,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.',
|
||||
@@ -4467,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:
|
||||
@@ -4485,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:
|
||||
@@ -7231,12 +7224,17 @@ const message = {
|
||||
storagePoolHelper: 'The storage pool stores VM disks and ISO files.',
|
||||
network: 'Network',
|
||||
bridgeName: 'Bridge Name',
|
||||
sourceInterface: 'Host network interface',
|
||||
macvtapNetworkHelper:
|
||||
'The VM acts like a separate computer on your LAN. Other devices can access its IP directly, without changing the host’s existing IP settings. Note: the host and VM cannot communicate directly through this connection by default.',
|
||||
macvtapInterfaceHelper:
|
||||
'Select a wired interface connected to the intended LAN; it can be shared with the host. The LAN assigns the VM’s IP, or you can set it manually in the VM. The network must allow multiple devices on this interface (multiple MAC addresses).',
|
||||
natNetworkHelper:
|
||||
'VMs can access the network through the host, but external devices usually cannot access them directly.',
|
||||
'Suitable for VMs that need internet access or expose only a few services. The VM goes online through the host. Other devices usually need separately configured port forwarding to access its services.',
|
||||
bridgeNetworkHelper:
|
||||
'Connect VMs to an existing host bridge (such as br0). If the host has an unused additional NIC, create a bridge for it and use it.',
|
||||
'The VM acts like a separate computer connected to a network switch. Both the host and other LAN devices can access it directly. First, configure a bridge on the host.',
|
||||
bridgeNameHelper:
|
||||
'Select an existing Linux bridge on the host, such as br0. Physical NICs are not bridges, and docker/libvirt managed bridges cannot be selected here.',
|
||||
'Select a configured bridge, such as br0. If none are available, create one on the host first. Physical interfaces and bridges managed by Docker/libvirt cannot be selected here.',
|
||||
natCIDRHelper: 'CIDR, gateway, and DHCP range are used by the new NAT virtual network.',
|
||||
networkCIDR: 'Network CIDR',
|
||||
gateway: 'Gateway',
|
||||
@@ -7254,6 +7252,7 @@ const message = {
|
||||
available: 'Available',
|
||||
vmCount: 'Linked VMs',
|
||||
usedIPs: 'Used IPs',
|
||||
usedIPsUnavailable: 'Retrieval not supported yet',
|
||||
monitor: 'Monitor',
|
||||
monitorNotReady: 'The virtual machine is not running. Monitoring data is unavailable.',
|
||||
monitorLoadFailed: 'Failed to get VM monitoring data.',
|
||||
|
||||
@@ -2930,6 +2930,9 @@ const message = {
|
||||
privateKey: 'Clave privada',
|
||||
publicKey: 'Clave pública',
|
||||
password: 'Contraseña',
|
||||
existingPassPhrase: 'Contraseña actual de la clave privada',
|
||||
existingPassPhraseHelper:
|
||||
'Introduce la contraseña original si la clave privada está cifrada; en caso contrario, deja el campo vacío. Esto no cambia la contraseña.',
|
||||
createMode: 'Método de creación',
|
||||
generate: 'Autogenerar',
|
||||
unSyncPass: 'La contraseña de la clave no se puede sincronizar',
|
||||
@@ -4292,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.',
|
||||
@@ -4357,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',
|
||||
@@ -4366,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',
|
||||
@@ -4383,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.',
|
||||
@@ -4405,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}?',
|
||||
@@ -4464,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',
|
||||
@@ -4474,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.',
|
||||
@@ -4520,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:
|
||||
@@ -4538,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:
|
||||
@@ -7346,12 +7343,17 @@ const message = {
|
||||
'El pool de almacenamiento guarda los discos de las máquinas virtuales y los archivos ISO.',
|
||||
network: 'Red',
|
||||
bridgeName: 'Nombre del bridge',
|
||||
sourceInterface: 'Interfaz de red del anfitrión',
|
||||
macvtapNetworkHelper:
|
||||
'La VM funciona como otro ordenador de la LAN. Otros dispositivos pueden acceder directamente a su IP sin cambiar la configuración IP del anfitrión. Nota: de forma predeterminada, el anfitrión y la VM no pueden comunicarse directamente por esta conexión.',
|
||||
macvtapInterfaceHelper:
|
||||
'Seleccione una interfaz cableada conectada a la LAN deseada; puede compartirla con el anfitrión. La LAN asigna la IP de la VM, o puede configurarla manualmente en ella. La red debe permitir varios dispositivos en esta interfaz (varias direcciones MAC).',
|
||||
natNetworkHelper:
|
||||
'Las VM pueden acceder a la red a través del host, pero los dispositivos externos normalmente no pueden acceder a ellas directamente.',
|
||||
'Adecuado para VM que necesitan acceso a Internet o publican pocos servicios. La VM sale a Internet a través del anfitrión. Para acceder a sus servicios desde otros dispositivos, normalmente debe configurar el reenvío de puertos por separado.',
|
||||
bridgeNetworkHelper:
|
||||
'Conecta las VM a un bridge existente del host (como br0). Si el host tiene una NIC adicional sin usar, puedes crear un bridge para ella y utilizarlo.',
|
||||
'La VM funciona como un ordenador independiente conectado a un conmutador. Tanto el anfitrión como otros dispositivos de la LAN pueden acceder directamente. Primero debe configurar un puente en el anfitrión.',
|
||||
bridgeNameHelper:
|
||||
'Seleccione un Linux bridge existente en el host, como br0. Las NIC físicas no son bridges, y los bridges gestionados por docker/libvirt no pueden seleccionarse aquí.',
|
||||
'Seleccione un puente configurado, como br0. Si no hay ninguno, créelo primero en el anfitrión. No puede seleccionar interfaces físicas ni puentes administrados por Docker/libvirt.',
|
||||
natCIDRHelper: 'CIDR, puerta de enlace y rango DHCP se usarán en la nueva red virtual NAT.',
|
||||
networkCIDR: 'CIDR de red',
|
||||
gateway: 'Puerta de enlace',
|
||||
@@ -7369,6 +7371,7 @@ const message = {
|
||||
available: 'Disponible',
|
||||
vmCount: 'VM vinculadas',
|
||||
usedIPs: 'IP usadas',
|
||||
usedIPsUnavailable: 'Obtención aún no disponible',
|
||||
monitor: 'Supervisión',
|
||||
monitorNotReady: 'La máquina virtual no está en ejecución. Los datos de supervisión no están disponibles.',
|
||||
monitorLoadFailed: 'No se pudieron obtener los datos de supervisión de la VM.',
|
||||
|
||||
@@ -2861,6 +2861,9 @@ const message = {
|
||||
privateKey: 'کلید خصوصی',
|
||||
publicKey: 'کلید عمومی',
|
||||
password: 'رمز عبور',
|
||||
existingPassPhrase: 'عبارت عبور فعلی کلید خصوصی',
|
||||
existingPassPhraseHelper:
|
||||
'اگر کلید خصوصی رمزگذاری شده است، عبارت عبور اصلی را وارد کنید؛ در غیر این صورت خالی بگذارید. این کار عبارت عبور را تغییر نمیدهد.',
|
||||
createMode: 'روش ایجاد',
|
||||
generate: 'تولید خودکار',
|
||||
unSyncPass: 'رمز عبور کلید قابل همگامسازی نیست',
|
||||
@@ -4210,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: 'دیواره آتش سیستم در حال حاضر فعال نیست. ابتدا آن را فعال کنید.',
|
||||
@@ -4263,8 +4271,7 @@ const message = {
|
||||
dockerGuard: 'محافظت از پورت کانتینر',
|
||||
systemFirewall: 'فایروال میزبان',
|
||||
systemFirewallHelper: 'دسترسی پورتهای میزبان و قوانین ورودی را کنترل میکند.',
|
||||
forwardPolicyDropWarning:
|
||||
'سیاست پیشفرض FORWARD برای {0} برابر DROP است. ترافیک هدایتشده به میزبانهای دیگر ممکن است مسدود شود، مگر اینکه قواعد iptables/ip6tables صریحاً آن را مجاز کنند. قواعد مجازکننده نسخه IP مربوطه را بررسی کنید.',
|
||||
forwardPolicyDropWarning: 'این سرور هدایت ترافیک شبکه را محدود کرده است. ممکن است قوانین هدایت پورت کار نکنند.',
|
||||
forwardingHelper: 'قوانین انتقال پورت را مدیریت میکند.',
|
||||
dockerFirewallHelper: 'روش مدیریت محافظت از پورت کانتینر در 1Panel را انتخاب میکند.',
|
||||
dockerNftablesRequirement: 'Docker ≥ 29.0.0، آزمایشی',
|
||||
@@ -4273,6 +4280,7 @@ const message = {
|
||||
addressFamily: 'نسخه IP',
|
||||
portOrRange: 'پورت / بازه',
|
||||
batchLimit: 'در هر نوبت حداکثر {0} قانون پس از گسترش ایجاد یا وارد کنید.',
|
||||
importDuplicatesRemoved: '{0} قانون تکراری، شامل موارد تکراری در فایل و قوانین موجود، حذف شد.',
|
||||
importLimit: 'حداکثر {0} قانون (پس از گسترش) وارد کنید؛ حجم فایل نباید از {1} کیلوبایت بیشتر باشد.',
|
||||
ruleSyncTitle: 'همگامسازی قوانین',
|
||||
ruleSyncAction: 'همگامسازی قوانین',
|
||||
@@ -4290,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} حذف شد.',
|
||||
@@ -4312,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} تغییر داده شود؟',
|
||||
@@ -4371,8 +4371,7 @@ const message = {
|
||||
portDetails: 'جزئیات پورت',
|
||||
orphanEndpoints: 'نقاط پایانی بدون ارتباط',
|
||||
orphanPolicies: 'قوانین بدون ارتباط',
|
||||
orphanPoliciesHelper:
|
||||
'{0} قانون زیر به پورت Docker متصل نیستند. با شناسایی پورت منطبق، بهطور خودکار متصل میشوند.',
|
||||
orphanPoliciesHelper: 'این {0} قانون سیستم پورت منتشرشده منطبق در Docker ندارند.',
|
||||
composeOrApp: 'Compose / برنامه',
|
||||
sources: 'منابع',
|
||||
deniedSources: 'منابع ردشده',
|
||||
@@ -4381,6 +4380,8 @@ const message = {
|
||||
protectionMode: 'حالت محافظت',
|
||||
denySources: 'رد منابع مشخصشده',
|
||||
allowSources: 'فقط اجازه به منابع مشخصشده',
|
||||
acceptSources: 'پذیرش مستقیم منابع مشخصشده',
|
||||
acceptAll: 'پذیرش مستقیم تمام ترافیک',
|
||||
denyAll: 'رد همه دسترسیها',
|
||||
dockerGuardMixedFamilyHelper:
|
||||
'هر دو IPv4 و IPv6 انتخاب شدهاند. سیاستهای مبتنی بر مبدأ را جداگانه تنظیم کنید؛ رد همه دسترسیها را میتوان مستقیماً اعمال کرد.',
|
||||
@@ -4423,7 +4424,7 @@ const message = {
|
||||
'قطع اتصال - در صورت قطع اتصال، همه قوانین دیواره آتش اضافه شده بیاثر میشوند. با احتیاط ادامه دهید. تأیید میکنید؟',
|
||||
portWhiteList: 'لیست سفید پورت',
|
||||
whitelistConfigHelper:
|
||||
'فهرست مجاز فقط تنظیمات را ذخیره میکند و از قوانین مجاز منطبق محافظت میکند. قوانین مفقود هنگام شروع، راهاندازی مجدد، مقداردهی اولیه یا همگامسازی اضافه میشوند. ویرایش یا حذف موارد فهرست مجاز، قوانین موجود را حذف نمیکند.',
|
||||
'با ذخیره فهرست مجاز، قوانین اجازهٔ مفقود اضافه و قوانین منطبق محافظت میشوند. ویرایش یا حذف موارد فهرست، قوانین موجود را حذف نمیکند.',
|
||||
whitelistDeleteConfirm:
|
||||
'فقط این ورودی فهرست سفید حذف میشود. قوانین موجود فایروال باقی میمانند. ادامه میدهید؟',
|
||||
portWhiteListHelper:
|
||||
@@ -4441,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:
|
||||
@@ -7185,12 +7186,17 @@ const message = {
|
||||
storagePoolHelper: 'استخر ذخیرهسازی، دیسکهای ماشین مجازی و فایلهای ISO را ذخیره میکند.',
|
||||
network: 'شبکه',
|
||||
bridgeName: 'نام Bridge',
|
||||
sourceInterface: 'رابط شبکه میزبان',
|
||||
macvtapNetworkHelper:
|
||||
'ماشین مجازی مانند یک رایانه مستقل در شبکه محلی عمل میکند. دستگاههای دیگر بدون تغییر تنظیمات IP میزبان، مستقیماً به IP آن دسترسی دارند. توجه: بهطور پیشفرض میزبان و ماشین مجازی از طریق این اتصال ارتباط مستقیم ندارند.',
|
||||
macvtapInterfaceHelper:
|
||||
'رابط سیمی متصل به شبکه محلی موردنظر را انتخاب کنید؛ میتوان آن را با میزبان مشترک استفاده کرد. IP ماشین مجازی را شبکه تخصیص میدهد یا میتوانید در ماشین دستی تنظیم کنید. شبکه باید اتصال چند دستگاه از این رابط را مجاز بداند (چند آدرس MAC).',
|
||||
natNetworkHelper:
|
||||
'ماشین مجازی میتواند از طریق میزبان به شبکه دسترسی داشته باشد، اما دستگاههای خارجی معمولاً نمیتوانند مستقیماً به آن دسترسی پیدا کنند.',
|
||||
'برای ماشینهایی مناسب است که به اینترنت نیاز دارند یا فقط چند سرویس ارائه میکنند. ماشین مجازی از طریق میزبان به اینترنت وصل میشود. دستگاههای دیگر معمولاً برای دسترسی به سرویسهای آن به تنظیم جداگانه هدایت پورت نیاز دارند.',
|
||||
bridgeNetworkHelper:
|
||||
'ماشین مجازی را به bridge موجود میزبان (مانند br0) متصل کنید. اگر میزبان کارت شبکه اضافیِ استفادهنشده دارد، میتوانید برای آن bridge بسازید و استفاده کنید.',
|
||||
'ماشین مجازی مانند یک رایانه مستقل متصل به سوئیچ شبکه عمل میکند. میزبان و دستگاههای دیگر شبکه محلی مستقیماً به آن دسترسی دارند. ابتدا باید یک پل شبکه روی میزبان تنظیم کنید.',
|
||||
bridgeNameHelper:
|
||||
'یک Linux bridge موجود روی میزبان، مانند br0، انتخاب کنید. کارت شبکه فیزیکی Bridge نیست و Bridgeهای مدیریتشده توسط docker/libvirt را نمیتوان اینجا انتخاب کرد.',
|
||||
'یک پل تنظیمشده مانند br0 انتخاب کنید. اگر گزینهای نیست، ابتدا روی میزبان پل بسازید. رابطهای فیزیکی و پلهای تحت مدیریت Docker/libvirt در اینجا قابل انتخاب نیستند.',
|
||||
natCIDRHelper: 'CIDR، دروازه و بازه DHCP برای ایجاد شبکه مجازی NAT استفاده میشوند.',
|
||||
networkCIDR: 'CIDR شبکه',
|
||||
gateway: 'دروازه',
|
||||
@@ -7208,6 +7214,7 @@ const message = {
|
||||
available: 'در دسترس',
|
||||
vmCount: 'ماشینهای مجازی مرتبط',
|
||||
usedIPs: 'IPهای استفادهشده',
|
||||
usedIPsUnavailable: 'دریافت هنوز پشتیبانی نمیشود',
|
||||
monitor: 'مانیتورینگ',
|
||||
monitorNotReady: 'ماشین مجازی در حال اجرا نیست و دادههای مانیتورینگ در دسترس نیست.',
|
||||
monitorLoadFailed: 'دریافت دادههای مانیتورینگ ماشین مجازی ناموفق بود.',
|
||||
|
||||
@@ -2865,6 +2865,9 @@ const message = {
|
||||
privateKey: '秘密鍵',
|
||||
publicKey: '公開鍵',
|
||||
password: 'パスワード',
|
||||
existingPassPhrase: '既存の秘密鍵のパスフレーズ',
|
||||
existingPassPhraseHelper:
|
||||
'秘密鍵が暗号化されている場合は元のパスフレーズを入力し、それ以外は空欄にしてください。パスフレーズは変更されません。',
|
||||
createMode: '作成方法',
|
||||
generate: '自動生成',
|
||||
unSyncPass: '鍵パスワードは同期できません',
|
||||
@@ -4233,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: '現在、システムファイアウォールは有効になっていません。最初に有効にします。',
|
||||
@@ -4289,7 +4295,7 @@ const message = {
|
||||
systemFirewall: 'ホストファイアウォール',
|
||||
systemFirewallHelper: 'ホストのポートアクセスと受信ルールを管理します。',
|
||||
forwardPolicyDropWarning:
|
||||
'{0} の FORWARD のデフォルトポリシーは DROP です。iptables/ip6tables のルールで明示的に許可されていない他のホストへの転送トラフィックは、遮断される可能性があります。該当する IP バージョンの許可ルールを確認してください。',
|
||||
'このサーバーではネットワーク転送が制限されているため、ポート転送ルールが機能しない可能性があります。',
|
||||
forwardingHelper: 'ポート転送ルールを管理します。',
|
||||
dockerFirewallHelper: '1Panel のコンテナポート保護の管理方法を選択します。',
|
||||
dockerNftablesRequirement: 'Docker ≥ 29.0.0、実験的',
|
||||
@@ -4298,6 +4304,7 @@ const message = {
|
||||
addressFamily: 'IP バージョン',
|
||||
portOrRange: 'ポート / 範囲',
|
||||
batchLimit: '一度に作成またはインポートできるルールは、展開後の件数で最大 {0} 件です。',
|
||||
importDuplicatesRemoved: 'ファイル内の重複と既存ルールを含む、{0} 件の重複ルールを除外しました。',
|
||||
importLimit: '最大 {0} 件のルール(展開後の件数)をインポートできます。ファイルサイズの上限は {1} KB です。',
|
||||
ruleSyncTitle: 'ルールを同期',
|
||||
ruleSyncAction: 'ルールを同期',
|
||||
@@ -4315,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} 件。',
|
||||
@@ -4337,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} に切り替えますか?',
|
||||
@@ -4393,8 +4391,7 @@ const message = {
|
||||
portDetails: 'ポート詳細',
|
||||
orphanEndpoints: '未関連付けエンドポイント',
|
||||
orphanPolicies: '未関連付けルール',
|
||||
orphanPoliciesHelper:
|
||||
'以下の {0} 件のルールは Docker ポートにバインドされていません。一致するポートが検出されると自動的にバインドされます。',
|
||||
orphanPoliciesHelper: 'これら {0} 件のシステムルールに一致する Docker 公開ポートはありません。',
|
||||
composeOrApp: 'Compose / アプリ',
|
||||
sources: '送信元',
|
||||
deniedSources: '拒否する送信元',
|
||||
@@ -4403,6 +4400,8 @@ const message = {
|
||||
protectionMode: '保護モード',
|
||||
denySources: '指定した送信元を拒否',
|
||||
allowSources: '指定した送信元のみ許可',
|
||||
acceptSources: '指定した送信元を直接許可',
|
||||
acceptAll: 'すべての通信を直接許可',
|
||||
denyAll: 'すべてのアクセスを拒否',
|
||||
dockerGuardMixedFamilyHelper:
|
||||
'IPv4 と IPv6 の両方が選択されています。送信元指定は個別に設定してください。「すべて拒否」はそのまま適用できます。',
|
||||
@@ -4446,7 +4445,7 @@ const message = {
|
||||
'アンバインド - アンバインドすると、追加されたすべてのファイアウォールルールが無効になります。注意して操作してください。確認しますか?',
|
||||
portWhiteList: 'ポートホワイトリスト',
|
||||
whitelistConfigHelper:
|
||||
'許可リストは設定の保存と一致する許可ルールの保護のみを行います。不足するルールは起動、再起動、初期化、同期時に追加されます。許可リストを編集・削除しても既存のルールは削除されません。',
|
||||
'許可リストの保存時に不足する許可ルールを追加し、一致するルールを保護します。許可リストの編集や削除では既存のルールは削除されません。',
|
||||
whitelistDeleteConfirm:
|
||||
'このホワイトリスト項目のみを削除します。既存のファイアウォールルールは保持されます。続行しますか?',
|
||||
portWhiteListHelper:
|
||||
@@ -4464,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:
|
||||
@@ -7217,12 +7216,17 @@ const message = {
|
||||
storagePoolHelper: 'ストレージプールは仮想マシンのディスクと ISO ファイルを保存します。',
|
||||
network: 'ネットワーク',
|
||||
bridgeName: 'ブリッジ名',
|
||||
sourceInterface: 'ホストのネットワークインターフェース',
|
||||
macvtapNetworkHelper:
|
||||
'VM を LAN 内の独立したコンピューターのように使えます。ホストの既存 IP 設定を変更せず、他の機器から VM の IP に直接アクセスできます。注意:既定では、この接続を通じてホストと VM は直接通信できません。',
|
||||
macvtapInterfaceHelper:
|
||||
'接続先の LAN につながる有線インターフェースを選択します。ホストと共有できます。VM の IP は LAN から割り当てられるか、VM 内で手動設定します。ネットワーク側で複数の機器(複数の MAC アドレス)の接続を許可する必要があります。',
|
||||
natNetworkHelper:
|
||||
'VM はホスト経由でネットワークに接続できますが、外部の機器から VM へ直接アクセスすることは通常できません。',
|
||||
'インターネット接続だけが必要な VM や、少数のサービスを公開する VM に適しています。VM はホスト経由で通信します。他の機器からサービスにアクセスするには、通常、別途ポート転送の設定が必要です。',
|
||||
bridgeNetworkHelper:
|
||||
'VM をホスト上の既存ブリッジ(br0 など)に接続します。未使用の追加 NIC がある場合は、その NIC 用のブリッジを作成して使用できます。',
|
||||
'VM をネットワークスイッチにつないだ独立したコンピューターのように使えます。ホストと LAN 内の他の機器から直接アクセスできます。先にホストでブリッジを設定してください。',
|
||||
bridgeNameHelper:
|
||||
'br0 など、ホスト上に既に存在する Linux bridge を選択してください。物理 NIC は bridge ではなく、docker/libvirt 管理の bridge はここでは選択できません。',
|
||||
'設定済みのブリッジ(例:br0)を選択します。選択肢がない場合は、先にホストで作成してください。物理インターフェースや Docker/libvirt が管理するブリッジは選択できません。',
|
||||
natCIDRHelper: 'CIDR、ゲートウェイ、DHCP アドレス範囲は新しい NAT 仮想ネットワークに使用されます。',
|
||||
networkCIDR: 'ネットワーク CIDR',
|
||||
gateway: 'ゲートウェイ',
|
||||
@@ -7240,6 +7244,7 @@ const message = {
|
||||
available: '利用可能',
|
||||
vmCount: '関連 VM',
|
||||
usedIPs: '使用済み IP',
|
||||
usedIPsUnavailable: '取得は未対応です',
|
||||
monitor: '監視',
|
||||
monitorNotReady: '仮想マシンが実行されていないため、監視データを利用できません。',
|
||||
monitorLoadFailed: 'VM の監視データを取得できませんでした。',
|
||||
|
||||
@@ -2827,6 +2827,9 @@ const message = {
|
||||
privateKey: '개인 키',
|
||||
publicKey: '공개 키',
|
||||
password: '비밀번호',
|
||||
existingPassPhrase: '기존 개인 키 암호',
|
||||
existingPassPhraseHelper:
|
||||
'개인 키가 암호화되어 있으면 원래 암호를 입력하고, 그렇지 않으면 비워 두세요. 이 작업은 암호를 변경하지 않습니다.',
|
||||
createMode: '생성 방식',
|
||||
generate: '자동 생성',
|
||||
unSyncPass: '키 비밀번호 동기화 불가',
|
||||
@@ -4156,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: '현재 시스템 방화벽이 활성화되지 않았습니다. 먼저 활성화하세요.',
|
||||
@@ -4210,8 +4215,7 @@ const message = {
|
||||
dockerGuard: '컨테이너 포트 보호',
|
||||
systemFirewall: '호스트 방화벽',
|
||||
systemFirewallHelper: '호스트 포트 접근과 인바운드 규칙을 관리합니다.',
|
||||
forwardPolicyDropWarning:
|
||||
'{0}의 FORWARD 기본 정책은 DROP입니다. iptables/ip6tables 규칙에서 명시적으로 허용하지 않은 다른 호스트로의 전달 트래픽은 차단될 수 있습니다. 해당 IP 버전의 허용 규칙을 확인하세요.',
|
||||
forwardPolicyDropWarning: '현재 서버에서 네트워크 전달이 제한되어 포트 전달 규칙이 작동하지 않을 수 있습니다.',
|
||||
forwardingHelper: '포트 포워딩 규칙을 관리합니다.',
|
||||
dockerFirewallHelper: '1Panel 컨테이너 포트 보호 관리 방식을 선택합니다.',
|
||||
dockerNftablesRequirement: 'Docker ≥ 29.0.0, 실험적',
|
||||
@@ -4220,6 +4224,7 @@ const message = {
|
||||
addressFamily: 'IP 버전',
|
||||
portOrRange: '포트 / 범위',
|
||||
batchLimit: '한 번에 생성하거나 가져올 수 있는 규칙은 확장 후 기준으로 최대 {0}개입니다.',
|
||||
importDuplicatesRemoved: '파일 내 중복 및 이미 존재하는 규칙을 포함하여 중복 규칙 {0}개를 제거했습니다.',
|
||||
importLimit: '최대 {0}개의 규칙(확장 후 기준)을 가져올 수 있으며, 파일 크기는 {1} KB를 초과할 수 없습니다.',
|
||||
ruleSyncTitle: '규칙 동기화',
|
||||
ruleSyncAction: '규칙 동기화',
|
||||
@@ -4237,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}개.',
|
||||
@@ -4259,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}(으)로 전환하시겠습니까?',
|
||||
@@ -4316,8 +4311,7 @@ const message = {
|
||||
portDetails: '포트 상세 정보',
|
||||
orphanEndpoints: '연결되지 않은 엔드포인트',
|
||||
orphanPolicies: '연결되지 않은 규칙',
|
||||
orphanPoliciesHelper:
|
||||
'다음 {0}개 규칙은 Docker 포트에 바인딩되지 않았습니다. 일치하는 포트가 감지되면 자동으로 바인딩됩니다.',
|
||||
orphanPoliciesHelper: '이 시스템 규칙 {0}개와 일치하는 Docker 게시 포트가 없습니다.',
|
||||
composeOrApp: 'Compose / 앱',
|
||||
sources: '소스',
|
||||
deniedSources: '거부된 소스',
|
||||
@@ -4326,6 +4320,8 @@ const message = {
|
||||
protectionMode: '보호 모드',
|
||||
denySources: '지정한 소스 거부',
|
||||
allowSources: '지정한 소스만 허용',
|
||||
acceptSources: '지정된 소스를 직접 허용',
|
||||
acceptAll: '모든 트래픽을 직접 허용',
|
||||
denyAll: '모든 접근 거부',
|
||||
dockerGuardMixedFamilyHelper:
|
||||
'IPv4와 IPv6가 모두 선택되었습니다. 출발지 지정 정책은 각각 설정해야 하며, 모든 접근 거부는 바로 적용할 수 있습니다.',
|
||||
@@ -4367,7 +4363,7 @@ const message = {
|
||||
'바인딩 해제 - 바인딩 해제 시 추가된 모든 방화벽 규칙이 무효화됩니다. 주의하여 진행하세요. 확인하시겠습니까?',
|
||||
portWhiteList: '포트 화이트리스트',
|
||||
whitelistConfigHelper:
|
||||
'허용 목록은 설정만 저장하고 일치하는 허용 규칙을 보호합니다. 누락된 규칙은 시작, 재시작, 초기화 또는 동기화 시 추가됩니다. 허용 목록을 편집하거나 삭제해도 기존 규칙은 삭제되지 않습니다.',
|
||||
'허용 목록을 저장하면 누락된 허용 규칙을 추가하고 일치하는 규칙을 보호합니다. 허용 목록 항목을 수정하거나 삭제해도 기존 규칙은 삭제되지 않습니다.',
|
||||
whitelistDeleteConfirm: '이 화이트리스트 항목만 삭제됩니다. 기존 방화벽 규칙은 유지됩니다. 계속하시겠습니까?',
|
||||
portWhiteListHelper:
|
||||
'단일 포트(예: 80) 또는 포트 범위(예: 8000-8100)를 입력하세요. 포트 번호는 1~65535여야 합니다.',
|
||||
@@ -4384,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:
|
||||
@@ -7078,12 +7074,17 @@ const message = {
|
||||
storagePoolHelper: '스토리지 풀은 가상 머신 디스크와 ISO 파일을 저장합니다.',
|
||||
network: '네트워크',
|
||||
bridgeName: '브리지 이름',
|
||||
sourceInterface: '호스트 네트워크 인터페이스',
|
||||
macvtapNetworkHelper:
|
||||
'VM을 LAN의 독립된 컴퓨터처럼 사용할 수 있습니다. 호스트의 기존 IP 설정을 바꾸지 않고 다른 장치에서 VM의 IP로 직접 접근할 수 있습니다. 참고: 기본적으로 이 연결을 통해 호스트와 VM은 직접 통신할 수 없습니다.',
|
||||
macvtapInterfaceHelper:
|
||||
'대상 LAN에 연결된 유선 인터페이스를 선택하세요. 호스트와 공유할 수 있습니다. VM의 IP는 LAN에서 할당하거나 VM 내부에서 직접 설정합니다. 네트워크에서 이 인터페이스를 통한 여러 장치의 연결(여러 MAC 주소)을 허용해야 합니다.',
|
||||
natNetworkHelper:
|
||||
'VM은 호스트를 통해 네트워크에 연결할 수 있지만, 외부 장치에서는 일반적으로 VM에 직접 접근할 수 없습니다.',
|
||||
'인터넷 접속만 필요하거나 소수의 서비스를 공개하는 VM에 적합합니다. VM은 호스트를 통해 인터넷에 연결됩니다. 다른 장치에서 VM 서비스에 접근하려면 일반적으로 포트 전달을 별도로 설정해야 합니다.',
|
||||
bridgeNetworkHelper:
|
||||
'VM을 호스트의 기존 브리지(예: br0)에 연결합니다. 호스트에 사용하지 않는 추가 네트워크 카드가 있으면 브리지를 만들어 사용할 수 있습니다.',
|
||||
'VM을 네트워크 스위치에 연결된 독립된 컴퓨터처럼 사용할 수 있습니다. 호스트와 LAN의 다른 장치에서 직접 접근할 수 있습니다. 먼저 호스트에 브리지를 설정해야 합니다.',
|
||||
bridgeNameHelper:
|
||||
'br0와 같이 호스트에 이미 존재하는 Linux bridge를 선택하세요. 물리 NIC는 bridge가 아니며 docker/libvirt가 관리하는 bridge는 여기서 선택할 수 없습니다.',
|
||||
'설정된 브리지(예: br0)를 선택하세요. 선택 항목이 없으면 먼저 호스트에 브리지를 만드세요. 물리 인터페이스와 Docker/libvirt가 관리하는 브리지는 선택할 수 없습니다.',
|
||||
natCIDRHelper: 'CIDR, 게이트웨이, DHCP 주소 범위는 새 NAT 가상 네트워크에 사용됩니다.',
|
||||
networkCIDR: '네트워크 CIDR',
|
||||
gateway: '게이트웨이',
|
||||
@@ -7101,6 +7102,7 @@ const message = {
|
||||
available: '사용 가능',
|
||||
vmCount: '연결된 VM',
|
||||
usedIPs: '사용된 IP',
|
||||
usedIPsUnavailable: '아직 조회를 지원하지 않음',
|
||||
monitor: '모니터링',
|
||||
monitorNotReady: '가상 머신이 실행 중이 아니어서 모니터링 데이터를 사용할 수 없습니다.',
|
||||
monitorLoadFailed: 'VM 모니터링 데이터를 가져오지 못했습니다.',
|
||||
|
||||
@@ -2807,6 +2807,9 @@ const message = {
|
||||
privateKey: 'ຄີສ່ວນຕົວ',
|
||||
publicKey: 'ຄີສາທາລະນະ',
|
||||
password: 'ລະຫັດຜ່ານ',
|
||||
existingPassPhrase: 'ລະຫັດຜ່ານເດີມຂອງຄີສ່ວນຕົວ',
|
||||
existingPassPhraseHelper:
|
||||
'ຖ້າຄີສ່ວນຕົວຖືກເຂົ້າລະຫັດ ໃຫ້ປ້ອນລະຫັດຜ່ານເດີມ; ຖ້າບໍ່ ໃຫ້ປະໄວ້ຫວ່າງ. ການດຳເນີນການນີ້ບໍ່ປ່ຽນລະຫັດຜ່ານ.',
|
||||
createMode: 'ວິທີການສ້າງ',
|
||||
generate: 'ສ້າງອັດຕະໂນມັດ',
|
||||
unSyncPass: 'ບໍ່ສາມາດຊິງຄ໌ລະຫັດຜ່ານຄີໄດ້',
|
||||
@@ -4125,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: 'ໄຟວໍລະບົບຍັງບໍ່ໄດ້ເປີດໃຊ້. ກະລຸນາເປີດກ່ອນ.',
|
||||
@@ -4176,8 +4182,7 @@ const message = {
|
||||
dockerGuard: 'ການປ້ອງກັນພອດຄອນເທນເນີ',
|
||||
systemFirewall: 'ໄຟວໍໂຮສ',
|
||||
systemFirewallHelper: 'ຄວບຄຸມການເຂົ້າເຖິງພອດໂຮສ ແລະ ກົດຂາເຂົ້າ.',
|
||||
forwardPolicyDropWarning:
|
||||
'ນະໂຍບາຍ FORWARD ເລີ່ມຕົ້ນສຳລັບ {0} ແມ່ນ DROP. ການຈະລາຈອນທີ່ສົ່ງຕໍ່ໄປຍັງໂຮສອື່ນອາດຖືກບລັອກ ຖ້າບໍ່ມີກົດ iptables/ip6tables ອະນຸຍາດຢ່າງຊັດເຈນ. ກະລຸນາກວດສອບກົດອະນຸຍາດສຳລັບເວີຊັນ IP ທີ່ກ່ຽວຂ້ອງ.',
|
||||
forwardPolicyDropWarning: 'ເຊີບເວີນີ້ຈຳກັດການສົ່ງຕໍ່ເຄືອຂ່າຍ. ກົດການສົ່ງຕໍ່ພອດອາດບໍ່ເຮັດວຽກ.',
|
||||
forwardingHelper: 'ຈັດການກົດການສົ່ງຕໍ່ພອດ.',
|
||||
dockerFirewallHelper: 'ເລືອກວິທີທີ່ 1Panel ຈັດການການປ້ອງກັນພອດຄອນເທນເນີ.',
|
||||
dockerNftablesRequirement: 'Docker ≥ 29.0.0, ທົດລອງ',
|
||||
@@ -4186,6 +4191,7 @@ const message = {
|
||||
addressFamily: 'ລຸ້ນ IP',
|
||||
portOrRange: 'ພອດ / ຊ່ວງ',
|
||||
batchLimit: 'ແຕ່ລະຄັ້ງສາມາດສ້າງ ຫຼື ນຳເຂົ້າໄດ້ສູງສຸດ {0} ກົດ ໂດຍນັບຫຼັງຈາກຂະຫຍາຍແລ້ວ.',
|
||||
importDuplicatesRemoved: 'ໄດ້ລຶບກົດທີ່ຊ້ຳກັນ {0} ລາຍການ ລວມທັງກົດທີ່ຊ້ຳໃນໄຟລ໌ ແລະກົດທີ່ມີຢູ່ແລ້ວ.',
|
||||
importLimit: 'ນຳເຂົ້າໄດ້ສູງສຸດ {0} ກົດ (ນັບຫຼັງຈາກຂະຫຍາຍແລ້ວ), ຂະໜາດໄຟລ໌ບໍ່ເກີນ {1} KB.',
|
||||
ruleSyncTitle: 'ຊິງກົດ',
|
||||
ruleSyncAction: 'ຊິງກົດ',
|
||||
@@ -4202,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}.',
|
||||
@@ -4224,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} ບໍ?',
|
||||
@@ -4278,8 +4274,7 @@ const message = {
|
||||
portDetails: 'ລາຍລະອຽດພອດ',
|
||||
orphanEndpoints: 'ຈຸດເຊື່ອມຕໍ່ທີ່ບໍ່ກ່ຽວຂ້ອງ',
|
||||
orphanPolicies: 'ກົດທີ່ບໍ່ໄດ້ເຊື່ອມໂຍງ',
|
||||
orphanPoliciesHelper:
|
||||
'ກົດ {0} ລາຍການຕໍ່ໄປນີ້ຍັງບໍ່ໄດ້ຜູກກັບພອດ Docker. ກົດເຫຼົ່ານີ້ຈະຖືກຜູກອັດຕະໂນມັດເມື່ອກວດພົບພອດທີ່ກົງກັນ.',
|
||||
orphanPoliciesHelper: 'ກົດລະບົບ {0} ລາຍການນີ້ບໍ່ມີພອດ Docker ທີ່ເຜີຍແຜ່ທີ່ກົງກັນ.',
|
||||
composeOrApp: 'Compose / ແອັບ',
|
||||
sources: 'ແຫຼ່ງ',
|
||||
deniedSources: 'ແຫຼ່ງທີ່ປະຕິເສດ',
|
||||
@@ -4288,6 +4283,8 @@ const message = {
|
||||
protectionMode: 'ໂໝດປ້ອງກັນ',
|
||||
denySources: 'ປະຕິເສດແຫຼ່ງທີ່ລະບຸ',
|
||||
allowSources: 'ອະນຸຍາດສະເພາະແຫຼ່ງທີ່ລະບຸ',
|
||||
acceptSources: 'ອະນຸຍາດແຫຼ່ງທີ່ລະບຸໂດຍກົງ',
|
||||
acceptAll: 'ອະນຸຍາດການເຂົ້າເຖິງທັງໝົດໂດຍກົງ',
|
||||
denyAll: 'ປະຕິເສດການເຂົ້າເຖິງທັງໝົດ',
|
||||
dockerGuardMixedFamilyHelper:
|
||||
'ເລືອກທັງ IPv4 ແລະ IPv6 ແລ້ວ. ນະໂຍບາຍທີ່ລະບຸແຫຼ່ງທີ່ມາຕ້ອງຕັ້ງຄ່າແຍກກັນ; ການປະຕິເສດທັງໝົດສາມາດນຳໃຊ້ໄດ້ໂດຍກົງ.',
|
||||
@@ -4327,7 +4324,7 @@ const message = {
|
||||
'ຍົກເລີກການຜູກມັດ - ເມື່ອຍົກເລີກ, ກົດລະບຽບໄຟວໍທັງໝົດຈະບໍ່ມີຜົນ. ກະລຸນາເຮັດດ້ວຍຄວາມລະມັດລະວັງ. ຢືນຢັນ?',
|
||||
portWhiteList: 'ພອດທີ່ອະນຸຍາດ',
|
||||
whitelistConfigHelper:
|
||||
'ລາຍການອະນຸຍາດບັນທຶກສະເພາະການຕັ້ງຄ່າ ແລະ ປົກປ້ອງກົດອະນຸຍາດທີ່ກົງກັນ. ກົດທີ່ຂາດຈະຖືກເພີ່ມເມື່ອເລີ່ມ, ເລີ່ມໃໝ່, ຕັ້ງຄ່າເບື້ອງຕົ້ນ ຫຼື ຊິງຂໍ້ມູນ. ການແກ້ໄຂ ຫຼື ລຶບລາຍການອະນຸຍາດຈະບໍ່ລຶບກົດທີ່ມີຢູ່.',
|
||||
'ການບັນທຶກລາຍຊື່ອະນຸຍາດຈະເພີ່ມກົດອະນຸຍາດທີ່ຂາດ ແລະປ້ອງກັນກົດທີ່ກົງກັນ. ການແກ້ໄຂ ຫຼືລຶບລາຍການຈະບໍ່ລຶບກົດທີ່ມີຢູ່.',
|
||||
whitelistDeleteConfirm: 'ຈະລຶບສະເພາະລາຍການບັນຊີຂາວນີ້. ກົດໄຟວໍທີ່ມີຢູ່ຈະຖືກເກັບໄວ້. ສືບຕໍ່ບໍ?',
|
||||
portWhiteListHelper: 'ປ້ອນພອດດຽວ (ເຊັ່ນ 80) ຫຼື ຊ່ວງພອດ (ເຊັ່ນ 8000-8100). ໝາຍເລກພອດຕ້ອງຢູ່ລະຫວ່າງ 1 ຫາ 65535.',
|
||||
chain: 'Chain',
|
||||
@@ -4342,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:
|
||||
@@ -6987,11 +6983,17 @@ const message = {
|
||||
storagePoolHelper: 'ພູລຈັດເກັບໃຊ້ເກັບດິສກ໌ເຄື່ອງສະເໝືອນ ແລະ ໄຟລ໌ ISO.',
|
||||
network: 'ເຄືອຂ່າຍ',
|
||||
bridgeName: 'ຊື່ Bridge',
|
||||
natNetworkHelper: 'VM ສາມາດເຂົ້າເຄືອຂ່າຍຜ່ານ Host, ແຕ່ອຸປະກອນພາຍນອກໂດຍທົ່ວໄປບໍ່ສາມາດເຂົ້າ VM ໂດຍກົງໄດ້.',
|
||||
sourceInterface: 'ອິນເຕີເຟດເຄືອຂ່າຍຂອງໂຮສ',
|
||||
macvtapNetworkHelper:
|
||||
'VM ເຮັດວຽກຄືຄອມພິວເຕີອິດສະຫຼະໃນ LAN. ອຸປະກອນອື່ນສາມາດເຂົ້າເຖິງ IP ຂອງມັນໂດຍກົງ ໂດຍບໍ່ປ່ຽນຄ່າ IP ເດີມຂອງໂຮສ. ໝາຍເຫດ: ຕາມຄ່າເລີ່ມຕົ້ນ ໂຮສແລະ VM ບໍ່ສາມາດສື່ສານໂດຍກົງຜ່ານການເຊື່ອມຕໍ່ນີ້.',
|
||||
macvtapInterfaceHelper:
|
||||
'ເລືອກອິນເຕີເຟດມີສາຍທີ່ເຊື່ອມກັບ LAN ເປົ້າໝາຍ; ສາມາດໃຊ້ຮ່ວມກັບໂຮສໄດ້. LAN ຈະແຈກ IP ໃຫ້ VM ຫຼືສາມາດຕັ້ງເອງພາຍໃນ VM. ເຄືອຂ່າຍຕ້ອງອະນຸຍາດຫຼາຍອຸປະກອນຜ່ານອິນເຕີເຟດນີ້ (ຫຼາຍທີ່ຢູ່ MAC).',
|
||||
natNetworkHelper:
|
||||
'ເໝາະສຳລັບ VM ທີ່ຕ້ອງການໃຊ້ອິນເຕີເນັດ ຫຼືເປີດບໍລິການພຽງໜ້ອຍດຽວ. VM ອອກອິນເຕີເນັດຜ່ານໂຮສ. ອຸປະກອນອື່ນທີ່ຈະເຂົ້າໃຊ້ບໍລິການຂອງ VM ໂດຍທົ່ວໄປຕ້ອງຕັ້ງຄ່າສົ່ງຕໍ່ພອດແຍກຕ່າງຫາກ.',
|
||||
bridgeNetworkHelper:
|
||||
'ເຊື່ອມຕໍ່ VM ກັບ Bridge ທີ່ມີຢູ່ໃນ Host (ເຊັ່ນ br0). ຖ້າ Host ມີ NIC ເພີ່ມເຕີມທີ່ບໍ່ໄດ້ໃຊ້ ໃຫ້ສ້າງ Bridge ສຳລັບມັນແລ້ວນຳມາໃຊ້.',
|
||||
'VM ເຮັດວຽກຄືຄອມພິວເຕີອິດສະຫຼະທີ່ເຊື່ອມຕໍ່ກັບສະວິດເຄືອຂ່າຍ. ໂຮສແລະອຸປະກອນອື່ນໃນ LAN ສາມາດເຂົ້າເຖິງມັນໂດຍກົງ. ຕ້ອງຕັ້ງຄ່າບຣິດຈ໌ເທິງໂຮສກ່ອນ.',
|
||||
bridgeNameHelper:
|
||||
'ເລືອກ Linux Bridge ທີ່ມີຢູ່ໃນ Host ເຊັ່ນ br0. NIC ກາຍະພາບບໍ່ແມ່ນ Bridge ແລະ ບໍ່ສາມາດເລືອກ Bridge ທີ່ docker/libvirt ຈັດການໄດ້.',
|
||||
'ເລືອກບຣິດຈ໌ທີ່ຕັ້ງຄ່າແລ້ວ ເຊັ່ນ br0. ຖ້າບໍ່ມີຕົວເລືອກ ໃຫ້ສ້າງບຣິດຈ໌ເທິງໂຮສກ່ອນ. ບໍ່ສາມາດເລືອກອິນເຕີເຟດທາງກາຍະພາບ ຫຼືບຣິດຈ໌ທີ່ Docker/libvirt ຈັດການໄດ້.',
|
||||
natCIDRHelper: 'CIDR, ປະຕູເຄືອຂ່າຍ ແລະ ຊ່ວງ DHCP ຈະຖືກໃຊ້ໂດຍເຄືອຂ່າຍສະເໝືອນ NAT ໃໝ່.',
|
||||
networkCIDR: 'CIDR ເຄືອຂ່າຍ',
|
||||
gateway: 'ປະຕູເຄືອຂ່າຍ',
|
||||
@@ -7009,6 +7011,7 @@ const message = {
|
||||
available: 'ໃຊ້ໄດ້',
|
||||
vmCount: 'VM ທີ່ເຊື່ອມໂຍງ',
|
||||
usedIPs: 'IP ທີ່ໃຊ້ແລ້ວ',
|
||||
usedIPsUnavailable: 'ຍັງບໍ່ຮອງຮັບການດຶງຂໍ້ມູນ',
|
||||
monitor: 'ການຕິດຕາມ',
|
||||
monitorNotReady: 'ເຄື່ອງສະເໝືອນບໍ່ໄດ້ເຮັດວຽກ. ບໍ່ມີຂໍ້ມູນຕິດຕາມ.',
|
||||
monitorLoadFailed: 'ດຶງຂໍ້ມູນຕິດຕາມ VM ລົ້ມເຫຼວ.',
|
||||
|
||||
@@ -2930,6 +2930,9 @@ const message = {
|
||||
privateKey: 'Kunci Persendirian',
|
||||
publicKey: 'Kunci Awam',
|
||||
password: 'Kata Laluan',
|
||||
existingPassPhrase: 'Frasa laluan kunci peribadi sedia ada',
|
||||
existingPassPhraseHelper:
|
||||
'Masukkan frasa laluan asal jika kunci peribadi disulitkan; jika tidak, biarkan kosong. Ini tidak mengubah frasa laluan.',
|
||||
createMode: 'Kaedah Penciptaan',
|
||||
generate: 'Jana Automatik',
|
||||
unSyncPass: 'Kata laluan kunci tidak dapat diselaraskan',
|
||||
@@ -4311,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.',
|
||||
@@ -4378,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',
|
||||
@@ -4387,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',
|
||||
@@ -4405,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.',
|
||||
@@ -4427,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}?',
|
||||
@@ -4483,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',
|
||||
@@ -4493,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.',
|
||||
@@ -4535,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:
|
||||
@@ -4553,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:
|
||||
@@ -7341,12 +7333,17 @@ const message = {
|
||||
storagePoolHelper: 'Kolam storan menyimpan cakera mesin maya dan fail ISO.',
|
||||
network: 'Rangkaian',
|
||||
bridgeName: 'Nama Bridge',
|
||||
sourceInterface: 'Antara muka rangkaian hos',
|
||||
macvtapNetworkHelper:
|
||||
'VM bertindak seperti komputer berasingan dalam LAN. Peranti lain boleh mengakses IP-nya secara terus tanpa mengubah tetapan IP hos. Nota: secara lalai, hos dan VM tidak boleh berkomunikasi secara terus melalui sambungan ini.',
|
||||
macvtapInterfaceHelper:
|
||||
'Pilih antara muka berwayar yang bersambung ke LAN sasaran; ia boleh dikongsi dengan hos. LAN memberikan IP VM, atau anda boleh menetapkannya secara manual dalam VM. Rangkaian mesti membenarkan beberapa peranti pada antara muka ini (berbilang alamat MAC).',
|
||||
natNetworkHelper:
|
||||
'VM boleh mengakses rangkaian melalui hos, tetapi peranti luar biasanya tidak boleh mengaksesnya secara langsung.',
|
||||
'Sesuai untuk VM yang memerlukan akses internet atau menyediakan beberapa perkhidmatan sahaja. VM mengakses internet melalui hos. Peranti lain biasanya memerlukan pemajuan port yang dikonfigurasi secara berasingan untuk mengakses perkhidmatannya.',
|
||||
bridgeNetworkHelper:
|
||||
'Sambungkan VM ke bridge hos sedia ada (seperti br0). Jika hos mempunyai kad rangkaian tambahan yang tidak digunakan, anda boleh mencipta bridge untuknya dan menggunakannya.',
|
||||
'VM bertindak seperti komputer berasingan yang disambungkan ke suis rangkaian. Hos dan peranti LAN lain boleh mengaksesnya secara terus. Konfigurasikan bridge pada hos terlebih dahulu.',
|
||||
bridgeNameHelper:
|
||||
'Pilih Linux bridge sedia ada pada hos, seperti br0. NIC fizikal bukan bridge, dan bridge yang diurus docker/libvirt tidak boleh dipilih di sini.',
|
||||
'Pilih bridge yang telah dikonfigurasi, seperti br0. Jika tiada pilihan, cipta bridge pada hos terlebih dahulu. Antara muka fizikal dan bridge yang diurus oleh Docker/libvirt tidak boleh dipilih di sini.',
|
||||
natCIDRHelper: 'CIDR, get laluan dan julat DHCP digunakan oleh rangkaian maya NAT baharu.',
|
||||
networkCIDR: 'CIDR Rangkaian',
|
||||
gateway: 'Get Laluan',
|
||||
@@ -7364,6 +7361,7 @@ const message = {
|
||||
available: 'Tersedia',
|
||||
vmCount: 'VM Terpaut',
|
||||
usedIPs: 'IP Digunakan',
|
||||
usedIPsUnavailable: 'Pengambilan belum disokong',
|
||||
monitor: 'Pemantauan',
|
||||
monitorNotReady: 'Mesin maya tidak berjalan. Data pemantauan tidak tersedia.',
|
||||
monitorLoadFailed: 'Gagal mendapatkan data pemantauan VM.',
|
||||
|
||||
@@ -2929,6 +2929,9 @@ const message = {
|
||||
privateKey: 'Chave Privada',
|
||||
publicKey: 'Chave Pública',
|
||||
password: 'Senha',
|
||||
existingPassPhrase: 'Senha atual da chave privada',
|
||||
existingPassPhraseHelper:
|
||||
'Informe a senha original se a chave privada estiver criptografada; caso contrário, deixe em branco. Isso não altera a senha.',
|
||||
createMode: 'Método de Criação',
|
||||
generate: 'Gerar Automaticamente',
|
||||
unSyncPass: 'Senha da chave não pode ser sincronizada',
|
||||
@@ -4331,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.',
|
||||
@@ -4396,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',
|
||||
@@ -4405,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',
|
||||
@@ -4422,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.',
|
||||
@@ -4444,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}?',
|
||||
@@ -4502,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',
|
||||
@@ -4512,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.',
|
||||
@@ -4557,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:
|
||||
@@ -4575,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:
|
||||
@@ -7383,12 +7379,17 @@ const message = {
|
||||
storagePoolHelper: 'O pool de armazenamento guarda os discos das máquinas virtuais e os arquivos ISO.',
|
||||
network: 'Rede',
|
||||
bridgeName: 'Nome da Bridge',
|
||||
sourceInterface: 'Interface de rede do host',
|
||||
macvtapNetworkHelper:
|
||||
'A VM funciona como um computador independente na LAN. Outros dispositivos podem acessar seu IP diretamente, sem alterar as configurações de IP do host. Atenção: por padrão, o host e a VM não podem se comunicar diretamente por esta conexão.',
|
||||
macvtapInterfaceHelper:
|
||||
'Selecione uma interface cabeada conectada à LAN desejada; ela pode ser compartilhada com o host. A LAN atribui o IP da VM, ou você pode configurá-lo manualmente na VM. A rede deve permitir vários dispositivos nesta interface (vários endereços MAC).',
|
||||
natNetworkHelper:
|
||||
'As VMs podem acessar a rede pelo host, mas dispositivos externos geralmente não podem acessá-las diretamente.',
|
||||
'Indicado para VMs que precisam de acesso à internet ou oferecem poucos serviços. A VM acessa a internet pelo host. Outros dispositivos geralmente precisam de encaminhamento de portas configurado separadamente para acessar seus serviços.',
|
||||
bridgeNetworkHelper:
|
||||
'Conecte as VMs a uma bridge existente do host (como br0). Se o host tiver uma placa de rede adicional não utilizada, crie uma bridge para ela e use-a.',
|
||||
'A VM funciona como um computador independente conectado a um switch. Tanto o host quanto outros dispositivos da LAN podem acessá-la diretamente. Primeiro, configure uma bridge no host.',
|
||||
bridgeNameHelper:
|
||||
'Selecione uma Linux bridge existente no host, como br0. NICs físicas não são bridges, e bridges gerenciadas por docker/libvirt não podem ser selecionadas aqui.',
|
||||
'Selecione uma bridge configurada, como br0. Se não houver nenhuma, crie uma no host primeiro. Interfaces físicas e bridges gerenciadas pelo Docker/libvirt não podem ser selecionadas aqui.',
|
||||
natCIDRHelper: 'CIDR, gateway e intervalo DHCP são usados pela nova rede virtual NAT.',
|
||||
networkCIDR: 'CIDR da Rede',
|
||||
gateway: 'Gateway',
|
||||
@@ -7406,6 +7407,7 @@ const message = {
|
||||
available: 'Disponível',
|
||||
vmCount: 'VMs Vinculadas',
|
||||
usedIPs: 'IPs Usados',
|
||||
usedIPsUnavailable: 'Consulta ainda não disponível',
|
||||
monitor: 'Monitoramento',
|
||||
monitorNotReady: 'A máquina virtual não está em execução. Os dados de monitoramento não estão disponíveis.',
|
||||
monitorLoadFailed: 'Falha ao obter os dados de monitoramento da VM.',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user