Compare commits

..
Author SHA1 Message Date
Yuki Guoandssongliu 486cd5d0ad feat(redis): enable remote terminal through tracked background tasks (#14012)
* feat(redis): enable remote terminal through tracked background tasks

* feat(redis): enable remote terminal through tracked background tasks

* refactor(redis): remove redundant CLI status tracking

* refactor(redis): retain existing task execution flow

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 18:20:51 +08:00
ssongliu 00ce5a7b36 fix(firewall): preserve IPv6 RA protection and simplify error messages (#14015) 2026-10-09 17:45:57 +08:00
ssongliu c17f055e06 fix: preserve VM task timeout cleanup and terminate command groups (#14008) 2026-10-09 17:45:41 +08:00
王贺 f22df4b29c fix(file): stabilize closable tab width to avoid layout jitter (#14013)
The closable card tabs animated the close icon width between active and inactive tabs, so each tab switch reflowed the tab bar and caused the page to jitter. Reserve the close icon slot and toggle it with opacity instead, matching the terminal tab implementation.
2026-10-09 17:16:10 +08:00
Yuki Guoandssongliu b9bab90a14 fix: fix clean network (#14009)
* fix: fix clean network

* refactor(container): simplify network cleanup

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 17:14:50 +08:00
ssongliu ce14a104cc feat(vm): move shared VM support to agent (#14007) 2026-10-09 14:09:53 +08:00
CN_CoreStebandssongliu 2eaabccf94 fix(dashboard): prevent overlapping df collection and share disk queries (#13971)
* fix(dashboard): keep disk info working when a mount is stale or broken

The dashboard, the file manager mount list and the alert disk list
enumerated mounts with df. df calls statfs on every mount, so one bad
mount took the whole disk list down: the request hung until the
frontend timed out, or the list came back empty.

- enumerate mounts from /proc/self/mountinfo instead of running df
- read each mount through a guard with a 3s timeout that keeps at most
  one call per mount in flight
- fail closed: a mount that cannot be read stays in the list with
  errorType and error, and an unreadable mount table is an error
  rather than an empty list
- show failed mounts and their reasons on the dashboard and in the
  file manager
- cmd: after killing a timed-out command, wait at most 5s for it
  instead of forever

* fix(dashboard): distinguish remounted and hidden filesystems

* fix(disk): simplify disk queries and bound timeout handling

* fix(disk): prevent overlapping df collection tasks

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 10:14:16 +08:00
ssongliu e119239d3d refactor: manage firewall rules through runtime state and file backups (#13998)
Replace database rule synchronization with native rule operations and file-based backup, import, and recovery. Update firewall management views, descriptions, whitelist handling, and translations.

Include terminal reconnect fixes and xpack build tags for Darwin build targets. Remove temporary regression test files before committing.
2026-10-08 18:19:30 +08:00
ssongliu 14a57af4e9 feat(vm): clarify network modes and add macvtap translations (#13996) 2026-10-08 18:18:17 +08:00
156 changed files with 9426 additions and 12145 deletions
+3 -18
View File
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
return return
} }
total, list, err := containerService.Page(c.Request.Context(), req) total, list, err := containerService.Page(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -249,10 +249,9 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
// @Success 200 {object} dto.ContainerStatus // @Success 200 {object} dto.ContainerStatus
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param containersOnly query boolean false "Only count containers"
// @Router /containers/status [get] // @Router /containers/status [get]
func (b *BaseApi) LoadContainerStatus(c *gin.Context) { func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true") data, err := containerService.LoadStatus()
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -416,14 +415,9 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
// @Success 200 {array} dto.ContainerListStats // @Success 200 {array} dto.ContainerListStats
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
// @Router /containers/list/stats [get] // @Router /containers/list/stats [get]
func (b *BaseApi) ContainerListStats(c *gin.Context) { func (b *BaseApi) ContainerListStats(c *gin.Context) {
var ids []string data, err := containerService.ContainerListStats()
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
}
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -970,12 +964,3 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
containerService.StreamLogs(c, streamLog) containerService.StreamLogs(c, streamLog)
} }
func (b *BaseApi) CleanNetworks(c *gin.Context) {
result, err := containerService.CleanNetworks()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
+10 -4
View File
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
// @Tags Database Redis // @Tags Database Redis
// @Summary Install redis-cli // @Summary Install redis-cli
// @Success 200 // @Accept json
// @Param request body dto.RedisCliInstall true "request"
// @Success 200 {string} string
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/redis/install/cli [post] // @Router /databases/redis/install/cli [post]
func (b *BaseApi) InstallCli(c *gin.Context) { func (b *BaseApi) InstallCli(c *gin.Context) {
if err := redisService.InstallCli(); err != nil { var req dto.RedisCliInstall
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := redisService.InstallCli(req)
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, data)
helper.Success(c)
} }
// @Tags Database Redis // @Tags Database Redis
+79 -138
View File
@@ -4,11 +4,9 @@ import (
"errors" "errors"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"net/http" "net/http"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "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/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
@@ -90,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/base [post] // @Router /hosts/firewall/forward/base [post]
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) { func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
data, err := forwardingService.LoadBaseInfo() data, err := forwardingService.LoadBaseInfo(c.Request.Context())
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -111,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
total, items, err := forwardingService.SearchRules(request) total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -262,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
helper.SuccessWithData(c, info) 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 // @Tags Firewall
// @Summary Queue firewall rule creation // @Summary Queue firewall rule creation
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log. // @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) 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 // @Tags Firewall
// @Summary Queue firewall rule deletion // @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 // @Accept json
// @Param request body dto.FirewallRuleDelete true "request" // @Param request body dto.FirewallRuleDelete true "request"
// @Success 200 {object} dto.FirewallRuleDeleteResponse // @Success 200 {object} dto.FirewallRuleDeleteResponse
@@ -394,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
} }
// @Tags Firewall // @Tags Firewall
// @Summary Update a managed unified firewall v2 rule // @Summary Update a firewall rule
// @Accept json // @Accept json
// @Param request body dto.FirewallRuleUpdate true "request" // @Param request body dto.FirewallRuleUpdate true "request"
// @Success 200 // @Success 200
@@ -402,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/rules/update [post] // @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) { func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
var request dto.FirewallRuleUpdate var request dto.FirewallRuleUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
if !normalizeFirewallRuleUUID(c, &request.UUID) { if err := firewallService.Update(c.Request.Context(), request); err != nil {
return
}
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
handleFirewallRuleError(c, err) handleFirewallRuleError(c, err)
return return
} }
@@ -419,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
} }
// @Tags Firewall // @Tags Firewall
// @Summary Reorder a managed unified firewall v2 rule // @Summary Reorder a firewall rule
// @Accept json // @Accept json
// @Param request body dto.FirewallRuleReorder true "request" // @Param request body dto.FirewallRuleReorder true "request"
// @Success 200 // @Success 200
@@ -427,35 +340,19 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/rules/reorder [post] // @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) { func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
var request dto.FirewallRuleReorder var request dto.FirewallRuleReorder
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
if !normalizeFirewallRuleUUID(c, &request.UUID) { if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
return
}
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
handleFirewallRuleError(c, err) handleFirewallRuleError(c, err)
return return
} }
helper.Success(c) 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) { func handleFirewallRuleError(c *gin.Context, err error) {
var businessErr buserr.BusinessError var businessErr buserr.BusinessError
isBusinessError := errors.As(err, &businessErr) 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) helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrRuleStale): case errors.Is(err, filter.ErrRuleStale):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err) 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), case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable): errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err) 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), case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err) 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": case isBusinessError && businessErr.Msg == "ErrInvalidParams":
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()}) c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
c.Abort() c.Abort()
case errors.Is(err, filter.ErrVerificationFailed):
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
default: default:
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err) helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
} }
@@ -501,7 +397,7 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
// @Tags Firewall // @Tags Firewall
// @Summary Create firewall port whitelist rules // @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 // @Accept json
// @Param request body dto.FirewallPortWhitelistCreate true "request" // @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200 // @Success 200
@@ -523,7 +419,7 @@ func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
// @Tags Firewall // @Tags Firewall
// @Summary Update firewall port whitelist rules // @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 // @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request" // @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200 // @Success 200
@@ -545,7 +441,7 @@ func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
// @Tags Firewall // @Tags Firewall
// @Summary Delete firewall port whitelist rules // @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 // @Accept json
// @Param request body dto.FirewallPortWhitelistDelete true "request" // @Param request body dto.FirewallPortWhitelistDelete true "request"
// @Success 200 // @Success 200
@@ -622,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
helper.SuccessWithData(c, data) 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 // @Tags Firewall
// @Summary Operate Docker port guard // @Summary Operate Docker port guard
// @Accept json // @Accept json
@@ -737,3 +618,63 @@ func handleDockerPortGuardError(c *gin.Context, err error) {
} }
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err) 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)
}
@@ -1,17 +0,0 @@
package dto
type NetworkCleanupReport struct {
Deleted []NetworkCleanupItem `json:"deleted"`
Skipped []NetworkCleanupItem `json:"skipped"`
Failed []NetworkCleanupItem `json:"failed"`
}
type NetworkCleanupItem struct {
ID string `json:"id"`
Name string `json:"name"`
Reason string `json:"reason,omitempty"`
}
type NetworkCleanupTask struct {
TaskID string `json:"taskID"`
}
+4
View File
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
Port int64 `json:"port"` Port int64 `json:"port"`
} }
type RedisCliInstall struct {
TaskID string `json:"taskID" validate:"omitempty,uuid"`
}
// mysql // mysql
type MysqlDBSearch struct { type MysqlDBSearch struct {
PageInfo PageInfo
+91 -127
View File
@@ -2,11 +2,13 @@ package dto
import ( import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "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/filter"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
) )
type FirewallSubsystemStatus struct { type FirewallSubsystemStatus struct {
IPv6Enabled bool `json:"ipv6Enabled"`
Name string `json:"name"` Name string `json:"name"`
Backend string `json:"backend"` Backend string `json:"backend"`
ConflictBackend string `json:"conflictBackend,omitempty"` ConflictBackend string `json:"conflictBackend,omitempty"`
@@ -18,7 +20,6 @@ type FirewallSubsystemStatus struct {
PingStatus string `json:"pingStatus"` PingStatus string `json:"pingStatus"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"`
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"` LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"` IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"` IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
@@ -49,12 +50,12 @@ type FirewallBackendOption struct {
} }
type FirewallBackendFamilyStatus struct { type FirewallBackendFamilyStatus struct {
Available bool `json:"available"` Partial bool `json:"partial"`
Initialized bool `json:"initialized"` Available bool `json:"available"`
Bound bool `json:"bound"` Initialized bool `json:"initialized"`
Reason string `json:"reason,omitempty"` Bound bool `json:"bound"`
ForwardPolicy string `json:"forwardPolicy,omitempty"` Reason string `json:"reason,omitempty"`
RAInterfaces []string `json:"raInterfaces,omitempty"` RAInterfaces []string `json:"raInterfaces,omitempty"`
} }
type FirewallBackendGroup struct { type FirewallBackendGroup struct {
@@ -64,6 +65,7 @@ type FirewallBackendGroup struct {
} }
type FirewallSettings struct { type FirewallSettings struct {
IPv6Enabled bool `json:"ipv6Enabled"`
System FirewallBackendGroup `json:"system"` System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"` Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"` Docker FirewallBackendGroup `json:"docker"`
@@ -92,6 +94,17 @@ type FirewallBackendOperation struct {
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"` 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 { type FilterChainOperation struct {
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"` Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"` Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
@@ -104,42 +117,55 @@ type FilterChainOperationResponse struct {
} }
type FirewallInitializationTask 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 FirewallSystemPort = firewall.SystemPort
type FirewallRuleInventoryResponse struct { type FirewallRuleInventoryResponse struct {
IPv4Range filter.PositionRange `json:"ipv4Range"` IPv4Range filter.PositionRange `json:"ipv4Range"`
IPv6Range filter.PositionRange `json:"ipv6Range"` IPv6Range filter.PositionRange `json:"ipv6Range"`
Total int64 `json:"total"` Total int64 `json:"total"`
AllTotal int64 `json:"allTotal"` AllTotal int64 `json:"allTotal"`
ManagedTotal int64 `json:"managedTotal"` Items []filter.InventoryItem `json:"items"`
Items []filter.InventoryItem `json:"items"` Notices []filter.ScopeNotice `json:"notices,omitempty"`
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 { type FirewallRuleResetResponse struct {
Removed int `json:"removed"` BackupPath string `json:"backupPath"`
Disabled bool `json:"disabled"` Removed int `json:"removed"`
Disabled bool `json:"disabled"`
} }
type FirewallRuleReset struct { 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"` Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
WithDockerRestart bool `json:"withDockerRestart"` WithDockerRestart bool `json:"withDockerRestart"`
} }
type FirewallRuleInventory struct { type FirewallRuleInventory struct {
Refresh bool `json:"refresh,omitempty"`
PageInfo PageInfo
Scope filter.Scope `json:"scope,omitempty"` Scope filter.Scope `json:"scope,omitempty"`
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"` Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
All bool `json:"all,omitempty"` All bool `json:"all,omitempty"`
Info string `json:"info"` Info string `json:"info"`
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"` Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"` 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"`
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
} }
type FirewallNativeDetail struct { type FirewallNativeDetail struct {
@@ -150,6 +176,7 @@ type FirewallNativeDetail struct {
} }
type DockerPortGuardBase struct { type DockerPortGuardBase struct {
IPv6Enabled bool `json:"ipv6Enabled"`
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
IsExist bool `json:"isExist"` IsExist bool `json:"isExist"`
@@ -162,6 +189,7 @@ type DockerPortGuardBase struct {
} }
type DockerPortGuardFamilyStatus struct { type DockerPortGuardFamilyStatus struct {
Partial bool `json:"partial"`
State string `json:"state"` State string `json:"state"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
Initialized bool `json:"initialized"` Initialized bool `json:"initialized"`
@@ -182,11 +210,8 @@ type DockerPortGuardEndpoint struct {
Application string `json:"application,omitempty"` Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"` PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"` Mode string `json:"mode,omitempty"`
NativeAction string `json:"nativeAction,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Sources []string `json:"sources"` Sources []string `json:"sources"`
Effective bool `json:"effective"` Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
TrafficPath string `json:"trafficPath"` TrafficPath string `json:"trafficPath"`
ManagementTarget string `json:"managementTarget"` ManagementTarget string `json:"managementTarget"`
ManagementReason string `json:"managementReason,omitempty"` ManagementReason string `json:"managementReason,omitempty"`
@@ -223,6 +248,7 @@ type DockerPortGuardEndpointIdentity struct {
type DockerPortGuardPolicyBatch struct { type DockerPortGuardPolicyBatch struct {
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"` Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
Import bool `json:"import"`
} }
type DockerPortGuardPolicyBatchDelete struct { type DockerPortGuardPolicyBatchDelete struct {
@@ -231,31 +257,27 @@ type DockerPortGuardPolicyBatchDelete struct {
type DockerPortGuardPolicy struct { type DockerPortGuardPolicy struct {
DockerPortGuardEndpointIdentity DockerPortGuardEndpointIdentity
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"` 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"` Sources []string `json:"sources" validate:"dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
} }
type DockerPortGuardOperation struct { type DockerPortGuardOperation struct {
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"` BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` 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"`
} }
type FirewallRuleCreateItem struct { type FirewallRuleCreateItem struct {
Rule filter.FirewallRule `json:"rule" validate:"required"` Raw string `json:"raw,omitempty"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"` ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
SourceID string `json:"sourceID"` Rule filter.FirewallRule `json:"rule" validate:"required"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
} }
type FirewallRuleCreate struct { 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 { type FirewallRuleCreateResponse struct {
@@ -274,66 +296,13 @@ type FirewallRuleCreateFailure struct {
Error string `json:"error,omitempty"` 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 { type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"` Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"` }
type FirewallRuleDeleteItem struct {
FirewallRuleDeleteTarget
Observed filter.ObservedRule `json:"observed" validate:"required"`
} }
type FirewallRuleDeleteTarget struct { type FirewallRuleDeleteTarget struct {
@@ -350,13 +319,13 @@ type FirewallRuleDeleteResponse struct {
} }
type FirewallRuleDeleteFailure struct { type FirewallRuleDeleteFailure struct {
Index int `json:"index"` Index int `json:"index"`
UUID string `json:"uuid"` InstanceKey string `json:"instanceKey"`
Error string `json:"error"` Error string `json:"error"`
} }
type FirewallRuleUpdate struct { 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"` 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"` Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"` OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
@@ -364,26 +333,21 @@ type FirewallRuleUpdate struct {
} }
type FirewallRuleReorder struct { type FirewallRuleReorder struct {
UUID string `json:"uuid" validate:"required,max=64"` FirewallRuleDeleteTarget
TargetPosition *int64 `json:"targetPosition"` TargetPosition *int64 `json:"targetPosition"`
Priority *int `json:"priority"` Priority *int `json:"priority"`
} }
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) { type FirewallRuleExportItem struct {
p.Items = append(p.Items, item) filter.FirewallRule
switch item.Status { Raw string `json:"raw,omitempty"`
case firewallsync.StatusReady: ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
p.Ready++ }
p.Total++
case firewallsync.StatusExisting: type FirewallSubsystemBackup struct {
p.Existing++ Families []string `json:"families,omitempty"`
p.Total++ Subsystem string `json:"subsystem"`
case firewallsync.StatusRemove: Provider filter.Provider `json:"provider"`
p.Removed++ Forwarding []forwarding.Rule `json:"forwarding"`
case firewallsync.StatusBlocked: Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
p.Blocked++
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
p.Total++
}
}
} }
+2 -6
View File
@@ -24,15 +24,11 @@ type ForwardRule struct {
UsedStatus string `json:"usedStatus"` UsedStatus string `json:"usedStatus"`
Description string `json:"description"` Description string `json:"description"`
IsDesired bool `json:"isDesired"`
IsRuntime bool `json:"isRuntime"`
SyncStatus string `json:"syncStatus"`
} }
type ForwardRuleOperate struct { type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"` Import bool `json:"import"`
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"` Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
} }
type ForwardRuleOperation struct { type ForwardRuleOperation struct {
-51
View File
@@ -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"`
}
-50
View File
@@ -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
})
}
-187
View File
@@ -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
}
-38
View File
@@ -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
}
+53
View File
@@ -1,11 +1,13 @@
package repo package repo
import ( import (
"context"
"errors" "errors"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type SettingRepo struct{} type SettingRepo struct{}
@@ -19,12 +21,16 @@ type ISettingRepo interface {
WithByKey(key string) DBOption WithByKey(key string) DBOption
UpdateOrCreate(key, value string) error UpdateOrCreate(key, value string) error
UpdateValues(map[string]string) error
GetDescription(opts ...DBOption) (model.CommonDescription, error) GetDescription(opts ...DBOption) (model.CommonDescription, error)
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
CreateDescription(data *model.CommonDescription) error CreateDescription(data *model.CommonDescription) error
SaveDescriptions(context.Context, []model.CommonDescription) error
UpdateDescription(id string, val map[string]interface{}) error UpdateDescription(id string, val map[string]interface{}) error
DelDescription(id string) error DelDescription(id string) error
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
WithDescriptionIDs(ids []string) DBOption
WithByDescriptionID(id 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 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) { func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
var lists []model.CommonDescription var lists []model.CommonDescription
db := global.DB.Model(&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 { func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
return global.DB.Create(data).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 { func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
} }
func (s *SettingRepo) DelDescription(id string) error { func (s *SettingRepo) DelDescription(id string) error {
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).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 { func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
return g.Where("id = ?", id) 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
View File
@@ -4,11 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"mime" "mime"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
@@ -20,12 +17,10 @@ import (
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config" alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook" 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/copier"
"github.com/1Panel-dev/1Panel/agent/utils/email" "github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers" "github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
"github.com/shirou/gopsutil/v4/disk"
) )
type AlertService struct{} type AlertService struct{}
@@ -358,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
} }
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) { func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
var disks []dto.DiskDTO infos := loadDiskInfo(true)
excludes := map[string]struct{}{ disks := make([]dto.DiskDTO, 0, len(infos))
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {}, for _, item := range infos {
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {}, 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 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) { func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
var ( var (
opts []repo.DBOption opts []repo.DBOption
+7 -20
View File
@@ -1066,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
global.LOG.Errorf("error getting disk list, err: %v", err) global.LOG.Errorf("error getting disk list, err: %v", err)
return err return err
} }
var errMsgs []string
for _, item := range diskList { for _, item := range diskList {
err := checkAndCreateDiskAlert(alert, item.Path) if item.Total == 0 {
if err != nil {
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
errMsgs = append(errMsgs, errMsg)
global.LOG.Errorf("%s", errMsg)
continue continue
} }
} checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
if len(errMsgs) > 0 {
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
} }
return nil return nil
} }
func processSingleDisk(alert dto.AlertDTO) error { func processSingleDisk(alert dto.AlertDTO) error {
err := checkAndCreateDiskAlert(alert, alert.Project) usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
if err != nil { 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 return err
} }
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
return nil return nil
} }
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error { func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
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
}
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat) usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
commonTotal := float64(alert.Count) commonTotal := float64(alert.Count)
if alert.Cycle == 1 { if alert.Cycle == 1 {
commonTotal *= 1024 * 1024 * 1024 commonTotal *= 1024 * 1024 * 1024
} }
if usedTotal < commonTotal { if usedTotal < commonTotal {
return nil return
} }
params := createAlertDiskParams(path, usedStr) params := createAlertDiskParams(path, usedStr)
sender := NewAlertSender(alert, alert.Project) sender := NewAlertSender(alert, alert.Project)
sender.ResourceSend(path, params) sender.ResourceSend(path, params)
return nil
} }
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) { func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
+42 -84
View File
@@ -58,10 +58,10 @@ type ContainerService struct{}
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>") var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
type IContainerService interface { type IContainerService interface {
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) Page(req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions List() []dto.ContainerOptions
ListByImage(imageName string) []dto.ContainerOptions ListByImage(imageName string) []dto.ContainerOptions
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) LoadStatus() (dto.ContainerStatus, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error) ListNetwork() ([]dto.Options, error)
PageVolume(req dto.SearchWithPage) (int64, interface{}, error) PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
@@ -80,7 +80,7 @@ type IContainerService interface {
ContainerUpdate(req dto.ContainerOperate) error ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) ContainerListStats() ([]dto.ContainerListStats, error)
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error) ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
LoadResourceLimit() (*dto.ResourceLimit, error) LoadResourceLimit() (*dto.ResourceLimit, error)
ContainerRename(req dto.ContainerRename) error ContainerRename(req dto.ContainerRename) error
@@ -92,7 +92,6 @@ type IContainerService interface {
Inspect(req dto.InspectReq) (string, error) Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error DeleteNetwork(req dto.BatchDelete) error
CleanNetworks() (*dto.NetworkCleanupTask, error)
CreateNetwork(req dto.NetworkCreate) error CreateNetwork(req dto.NetworkCreate) error
DeleteVolume(req dto.BatchDelete) error DeleteVolume(req dto.BatchDelete) error
CreateVolume(req dto.VolumeCreate) error CreateVolume(req dto.VolumeCreate) error
@@ -113,7 +112,7 @@ func NewIContainerService() IContainerService {
return &ContainerService{} return &ContainerService{}
} }
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) { func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
@@ -124,7 +123,7 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
options.Filters = filters.NewArgs() options.Filters = filters.NewArgs()
options.Filters.Add("label", req.Filters) options.Filters.Add("label", req.Filters)
} }
containers, err := client.ContainerList(ctx, options) containers, err := client.ContainerList(context.Background(), options)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
@@ -207,30 +206,27 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
return options return options
} }
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) { func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
var data dto.ContainerStatus var data dto.ContainerStatus
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return data, err return data, err
} }
defer client.Close() defer client.Close()
if !containersOnly { c := context.Background()
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
data.ImageCount = len(images) images, _ := client.ImageList(c, image.ListOptions{All: true})
repo, _ := imageRepoRepo.List() data.ImageCount = len(images)
data.RepoCount = len(repo) repo, _ := imageRepoRepo.List()
templates, _ := composeRepo.List() data.RepoCount = len(repo)
data.ComposeTemplateCount = len(templates) templates, _ := composeRepo.List()
networks, _ := client.NetworkList(ctx, network.ListOptions{}) data.ComposeTemplateCount = len(templates)
data.NetworkCount = len(networks) networks, _ := client.NetworkList(c, network.ListOptions{})
volumes, _ := client.VolumeList(ctx, volume.ListOptions{}) data.NetworkCount = len(networks)
data.VolumeCount = len(volumes.Volumes) volumes, _ := client.VolumeList(c, volume.ListOptions{})
data.ComposeCount = loadComposeCount(ctx, client) data.VolumeCount = len(volumes.Volumes)
} data.ComposeCount = loadComposeCount(client)
containers, err := client.ContainerList(ctx, container.ListOptions{All: true}) containers, _ := client.ContainerList(c, container.ListOptions{All: true})
if err != nil {
return data, err
}
data.ContainerCount = len(containers) data.ContainerCount = len(containers)
for _, item := range containers { for _, item := range containers {
switch item.State { switch item.State {
@@ -296,65 +292,27 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
} }
return data, nil return data, nil
} }
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) { func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer client.Close() defer client.Close()
options := container.ListOptions{All: true} list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
if ids != nil {
if len(ids) == 0 {
return []dto.ContainerListStats{}, nil
}
options.Filters = filters.NewArgs()
for _, id := range ids {
options.Filters.Add("id", id)
}
}
list, err := client.ContainerList(ctx, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats {
return loadCpuAndMem(ctx, client, id)
}), nil
}
// A fixed worker pool bounds Docker stats requests, including for legacy callers
// that request all containers. Stopped containers do not need a stats sample.
func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats {
datas := make([]dto.ContainerListStats, len(list)) datas := make([]dto.ContainerListStats, len(list))
for i, item := range list {
datas[i].ContainerID = item.ID
}
jobs := make(chan int)
var wg sync.WaitGroup var wg sync.WaitGroup
for worker := 0; worker < min(8, len(list)); worker++ { wg.Add(len(list))
wg.Add(1) for i := 0; i < len(list); i++ {
go func() { go func(index int, item container.Summary) {
defer wg.Done() datas[index] = loadCpuAndMem(client, item.ID)
for index := range jobs { wg.Done()
if ctx.Err() != nil || list[index].State != "running" { }(i, list[i])
continue
}
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
datas[index] = load(sampleCtx, list[index].ID)
cancel()
}
}()
} }
dispatch:
for index := range list {
select {
case <-ctx.Done():
break dispatch
case jobs <- index:
}
}
close(jobs)
wg.Wait() wg.Wait()
return datas return datas, nil
} }
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
@@ -425,11 +383,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
} }
func (u *ContainerService) Prune(req dto.ContainerPrune) error { func (u *ContainerService) Prune(req dto.ContainerPrune) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
name := "" name := ""
switch req.PruneType { switch req.PruneType {
case "container": case "container":
@@ -450,6 +403,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
} }
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
if err := t.TaskCtx.Err(); err != nil {
return err
}
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
pruneFilters := filters.NewArgs() pruneFilters := filters.NewArgs()
if req.WithTagAll { if req.WithTagAll {
pruneFilters.Add("dangling", "false") pruneFilters.Add("dangling", "false")
@@ -473,10 +434,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
} }
SpaceReclaimed = int(rep.SpaceReclaimed) SpaceReclaimed = int(rep.SpaceReclaimed)
case "network": case "network":
_, err := client.NetworksPrune(context.Background(), pruneFilters) return cleanUnusedNetworks(t, client)
if err != nil {
return err
}
case "volume": case "volume":
versions, err := client.ServerVersion(context.Background()) versions, err := client.ServerVersion(context.Background())
if err != nil { if err != nil {
@@ -1757,11 +1715,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
return selected return selected
} }
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats { func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
data := dto.ContainerListStats{ data := dto.ContainerListStats{
ContainerID: containerItem, ContainerID: containerItem,
} }
res, err := client.ContainerStats(ctx, containerItem, false) res, err := client.ContainerStats(context.Background(), containerItem, false)
if err != nil { if err != nil {
return data return data
} }
@@ -1984,11 +1942,11 @@ func transPortToStr(ports []container.Port) []string {
return docker.SimplifyPorts(ports) return docker.SimplifyPorts(ports)
} }
func loadComposeCount(ctx context.Context, client *client.Client) int { func loadComposeCount(client *client.Client) int {
options := container.ListOptions{All: true} options := container.ListOptions{All: true}
options.Filters = filters.NewArgs() options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel) options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(ctx, options) list, err := client.ContainerList(context.Background(), options)
if err != nil { if err != nil {
return 0 return 0
} }
+119
View File
@@ -2,16 +2,24 @@ package service
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
) )
var networkCleanupSlot = make(chan struct{}, 1)
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) { func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
} }
return nil return nil
} }
func cleanUnusedNetworks(t *task.Task, cli *client.Client) error {
ctx := t.TaskCtx
select {
case networkCleanupSlot <- struct{}{}:
defer func() { <-networkCleanupSlot }()
case <-ctx.Done():
return ctx.Err()
}
if err := ctx.Err(); err != nil {
return err
}
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
deleted, skipped, failed := 0, 0, 0
defer func() {
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
"deleted": deleted, "skipped": skipped, "failed": failed,
}))
}()
var used map[string]bool
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
for _, n := range networks {
if err := ctx.Err(); err != nil {
return err
}
switch n.Name {
case "none", "host", "bridge", "1panel-network":
skipped++
continue
}
if n.Scope != "local" || n.Ingress || n.ConfigOnly {
skipped++
continue
}
values := map[string]interface{}{"name": n.Name, "id": n.ID}
if used == nil {
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
used = make(map[string]bool)
for _, c := range containers {
if c.NetworkSettings == nil {
continue
}
for name, endpoint := range c.NetworkSettings.Networks {
used[name] = true
if endpoint != nil {
used[endpoint.NetworkID] = true
}
}
}
}
if used[n.Name] || used[n.ID] {
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
continue
}
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
if errdefs.IsNotFound(err) {
skipped++
} else {
failed++
t.Logf("Failed to inspect network [%s] (%s): %v", n.Name, n.ID, err)
}
continue
}
if len(inspected.Containers) > 0 {
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
continue
}
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
switch {
case errdefs.IsNotFound(err):
skipped++
case errdefs.IsConflict(err):
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
default:
failed++
t.Logf("Failed to remove network [%s] (%s): %v", n.Name, n.ID, err)
}
continue
}
deleted++
}
if err := ctx.Err(); err != nil {
return err
}
if failed > 0 {
return errors.New(i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
}
return nil
}
@@ -1,71 +0,0 @@
package service
import (
"fmt"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
)
var networkCleanupMu sync.Mutex
func (u *ContainerService) CleanNetworks() (*dto.NetworkCleanupTask, error) {
taskItem, err := task.NewTaskWithOps(i18n.GetMsgByKey("Network"), task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
return nil, err
}
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
networkCleanupMu.Lock()
defer networkCleanupMu.Unlock()
if err := t.TaskCtx.Err(); err != nil {
return err
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
return executeNetworkCleanup(t, cli)
}, nil)
go func() {
if err := taskItem.Execute(); err != nil {
global.LOG.Errorf("network cleanup task %s failed: %v", taskItem.TaskID, err)
}
}()
return &dto.NetworkCleanupTask{TaskID: taskItem.TaskID}, nil
}
func executeNetworkCleanup(t *task.Task, cli docker.NetworkCleanupClient) error {
t.Log(i18n.GetMsgByKey("PruneStart"))
report, err := docker.CleanUnusedNetworks(t.TaskCtx, cli, func(status string, item dto.NetworkCleanupItem) {
key := "NetworkCleanupDeleted"
if status == "skipped" || status == "failed" {
key = map[string]string{
"protected": "NetworkCleanupProtected",
"container_connected": "NetworkCleanupConnected",
"network_in_use": "NetworkCleanupConnected",
"unsupported_network": "NetworkCleanupUnsupported",
"already_removed": "NetworkCleanupGone",
"inspect_failed": "NetworkCleanupInspectFailed",
"remove_failed": "NetworkCleanupRemoveFailed",
}[item.Reason]
}
t.Log(i18n.GetMsgWithMap(key, map[string]interface{}{"name": item.Name, "id": item.ID}))
})
if report != nil {
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
"deleted": len(report.Deleted), "skipped": len(report.Skipped), "failed": len(report.Failed),
}))
}
if err != nil {
return err
}
if len(report.Failed) > 0 {
return fmt.Errorf("%s", i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
}
return nil
}
@@ -1,113 +0,0 @@
package service
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/model"
"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"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type networkTaskClient struct{ fail bool }
func (f networkTaskClient) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return []network.Inspect{{ID: "reserved", Name: "1panel-network", Scope: "local"}, {ID: "connected", Name: "busy", Scope: "local"}, {ID: "unused", Name: "free", Scope: "local"}, {ID: "race", Name: "race", Scope: "local"}}, nil
}
func (f networkTaskClient) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
return nil, nil
}
func (f networkTaskClient) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
n := network.Inspect{}
if id == "connected" {
n.Containers = map[string]network.EndpointResource{"container-id": {}}
}
return n, nil
}
func (f networkTaskClient) NetworkRemove(_ context.Context, id string) error {
if id == "race" {
return errdefs.Conflict(errors.New("has active endpoints"))
}
if id != "unused" {
return errors.New("unexpected removal")
}
if f.fail {
return errors.New("remove failed")
}
return nil
}
func TestNetworkCleanupTaskPersistsLogsAndStatus(t *testing.T) {
oldDB, oldTaskDB, oldDir, oldI18n := global.DB, global.TaskDB, global.Dir, global.I18n
t.Cleanup(func() { global.DB = oldDB; global.TaskDB = oldTaskDB; global.Dir = oldDir; global.I18n = oldI18n })
dir := t.TempDir()
db, err := gorm.Open(sqlite.Open(filepath.Join(dir, "tasks.db")), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
defer sqlDB.Close()
if err := db.AutoMigrate(&model.Task{}); err != nil {
t.Fatal(err)
}
global.DB = nil
global.TaskDB = db
global.Dir.TaskDir = dir
i18n.Init()
for _, fail := range []bool{false, true} {
name := "success"
wantStatus := constant.StatusSuccess
if fail {
name = "partial failure"
wantStatus = constant.StatusFailed
}
t.Run(name, func(t *testing.T) {
item, err := task.NewTaskWithOps("Network", task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
t.Fatal(err)
}
item.AddSubTask("Clean", func(t *task.Task) error { return executeNetworkCleanup(t, networkTaskClient{fail: fail}) }, nil)
err = item.Execute()
if (err != nil) != fail {
t.Fatalf("unexpected execution error: %v", err)
}
var saved model.Task
if err := db.First(&saved, "id = ?", item.TaskID).Error; err != nil {
t.Fatal(err)
}
if saved.Status != wantStatus {
t.Fatalf("status %s, want %s", saved.Status, wantStatus)
}
content, err := os.ReadFile(saved.LogFile)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"[busy] (connected): containers connected", "[race] (race): containers connected", "[1panel-network] (reserved): reserved network", "Network cleanup finished:", "[TASK-END]"} {
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q in log: %s", want, content)
}
}
want := "Deleted network [free]"
if fail {
want = "Failed to remove network [free]"
}
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q", want)
}
})
}
}
+81 -56
View File
@@ -242,7 +242,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
currentInfo.SwapMemoryUsed = swapInfo.Used currentInfo.SwapMemoryUsed = swapInfo.Used
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
currentInfo.DiskData = loadDiskInfo() currentInfo.DiskData = loadDiskInfo(false)
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo() currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
if ioOption == "all" { if ioOption == "all" {
@@ -455,26 +455,9 @@ type diskInfo struct {
Device string Device string
} }
func loadDiskInfo() []dto.DiskInfo { func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
var datas []dto.DiskInfo var datas []dto.DiskInfo
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second)) stdout := loadDiskMounts()
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
}
}
lines := strings.Split(stdout, "\n") lines := strings.Split(stdout, "\n")
var mounts []diskInfo var mounts []diskInfo
@@ -522,43 +505,22 @@ func loadDiskInfo() []dto.DiskInfo {
itemData.Type = mount.Type itemData.Type = mount.Type
itemData.Device = mount.Device itemData.Device = mount.Device
type diskResult struct { state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
state *disk.UsageStat if err != nil {
err error global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
} } else {
resultCh := make(chan diskResult, 1) itemData.Total = state.Total
itemData.Free = state.Free
go func() { itemData.Used = state.Used
state, err := psutil.DISK.GetUsage(mount.Mount, false) itemData.UsedPercent = state.UsedPercent
resultCh <- diskResult{state: state, err: err} itemData.InodesTotal = state.InodesTotal
}() itemData.InodesUsed = state.InodesUsed
itemData.InodesFree = state.InodesFree
select { itemData.InodesUsedPercent = state.InodesUsedPercent
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()
} }
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
}(mounts[i]) }(mounts[i])
} }
wg.Wait() wg.Wait()
@@ -569,6 +531,69 @@ func loadDiskInfo() []dto.DiskInfo {
return datas 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) { func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
ok, client := accelerator.New() ok, client := accelerator.New()
if !ok { if !ok {
+63 -19
View File
@@ -8,6 +8,12 @@ import (
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -17,12 +23,15 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/compose" "github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/docker/docker/api/types/container" dockerclient "github.com/docker/docker/client"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
type RedisService struct{} type RedisService struct{}
// The CLI container is shared by all remote Redis databases on this node.
var redisCliInstallMutex sync.Mutex
type IRedisService interface { type IRedisService interface {
UpdateConf(req dto.RedisConfUpdate) error UpdateConf(req dto.RedisConfUpdate) error
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
@@ -33,7 +42,7 @@ type IRedisService interface {
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error) LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
CheckHasCli() bool CheckHasCli() bool
InstallCli() error InstallCli(req dto.RedisCliInstall) (string, error)
} }
func NewIRedisService() IRedisService { func NewIRedisService() IRedisService {
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
} }
func (u *RedisService) CheckHasCli() bool { func (u *RedisService) CheckHasCli() bool {
client, err := docker.NewDockerClient() installed, _ := u.checkCliInstalled()
if err != nil { return installed
return false
}
defer client.Close()
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return false
}
for _, item := range containerLists {
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
return true
}
}
return false
} }
func (u *RedisService) InstallCli() error { func (u *RedisService) checkCliInstalled() (bool, error) {
client, err := docker.NewDockerClient()
if err != nil {
return false, err
}
defer client.Close()
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
if dockerclient.IsErrNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
}
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
if !redisCliInstallMutex.TryLock() {
return "", buserr.New("TaskIsExecuting")
}
defer redisCliInstallMutex.Unlock()
item := dto.ContainerOperate{ item := dto.ContainerOperate{
TaskID: req.TaskID,
Name: "1Panel-redis-cli-tools", Name: "1Panel-redis-cli-tools",
Image: "redis:7.4.4", Image: "redis:7.4.4",
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}}, Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
} }
return NewIContainerService().ContainerCreate(item, false) running, err := taskRepo.GetFirst(
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
repo.WithByType(task.TaskScopeContainer),
taskRepo.WithByStatus(constant.StatusExecuting),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
if running.ID != "" {
return running.ID, nil
}
installed, err := u.checkCliInstalled()
if err != nil || installed {
return "", err
}
if item.TaskID == "" {
item.TaskID = uuid.NewString()
}
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
if err != nil {
return "", err
}
return "", buserr.New("TaskIsExecuting")
}
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
return "", err
}
return item.TaskID, nil
} }
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error { func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
+1 -1
View File
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
if baseInfo.SwapMemoryTotal != 0 { if baseInfo.SwapMemoryTotal != 0 {
baseInfo.SwapDetails = loadSwap() baseInfo.SwapDetails = loadSwap()
} }
disks := loadDiskInfo() disks := loadDiskInfo(false)
for _, item := range disks { for _, item := range disks {
baseInfo.MaxSize += item.Free baseInfo.MaxSize += item.Free
} }
+3
View File
@@ -487,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
if err := controller.Handle(req.Operation, service); err != nil { if err := controller.Handle(req.Operation, service); err != nil {
return err return err
} }
if req.Operation == "start" || req.Operation == "restart" {
return RestoreDockerPortGuard(context.Background())
}
return nil return nil
} }
+2 -3
View File
@@ -38,9 +38,8 @@ var (
monitorRepo = repo.NewIMonitorRepo() monitorRepo = repo.NewIMonitorRepo()
vllmMonitorRepo = &repo.VLLMMonitorRepo{} vllmMonitorRepo = &repo.VLLMMonitorRepo{}
settingRepo = repo.NewISettingRepo() settingRepo = repo.NewISettingRepo()
forwardingRuleRepo = repo.NewIForwardingRuleRepo() backupRepo = repo.NewIBackupRepo()
backupRepo = repo.NewIBackupRepo()
websiteRepo = repo.NewIWebsiteRepo() websiteRepo = repo.NewIWebsiteRepo()
websiteDomainRepo = repo.NewIWebsiteDomainRepo() websiteDomainRepo = repo.NewIWebsiteDomainRepo()
+1 -1
View File
@@ -1310,7 +1310,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
} }
func (f *FileService) GetHostMount() []dto.DiskInfo { func (f *FileService) GetHostMount() []dto.DiskInfo {
return loadDiskInfo() return loadDiskInfo(false)
} }
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) { func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
File diff suppressed because it is too large Load Diff
+233 -157
View File
@@ -2,17 +2,16 @@ package service
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"slices"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"github.com/1Panel-dev/1Panel/agent/app/dto" "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/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
@@ -39,9 +38,8 @@ const (
) )
type DockerPortGuardService struct { type DockerPortGuardService struct {
policies repo.IDockerPortGuardRepo
runtime dockerfirewall.Runtime runtime dockerfirewall.Runtime
runtimeForBackend func(string) dockerfirewall.Runtime runtimeForBackend func(context.Context, string) dockerfirewall.Runtime
client func() (*client.Client, error) client func() (*client.Client, error)
version func(string) string version func(string) string
} }
@@ -50,24 +48,35 @@ var dockerPortGuardServiceMu sync.Mutex
type IDockerPortGuardService interface { type IDockerPortGuardService interface {
LoadOverview(context.Context) (dto.DockerPortGuardList, error) LoadOverview(context.Context) (dto.DockerPortGuardList, error)
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error) LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
Operate(context.Context, dto.DockerPortGuardOperation) error Operate(context.Context, dto.DockerPortGuardOperation) error
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (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) { func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
policies, err := s.policies.ListManaged(ctx) families, err := loadFirewallFamilies()
if err != nil { if err != nil {
return dto.DockerPortGuardList{}, err 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 { unavailable := func() dto.DockerPortGuardList {
backend := selectedDockerFirewallBackend("") 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") 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() cli, err := s.client()
if err != nil { if err != nil {
@@ -79,13 +88,13 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
return unavailable(), nil return unavailable(), nil
} }
detectedBackend := dockerFirewallBackend(info) detectedBackend := dockerFirewallBackend(info)
backend := selectedDockerFirewallBackend(detectedBackend) backend = selectedDockerFirewallBackend(detectedBackend)
base := s.runtimeStatus(s.guardRuntime(backend), backend) base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
endpoints, err := discoverDockerEndpoints(ctx, cli, true) endpoints, err := discoverDockerEndpoints(ctx, cli, true)
if err != nil { if err != nil {
return dto.DockerPortGuardList{}, err return dto.DockerPortGuardList{}, err
} }
annotateDockerEndpointManagement(endpoints, detectedBackend) annotateDockerEndpointManagement(ctx, endpoints, detectedBackend)
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints) endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
sort.Slice(endpoints, func(i, j int) bool { 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) 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 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) { func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
cli, err := s.client() cli, err := s.client()
if err != nil { if err != nil {
@@ -117,7 +144,7 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.
if info, infoErr := cli.Info(ctx); infoErr == nil { if info, infoErr := cli.Info(ctx); infoErr == nil {
backend = dockerFirewallBackend(info) backend = dockerFirewallBackend(info)
} }
annotateDockerEndpointManagement(endpoints, backend) annotateDockerEndpointManagement(ctx, endpoints, backend)
return groupDockerGuardContainers(endpoints), nil return groupDockerGuardContainers(endpoints), nil
} }
@@ -126,34 +153,17 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
defer dockerPortGuardServiceMu.Unlock() defer dockerPortGuardServiceMu.Unlock()
switch request.Operation { switch request.Operation {
case "initialize": case "initialize":
runtime, backend, err := s.runtimeForDocker(ctx) return s.initialize(ctx, request, nil)
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
case "bind": case "bind":
runtime, _, err := s.runtimeForDocker(ctx) runtime, _, err := s.runtimeForDocker(ctx)
if err != nil { if err != nil {
return err 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 err
} }
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable) 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 { if s.runtime != nil {
err = s.runtime.Unbind() err = s.runtime.Unbind()
} else { } 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 err
} }
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable) 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) { 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 { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err return dto.FilterChainOperationResponse{}, err
} }
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskDocker, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0) if request.Operation != "initialize" {
if err != nil { return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
return dto.FilterChainOperationResponse{}, fmt.Errorf("create Docker port guard initialization task: %w", err)
} }
var runtime dockerfirewall.Runtime if request.BackupFile != "" {
var backend string if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil {
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func(t *task.Task) error { return dto.FilterChainOperationResponse{}, err
var err error
runtime, backend, err = s.runtimeForDocker(t.TaskCtx)
if err != nil {
return err
} }
t.Logf("backend=%s", backend) }
return nil return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error {
}, nil)
taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeDockerGuardStep", "Docker"), func(t *task.Task) error {
dockerPortGuardServiceMu.Lock() dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock() defer dockerPortGuardServiceMu.Unlock()
policies, err := s.runtimePolicies(t.TaskCtx) return s.initialize(t.TaskCtx, request, t)
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
} }
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) { func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
@@ -230,20 +207,34 @@ func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolic
if err != nil { if err != nil {
return dto.FilterChainOperationResponse{}, err return dto.FilterChainOperationResponse{}, err
} }
labels := make([]string, len(uuids)) return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error {
for i, id := range uuids { ctx := t.TaskCtx
labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id)
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, labels, func(ctx context.Context) error {
dockerPortGuardServiceMu.Lock() dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock() defer dockerPortGuardServiceMu.Unlock()
if err := ctx.Err(); err != nil { runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err return err
} }
if err := s.policies.DeleteBatch(ctx, uuids); err != nil { inventory, err := runtime.ListPolicies()
if err != nil {
return err 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) return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
} }
labels := make([]string, len(request.Policies)) 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)) endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
count := 0 count := 0
for i, policy := range request.Policies { for i, policy := range request.Policies {
@@ -264,7 +255,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
if err != nil { if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err) 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++ count++
} else { } else {
count += len(normalized.Sources) count += len(normalized.Sources)
@@ -275,42 +266,142 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
if count > filter.MaxAtomicExpansion { if count > filter.MaxAtomicExpansion {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", 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) normalized.UUID = uuid.NewString()
if err != nil { policies = append(policies, normalized)
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,
})
} }
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() dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock() defer dockerPortGuardServiceMu.Unlock()
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err 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 { if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
return err return err
} }
if err := s.policies.UpsertBatch(ctx, policies); err != nil { if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil {
return err return err
} }
return s.reconcileLocked(ctx) return nil
}) })
} }
func NewIDockerPortGuardService() IDockerPortGuardService { func (s *DockerPortGuardService) Restore(ctx context.Context) error {
return newDockerPortGuardService() 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) ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
ipv6 := runtime.Status(dockerfirewall.FamilyIPv6) var ipv6 dockerfirewall.FamilyStatus
if ipv6Enabled {
ipv6 = runtime.Status(dockerfirewall.FamilyIPv6)
}
version := "-" version := "-"
if s.version != nil { if s.version != nil {
version = s.version(backend) version = s.version(backend)
@@ -320,83 +411,68 @@ func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, b
name = "nftables-docker" name = "nftables-docker"
} }
return dto.DockerPortGuardBase{ return dto.DockerPortGuardBase{
IPv6Enabled: ipv6Enabled,
Name: name, Name: name,
Version: version, Version: version,
Backend: backend, 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, Initialized: ipv4.Initialized || ipv6.Initialized,
Bound: ipv4.Bound || ipv6.Bound, Bound: ipv4.Bound || ipv6.Bound,
IPv4: dto.DockerPortGuardFamilyStatus{State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.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{State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.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 { func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
endpoints := make([]dto.DockerPortGuardEndpoint, 0, len(policies)) matched := make(map[int]bool, len(policies))
for _, policy := range policies { byEndpoint := make(map[string]int, len(policies))
sources := []string{} for index, policy := range policies {
_ = json.Unmarshal([]byte(policy.Sources), &sources) key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{ if _, exists := byEndpoint[key]; !exists {
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, byEndpoint[key] = index
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
} }
for i := range endpoints { for i := range endpoints {
key := fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00")
policy, ok := byEndpoint[key] index, exists := byEndpoint[key]
if !ok { if !exists {
continue continue
} }
sources := []string{} policy := policies[index]
_ = json.Unmarshal([]byte(policy.Sources), &sources) endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, sources endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
endpoints[i].Description = policy.Description matched[index] = true
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard &&
((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
delete(byEndpoint, key)
} }
orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint)) orphans := make([]dto.DockerPortGuardEndpoint, 0)
for _, policy := range byEndpoint { for i, policy := range policies {
sources := []string{} if !matched[i] {
_ = json.Unmarshal([]byte(policy.Sources), &sources) orphans = append(orphans, policy)
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,
})
} }
return endpoints, orphanPolicies return endpoints, orphans
} }
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error { func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
if s.client == nil || len(requested) == 0 { if s.client == nil || len(requested) == 0 {
return nil return ctx.Err()
} }
cli, err := s.client() cli, err := s.client()
if err != nil { if err != nil {
return nil return ctx.Err()
} }
defer cli.Close() defer cli.Close()
info, err := cli.Info(ctx) info, err := cli.Info(ctx)
if err != nil { if err != nil {
return nil return ctx.Err()
} }
endpoints, err := discoverDockerEndpoints(ctx, cli, true) endpoints, err := discoverDockerEndpoints(ctx, cli, true)
if err != nil { 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)) targets := make(map[string]string, len(endpoints))
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
+333 -129
View File
@@ -10,7 +10,7 @@ import (
"sync" "sync"
"github.com/1Panel-dev/1Panel/agent/app/dto" "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/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -18,7 +18,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" 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/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/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -28,106 +31,115 @@ type IFirewallSettingService interface {
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error) Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error Operate(context.Context, dto.FirewallBackendOperation) error
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
} }
type FirewallSettingService struct{} type FirewallSettingService struct{}
var firewallWhitelistMu sync.Mutex 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() firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer func() {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { firewallRuleMutationMu.Unlock()
current, err := loadPortWhitelistSetting(tx) firewallWhitelistMu.Unlock()
if err != nil { if result == nil {
return err result = newFirewallService().SyncPortWhitelist(ctx)
} }
current = append(current, request.Rule) }()
current, err = firewall.ValidatePortWhitelist(current) current, err := loadFirewallPortWhiteList()
if err != nil { if err != nil {
return err return err
} }
value, err := json.Marshal(current) current = append(current, request.Rule)
if err != nil { current, err = firewall.ValidatePortWhitelist(current)
return err 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 { if err := validateFirewallWhitelistFamilies(current); err != nil {
return err return err
} }
return nil 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() firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer func() {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { firewallRuleMutationMu.Unlock()
current, err := loadPortWhitelistSetting(tx) firewallWhitelistMu.Unlock()
if err != nil { if result == nil {
return err result = newFirewallService().SyncPortWhitelist(ctx)
} }
index, err := findPortWhitelistRule(current, request.OldRule) }()
if err != nil { current, err := loadFirewallPortWhiteList()
return err if err != nil {
} return err
current[index] = request.Rule }
current, err = firewall.ValidatePortWhitelist(current) index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil { if err != nil {
return err return err
} }
value, err := json.Marshal(current) current[index] = request.Rule
if err != nil { current, err = firewall.ValidatePortWhitelist(current)
return err 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 { if err := validateFirewallWhitelistFamilies(current); err != nil {
return err return err
} }
return nil 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 { 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() firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer func() {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { firewallRuleMutationMu.Unlock()
current, err := loadPortWhitelistSetting(tx) firewallWhitelistMu.Unlock()
if err != nil { }()
return err current, err := loadFirewallPortWhiteList()
} if err != nil {
index, err := findPortWhitelistRule(current, *request.Rule) return err
if err != nil { }
return err if request.Rule == nil {
} return filter.ErrInvalidRule
current = slices.Delete(current, index, index+1) }
current, err = firewall.ValidatePortWhitelist(current) index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil { if err != nil {
return err return err
} }
value, err := json.Marshal(current) current = slices.Delete(current, index, index+1)
if err != nil { current, err = firewall.ValidatePortWhitelist(current)
return err 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 { if err := validateFirewallWhitelistFamilies(current); err != nil {
return err return err
} }
return nil 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) { 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) installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() { for _, name := range lifecycle.InstalledProviders() {
@@ -153,7 +165,7 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables { } else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
overview, err := loadSystemFirewallOverview(name, "base") overview, err := loadSystemFirewallOverview(name, "base", families)
if err != nil { if err != nil {
option.Message = err.Error() 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} { for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true} option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.Forwarding.Selected { if option.Installed && name == result.Forwarding.Selected {
manager, err := newForwardingAdapterFor(name) manager, err := newForwardingAdapterFor(ctx, name)
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} else { } else {
status, statusErr := loadForwardingFirewallOverview(manager) status, statusErr := loadForwardingFirewallOverview(manager, families)
option.IPv4, option.IPv6 = status.IPv4, status.IPv6 option.IPv4, option.IPv6 = status.IPv4, status.IPv6
if statusErr != nil { if statusErr != nil {
option.Message = statusErr.Error() option.Message = statusErr.Error()
} else { } else {
option.Initialized, option.Bound = status.IsInit, status.IsBind 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() { if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
} }
@@ -228,20 +240,25 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
option.Active = false option.Active = false
} }
if option.Active { if option.Active {
guard := newDockerFirewallRuntime(name) guard := newDockerFirewallRuntime(ctx, name)
ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6) for _, family := range families {
option.Initialized = ipv4.Initialized || ipv6.Initialized status := guard.Status(family)
option.Bound = ipv4.Bound || ipv6.Bound option.Initialized = option.Initialized || status.Initialized
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound option.Bound = option.Bound || status.Bound
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound info := dto.FirewallBackendFamilyStatus{
option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing Available: status.Reason != dockerfirewall.ReasonCommandMissing,
option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason }
if family == constant.FirewallFamilyIPv4 {
option.IPv4 = info
} else {
option.IPv6 = info
}
}
} }
result.Docker.Options = append(result.Docker.Options, option) result.Docker.Options = append(result.Docker.Options, option)
} }
var err error result.PortWhitelist, err = loadPortWhitelistSetting()
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
if err != nil { if err != nil {
return result, err 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 { 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 { if err := lockFirewallLifecycleIdle(); err != nil {
return err return err
} }
@@ -273,13 +294,12 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
} }
if request.Operation == "initialize" { if request.Operation == "initialize" {
service := newFirewallService() service := newFirewallService()
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
whitelistErr := service.SyncPortWhitelist(ctx) whitelistErr := service.SyncPortWhitelist(ctx)
return errors.Join(rulesErr, whitelistErr) return whitelistErr
} }
return nil return nil
case "forwarding": case "forwarding":
return s.operateForwarding(request) return s.operateForwarding(ctx, request)
case "docker": case "docker":
return s.operateDocker(ctx, request) return s.operateDocker(ctx, request)
default: 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 { func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{} return &FirewallSettingService{}
} }
@@ -297,9 +522,6 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
if _, err := lifecycle.NewClient(request.Backend); err != nil { if _, err := lifecycle.NewClient(request.Backend); err != nil {
return err return err
} }
if request.Operation == "cleanup" {
return cleanupSystemBackend(request.Backend)
}
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey) previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
if previous == "" { if previous == "" {
if client, err := lifecycle.NewClient(""); err == nil { if client, err := lifecycle.NewClient(""); err == nil {
@@ -363,32 +585,21 @@ func systemFirewallBackendInitialized(backend string) (bool, error) {
return client.Status() return client.Status()
} }
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error { func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
manager, err := newForwardingAdapterFor(request.Backend) if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
if err != nil {
return err 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) previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
if request.Operation == "select" { if request.Operation == "select" {
current := previous current := previous
if current == "" { if current == "" {
detected, err := newForwardingAdapter() detected, err := newForwardingAdapter(ctx)
if err != nil { if err != nil {
return err return err
} }
current = detected.Name() current = detected.Name()
} }
initialized, err := forwardingBackendInitialized(current) initialized, err := forwardingBackendInitialized(ctx, current)
if err != nil { if err != nil {
return err return err
} }
@@ -400,14 +611,13 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
return err return err
} }
if request.Operation == "initialize" { if request.Operation == "initialize" {
return newForwardingService().Enable() return newForwardingService().Enable(ctx)
} }
recordForwardingSyncError(nil)
return nil return nil
} }
func forwardingBackendInitialized(backend string) (bool, error) { func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
manager, err := newForwardingAdapterFor(backend) manager, err := newForwardingAdapterFor(ctx, backend)
if err != nil { if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) { if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil return false, nil
@@ -427,13 +637,7 @@ func forwardingBackendInitialized(backend string) (bool, error) {
} }
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) 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) previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if request.Operation == "select" { if request.Operation == "select" {
current := previous current := previous
@@ -443,7 +647,7 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
current = constant.FirewallProviderIptables current = constant.FirewallProviderIptables
} }
} }
initialized, err := dockerGuardBackendInitialized(current) initialized, err := dockerGuardBackendInitialized(ctx, current)
if err != nil { if err != nil {
return err return err
} }
@@ -469,8 +673,8 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
return nil return nil
} }
func dockerGuardBackendInitialized(backend string) (bool, error) { func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
guard := newDockerFirewallRuntime(backend) guard := newDockerFirewallRuntime(ctx, backend)
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} { for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
initialized, err := guard.Initialized(family) initialized, err := guard.Initialized(family)
if err != nil { if err != nil {
-396
View File
@@ -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)
}
}
File diff suppressed because it is too large Load Diff
+149 -425
View File
@@ -5,71 +5,55 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "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/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "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"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "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/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
) )
const (
forwardingSyncConverged = "converged"
forwardingSyncMissing = "missing"
forwardingSyncRuntimeOnly = "runtime_only"
)
type IForwardingService interface { type IForwardingService interface {
LoadBaseInfo() (dto.FirewallSubsystemStatus, error) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error)
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, 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) OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
Enable() error Enable(ctx context.Context) error
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
Restore(context.Context) error Restore(context.Context) error
} }
type ForwardingService struct { type ForwardingService struct {
clientFactory func() (forwarding.Adapter, error) clientFactory func(context.Context) (forwarding.Adapter, error)
rules repo.IForwardingRuleRepo
enabled func() (bool, error)
persistBackend func(string) error
markEnabled func() error
} }
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected") var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
var forwardingMutationMu sync.Mutex var forwardingMutationMu sync.Mutex
var ( func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) {
forwardingSyncStateMu sync.RWMutex families, err := loadFirewallFamilies()
forwardingLastSyncErr error if err != nil {
) return dto.FirewallSubsystemStatus{}, err
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey) selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
selected = strings.TrimSpace(selected) selected = strings.TrimSpace(selected)
if selected == "" { if selected == "" {
selected = constant.FirewallProviderIptables selected = constant.FirewallProviderIptables
} }
baseInfo := dto.FirewallSubsystemStatus{ 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 { if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
baseInfo.Name += "-forward" baseInfo.Name += "-forward"
} }
manager, err := s.clientFactory() manager, err := s.clientFactory(ctx)
if err != nil { if err != nil {
if errors.Is(err, errForwardingBackendUnavailable) { if errors.Is(err, errForwardingBackendUnavailable) {
baseInfo.Reason = constant.FirewallBackendNotInstalled baseInfo.Reason = constant.FirewallBackendNotInstalled
@@ -82,7 +66,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
return baseInfo, err return baseInfo, err
} }
version, versionErr := client.Version() version, versionErr := client.Version()
status, statusErr := loadForwardingFirewallOverview(manager) status, statusErr := loadForwardingFirewallOverview(manager, families)
if err := errors.Join(versionErr, statusErr); err != nil { if err := errors.Join(versionErr, statusErr); err != nil {
return baseInfo, err return baseInfo, err
} }
@@ -95,83 +79,51 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
baseInfo.PingStatus = firewall.LoadPingStatus() baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6 baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
for _, family := range []struct {
command string
status *dto.FirewallBackendFamilyStatus
}{
{"iptables", &baseInfo.IPv4},
{"ip6tables", &baseInfo.IPv6},
} {
policy, err := loadForwardPolicy(family.command)
if err != nil {
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
continue
}
family.status.ForwardPolicy = policy
}
return baseInfo, nil return baseInfo, nil
} }
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) { func (s *ForwardingService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
if request.Strategy != "" { var manager forwarding.Adapter
return 0, nil, nil 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 { if err != nil {
return 0, nil, err return 0, nil, err
} }
manager, err := s.clientFactory() rules, err := manager.List()
if err != nil {
return 0, nil, err
}
runtime, err := manager.List()
if err != nil {
return 0, nil, err
}
inventory, err := mergeForwardingInventory(stored, runtime)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
items := make([]dto.ForwardRule, 0, len(rules))
keyword := strings.ToLower(strings.TrimSpace(request.Info)) keyword := strings.ToLower(strings.TrimSpace(request.Info))
filtered := inventory[:0] for _, rule := range rules {
for _, item := range inventory { if keyword != "" && !strings.Contains(strings.ToLower(strings.Join([]string{rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface}, " ")), keyword) {
if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) { continue
filtered = append(filtered, item)
} }
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(items)
total := len(inventory)
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
if request.All { if request.All {
start, end = 0, total return int64(total), items, nil
} }
if start > total { start := min(max(request.Page-1, 0)*max(request.PageSize, 1), total)
return int64(total), make([]dto.ForwardRule, 0), nil end := min(start+max(request.PageSize, 1), total)
} return int64(total), items[start:end], 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
} }
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) { func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
@@ -198,7 +150,7 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
return dto.FilterChainOperationResponse{}, err return dto.FilterChainOperationResponse{}, err
} }
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { 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) }, nil, 0, 0)
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err) taskItem.LogFailedWithErr(taskItem.Name, err)
@@ -209,358 +161,130 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
} }
func (s *ForwardingService) Enable() error { func (s *ForwardingService) Enable(ctx context.Context) error {
forwardingMutationMu.Lock() forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock() defer forwardingMutationMu.Unlock()
manager, err := s.clientFactory() manager, err := s.clientFactory(ctx)
if err != nil { if err != nil {
recordForwardingSyncError(err)
return err return err
} }
if err := s.persistForwardingEnabled(); err != nil { rules, err := manager.List()
recordForwardingSyncError(err)
return err
}
if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err)
return err
}
rules, err := s.rules.List(context.Background())
if err != nil { if err != nil {
recordForwardingSyncError(err)
return err return err
} }
err = manager.ReplaceRules(forwardingRulesFromModels(rules)) families, err := loadFirewallFamilies()
recordForwardingSyncError(err) if err != nil {
return err 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) { func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err return dto.FilterChainOperationResponse{}, err
} }
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0) var backup dto.FirewallSubsystemBackup
if err != nil { if request.BackupFile != "" {
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 err error var err error
manager, err = s.clientFactory() backup, err = readFirewallSubsystemBackup(request.BackupFile, "forwarding")
if err != nil { if err != nil {
recordForwardingSyncError(err) return dto.FilterChainOperationResponse{}, err
return 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() }() operations := dto.ForwardRuleOperate{Rules: make([]dto.ForwardRuleOperation, 0, len(backup.Forwarding))}
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil 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 { func NewIForwardingService() IForwardingService {
return newForwardingService() return newForwardingService()
} }
func loadForwardPolicy(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")
if err != nil {
return "", err
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
if fields[2] != "ACCEPT" && fields[2] != "DROP" {
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
}
return fields[2], nil
}
}
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...)
}
+2
View File
@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -44,6 +45,7 @@ type ISettingService interface {
GetLocalConnForSSH() (dto.SSHConnData, error) GetLocalConnForSSH() (dto.SSHConnData, error)
SaveDescription(req dto.CommonDescription) error SaveDescription(req dto.CommonDescription) error
CleanupDescriptions(context.Context) (int64, error)
} }
func NewISettingService() ISettingService { func NewISettingService() ISettingService {
+114
View File
@@ -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
}
+10
View File
@@ -87,6 +87,7 @@ const (
TaskScopeCronjob = "Cronjob" TaskScopeCronjob = "Cronjob"
TaskScopeClam = "Clam" TaskScopeClam = "Clam"
TaskScopeSystem = "System" TaskScopeSystem = "System"
TaskScopeVm = "VirtualMachine"
TaskScopeFirewall = "Firewall" TaskScopeFirewall = "Firewall"
TaskScopeAppStore = "AppStore" TaskScopeAppStore = "AppStore"
TaskScopeSnapshot = "Snapshot" TaskScopeSnapshot = "Snapshot"
@@ -99,6 +100,7 @@ const (
TaskScopeTamper = "Tamper" TaskScopeTamper = "Tamper"
TaskScopeFileConvert = "Convert" TaskScopeFileConvert = "Convert"
TaskScopeTask = "Task" TaskScopeTask = "Task"
TaskScopeVm = "VirtualMachine"
) )
func GetTaskName(resourceName, operate, scope string) string { func GetTaskName(resourceName, operate, scope string) string {
@@ -257,7 +259,15 @@ func (s *SubTask) Execute() error {
case <-ctx.Done(): case <-ctx.Done():
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName)) s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
err = errors.New("timeout!") err = errors.New("timeout!")
if s.RootTask.Task.Type == TaskScopeVm {
<-done
}
case err = <-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 { if err != nil {
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err)) s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
if err.Error() == i18n.GetMsgByKey("ErrShutDown") { if err.Error() == i18n.GetMsgByKey("ErrShutDown") {
+99 -111
View File
@@ -265,8 +265,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]", "formatZH": "应用端口修改 [key]-[name] => [port]",
"formatEN": "Application port update [key]-[name] =\u003e [port]" "formatEN": "Application port update [key]-[name] => [port]"
}, },
"/apps/installed/sort/update": { "/apps/installed/sort/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -644,13 +644,6 @@
"formatZH": "创建容器网络 name", "formatZH": "创建容器网络 name",
"formatEN": "create container network [name]" "formatEN": "create container network [name]"
}, },
"/containers/network/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理未使用的容器网络",
"formatEN": "Clean unused container networks"
},
"/containers/network/del": { "/containers/network/del": {
"bodyKeys": [ "bodyKeys": [
"names" "names"
@@ -686,8 +679,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "容器重命名 [name] =\u003e [newName]", "formatZH": "容器重命名 [name] => [newName]",
"formatEN": "rename container [name] =\u003e [newName]" "formatEN": "rename container [name] => [newName]"
}, },
"/containers/repo": { "/containers/repo": {
"bodyKeys": [ "bodyKeys": [
@@ -838,8 +831,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/auth/expired/reset": { "/core/auth/expired/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -1723,8 +1716,8 @@
"output_value": "name" "output_value": "name"
} }
], ],
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/enterprise/users/del": { "/core/enterprise/users/del": {
"bodyKeys": [ "bodyKeys": [
@@ -1925,8 +1918,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", "formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]" "formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
}, },
"/core/settings/memo": { "/core/settings/memo": {
"bodyKeys": [], "bodyKeys": [],
@@ -1955,8 +1948,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统端口 =\u003e [serverPort]", "formatZH": "修改系统端口 => [serverPort]",
"formatEN": "update system port =\u003e [serverPort]" "formatEN": "update system port => [serverPort]"
}, },
"/core/settings/proxy/update": { "/core/settings/proxy/update": {
"bodyKeys": [ "bodyKeys": [
@@ -1981,8 +1974,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统 ssl =\u003e [ssl]", "formatZH": "修改系统 ssl => [ssl]",
"formatEN": "update system ssl =\u003e [ssl]" "formatEN": "update system ssl => [ssl]"
}, },
"/core/settings/terminal/update": { "/core/settings/terminal/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -1998,8 +1991,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] =\u003e [value]", "formatZH": "修改系统配置 [key] => [value]",
"formatEN": "update system setting [key] =\u003e [value]" "formatEN": "update system setting [key] => [value]"
}, },
"/core/settings/upgrade": { "/core/settings/upgrade": {
"bodyKeys": [ "bodyKeys": [
@@ -2007,8 +2000,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新系统 =\u003e [version]", "formatZH": "更新系统 => [version]",
"formatEN": "upgrade system =\u003e [version]" "formatEN": "upgrade system => [version]"
}, },
"/core/xapp/appQRCode": { "/core/xapp/appQRCode": {
"bodyKeys": [ "bodyKeys": [
@@ -2466,7 +2459,7 @@
"formatZH": "同步 SSL 证书 [primaryDomain]", "formatZH": "同步 SSL 证书 [primaryDomain]",
"formatEN": "sync SSL certificate [primaryDomain]" "formatEN": "sync SSL certificate [primaryDomain]"
}, },
"/core/xpack/vms": { "/xpack/vms": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
], ],
@@ -2475,7 +2468,7 @@
"formatZH": "创建虚拟机 [name]", "formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]" "formatEN": "create VM [name]"
}, },
"/core/xpack/vms/del": { "/xpack/vms/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2493,7 +2486,7 @@
"formatZH": "删除虚拟机 [name]", "formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]" "formatEN": "delete VM [name]"
}, },
"/core/xpack/vms/disks/resize": { "/xpack/vms/disks/resize": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"target", "target",
@@ -2504,14 +2497,14 @@
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB", "formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB" "formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
}, },
"/core/xpack/vms/environment/enable": { "/xpack/vms/environment/enable": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "启用虚拟机运行环境", "formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment" "formatEN": "enable VM runtime environment"
}, },
"/core/xpack/vms/iso": { "/xpack/vms/iso": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2521,7 +2514,7 @@
"formatZH": "创建虚拟机镜像 [name] [type]", "formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]" "formatEN": "create VM ISO [name] [type]"
}, },
"/core/xpack/vms/iso/del": { "/xpack/vms/iso/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2539,7 +2532,7 @@
"formatZH": "删除虚拟机镜像 [name]", "formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]" "formatEN": "delete VM ISO [name]"
}, },
"/core/xpack/vms/iso/update": { "/xpack/vms/iso/update": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2549,7 +2542,7 @@
"formatZH": "更新虚拟机镜像 [name] [type]", "formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]" "formatEN": "update VM ISO [name] [type]"
}, },
"/core/xpack/vms/networks": { "/xpack/vms/networks": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2559,7 +2552,7 @@
"formatZH": "创建虚拟机网络 [name] [type]", "formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]" "formatEN": "create VM network [name] [type]"
}, },
"/core/xpack/vms/networks/del": { "/xpack/vms/networks/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2577,7 +2570,7 @@
"formatZH": "删除虚拟机网络 [name]", "formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]" "formatEN": "delete VM network [name]"
}, },
"/core/xpack/vms/networks/update": { "/xpack/vms/networks/update": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2595,7 +2588,7 @@
"formatZH": "更新虚拟机网络 [name]", "formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]" "formatEN": "update VM network [name]"
}, },
"/core/xpack/vms/operate": { "/xpack/vms/operate": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"operate" "operate"
@@ -2605,14 +2598,14 @@
"formatZH": "操作虚拟机 [name] [operate]", "formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]" "formatEN": "operate VM [name] [operate]"
}, },
"/core/xpack/vms/orphans/clean": { "/xpack/vms/orphans/clean": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据", "formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data" "formatEN": "clean VM orphaned data"
}, },
"/core/xpack/vms/rename": { "/xpack/vms/rename": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"newName" "newName"
@@ -2622,7 +2615,7 @@
"formatZH": "重命名虚拟机 [name] 为 [newName]", "formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]" "formatEN": "rename VM [name] to [newName]"
}, },
"/core/xpack/vms/snapshots": { "/xpack/vms/snapshots": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -2632,7 +2625,7 @@
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]", "formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]" "formatEN": "create VM [name] snapshot [snapshotName]"
}, },
"/core/xpack/vms/snapshots/del": { "/xpack/vms/snapshots/del": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -2642,7 +2635,7 @@
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]", "formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]" "formatEN": "delete VM [name] snapshot [snapshotName]"
}, },
"/core/xpack/vms/snapshots/recover": { "/xpack/vms/snapshots/recover": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -2652,7 +2645,7 @@
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]", "formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]" "formatEN": "recover VM [name] snapshot [snapshotName]"
}, },
"/core/xpack/vms/storages": { "/xpack/vms/storages": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2662,7 +2655,7 @@
"formatZH": "创建虚拟机存储 [name] [type]", "formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]" "formatEN": "create VM storage [name] [type]"
}, },
"/core/xpack/vms/storages/del": { "/xpack/vms/storages/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2680,7 +2673,7 @@
"formatZH": "删除虚拟机存储 [name]", "formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]" "formatEN": "delete VM storage [name]"
}, },
"/core/xpack/vms/storages/update": { "/xpack/vms/storages/update": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2698,14 +2691,14 @@
"formatZH": "更新虚拟机存储 [name]", "formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]" "formatEN": "update VM storage [name]"
}, },
"/core/xpack/vms/sync": { "/xpack/vms/sync": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "同步虚拟机", "formatZH": "同步虚拟机",
"formatEN": "sync virtual machines" "formatEN": "sync virtual machines"
}, },
"/core/xpack/vms/templates": { "/xpack/vms/templates": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"templateName" "templateName"
@@ -2715,7 +2708,7 @@
"formatZH": "创建虚拟机 [name] 模板 [templateName]", "formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]" "formatEN": "create VM [name] template [templateName]"
}, },
"/core/xpack/vms/templates/del": { "/xpack/vms/templates/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2733,7 +2726,7 @@
"formatZH": "删除虚拟机模板 [name]", "formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]" "formatEN": "delete VM template [name]"
}, },
"/core/xpack/vms/update": { "/xpack/vms/update": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
], ],
@@ -2895,8 +2888,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "首页应用 [key] =\u003e 显示:[value]", "formatZH": "首页应用 [key] => 显示:[value]",
"formatEN": "app launcher [key] =\u003e show: [value]" "formatEN": "app launcher [key] => show: [value]"
}, },
"/dashboard/quick/change": { "/dashboard/quick/change": {
"bodyKeys": [], "bodyKeys": [],
@@ -3024,7 +3017,7 @@
} }
], ],
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]", "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": { "/databases/grants": {
"bodyKeys": [ "bodyKeys": [
@@ -3105,7 +3098,7 @@
} }
], ],
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]", "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": { "/databases/mongodb/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3190,7 +3183,7 @@
} }
], ],
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", "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": { "/databases/pg/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3295,8 +3288,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]", "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]" "formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
}, },
"/databases/variables/update": { "/databases/variables/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -3332,8 +3325,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]", "formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]" "formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
}, },
"/files/chunkdownload": { "/files/chunkdownload": {
"bodyKeys": [ "bodyKeys": [
@@ -3423,8 +3416,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改权限 [path] =\u003e [mode]", "formatZH": "修改权限 [path] => [mode]",
"formatEN": "Change mode [path] =\u003e [mode]" "formatEN": "Change mode [path] => [mode]"
}, },
"/files/move": { "/files/move": {
"bodyKeys": [ "bodyKeys": [
@@ -3433,8 +3426,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]", "formatZH": "移动文件 [oldPaths] => [newPath]",
"formatEN": "Move [oldPaths] =\u003e [newPath]" "formatEN": "Move [oldPaths] => [newPath]"
}, },
"/files/owner": { "/files/owner": {
"bodyKeys": [ "bodyKeys": [
@@ -3444,8 +3437,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]", "formatZH": "修改用户/组 [path] => [user]/[group]",
"formatEN": "Change owner [path] =\u003e [user]/[group]" "formatEN": "Change owner [path] => [user]/[group]"
}, },
"/files/preview": { "/files/preview": {
"bodyKeys": [ "bodyKeys": [
@@ -3479,8 +3472,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "重命名 [oldName] =\u003e [newName]", "formatZH": "重命名 [oldName] => [newName]",
"formatEN": "Rename [oldName] =\u003e [newName]" "formatEN": "Rename [oldName] => [newName]"
}, },
"/files/save": { "/files/save": {
"bodyKeys": [ "bodyKeys": [
@@ -3537,8 +3530,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "下载 url =\u003e [path]/[name]", "formatZH": "下载 url => [path]/[name]",
"formatEN": "Download url =\u003e [path]/[name]" "formatEN": "Download url => [path]/[name]"
}, },
"/files/wget/process/remove": { "/files/wget/process/remove": {
"bodyKeys": [ "bodyKeys": [
@@ -3688,12 +3681,16 @@
"formatZH": "删除 Docker 端口防护策略 [uuids]", "formatZH": "删除 Docker 端口防护策略 [uuids]",
"formatEN": "delete Docker port guard policies [uuids]" "formatEN": "delete Docker port guard policies [uuids]"
}, },
"/hosts/firewall/docker/sync": { "/hosts/firewall/family/operate": {
"bodyKeys": [], "bodyKeys": [
"subsystem",
"family",
"operation"
],
"formatEN": "[operation] [subsystem] [family] firewall chains",
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": []
"formatZH": "同步 Docker 端口防护规则",
"formatEN": "sync Docker port guard rules"
}, },
"/hosts/firewall/filter/operate": { "/hosts/firewall/filter/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3734,13 +3731,6 @@
"formatZH": "添加防火墙规则", "formatZH": "添加防火墙规则",
"formatEN": "create firewall rules" "formatEN": "create firewall rules"
}, },
"/hosts/firewall/rules/adopt": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "纳管防火墙规则",
"formatEN": "adopt firewall rule"
},
"/hosts/firewall/rules/delete": { "/hosts/firewall/rules/delete": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
@@ -3750,12 +3740,12 @@
}, },
"/hosts/firewall/rules/reorder": { "/hosts/firewall/rules/reorder": {
"bodyKeys": [ "bodyKeys": [
"uuid" "instanceKey"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "调整防火墙规则顺序 [uuid]", "formatZH": "调整防火墙规则顺序 [instanceKey]",
"formatEN": "reorder firewall rule [uuid]" "formatEN": "reorder firewall rule [instanceKey]"
}, },
"/hosts/firewall/rules/reset": { "/hosts/firewall/rules/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -3764,25 +3754,23 @@
"formatZH": "重置防火墙规则", "formatZH": "重置防火墙规则",
"formatEN": "reset firewall rules" "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": { "/hosts/firewall/rules/update": {
"bodyKeys": [ "bodyKeys": [
"uuid" "instanceKey"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防火墙规则 [uuid]", "formatZH": "更新防火墙规则 [instanceKey]",
"formatEN": "update firewall rule [uuid]" "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": { "/hosts/firewall/settings/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3900,8 +3888,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]", "formatZH": "修改 SSH 配置 [key] => [newValue]",
"formatEN": "update SSH setting [key] =\u003e [newValue]" "formatEN": "update SSH setting [key] => [newValue]"
}, },
"/hosts/tool/config/set": { "/hosts/tool/config/set": {
"bodyKeys": [ "bodyKeys": [
@@ -4162,7 +4150,7 @@
} }
], ],
"formatZH": "快照 [name] 描述信息修改 [description]", "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": { "/settings/snapshot/import": {
"bodyKeys": [ "bodyKeys": [
@@ -4254,8 +4242,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] =\u003e [value]", "formatZH": "修改系统配置 [key] => [value]",
"formatEN": "update system setting [key] =\u003e [value]" "formatEN": "update system setting [key] => [value]"
}, },
"/toolbox/clam": { "/toolbox/clam": {
"bodyKeys": [ "bodyKeys": [
@@ -4373,8 +4361,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机参数 [key] =\u003e [value]", "formatZH": "修改主机参数 [key] => [value]",
"formatEN": "update device conf [key] =\u003e [value]" "formatEN": "update device conf [key] => [value]"
}, },
"/toolbox/device/update/host": { "/toolbox/device/update/host": {
"bodyKeys": [ "bodyKeys": [
@@ -4383,8 +4371,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机 Host [key] =\u003e [value]", "formatZH": "修改主机 Host [key] => [value]",
"formatEN": "update device host [key] =\u003e [value]" "formatEN": "update device host [key] => [value]"
}, },
"/toolbox/device/update/swap": { "/toolbox/device/update/swap": {
"bodyKeys": [ "bodyKeys": [
@@ -4412,8 +4400,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]", "formatZH": "修改 Fail2ban 配置 [key] => [value]",
"formatEN": "update fail2ban conf [key] =\u003e [value]" "formatEN": "update fail2ban conf [key] => [value]"
}, },
"/toolbox/ftp": { "/toolbox/ftp": {
"bodyKeys": [ "bodyKeys": [
@@ -4672,8 +4660,8 @@
"output_value": "domain" "output_value": "domain"
} }
], ],
"formatZH": "修改默认 server =\u003e [domain]", "formatZH": "修改默认 server => [domain]",
"formatEN": "Change default server =\u003e [domain]" "formatEN": "Change default server => [domain]"
}, },
"/websites/del": { "/websites/del": {
"bodyKeys": [ "bodyKeys": [
@@ -5436,8 +5424,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]", "formatZH": "更新防篡改信息 [website][path] => [status]",
"formatEN": "update tamper info [website][path] =\u003e [status]" "formatEN": "update tamper info [website][path] => [status]"
}, },
"/xpack/vllm/command-template/create": { "/xpack/vllm/command-template/create": {
"bodyKeys": [ "bodyKeys": [
+1 -5
View File
@@ -17,6 +17,7 @@ const (
) )
const ( const (
FirewallIPv6SupportKey = "FirewallIPv6Support"
FirewallSystemBackendKey = "FirewallProvider" FirewallSystemBackendKey = "FirewallProvider"
FirewallForwardingBackendKey = "ForwardingBackend" FirewallForwardingBackendKey = "ForwardingBackend"
FirewallDockerBackendKey = "DockerFirewallBackend" FirewallDockerBackendKey = "DockerFirewallBackend"
@@ -33,12 +34,7 @@ const (
const ( const (
FirewallSystemAcceptedPortSourcePrefix = "accepted-port:" FirewallSystemAcceptedPortSourcePrefix = "accepted-port:"
FirewallRuleOriginCreated = "created"
FirewallRuleOriginAdopted = "adopted"
FirewallRuleSourceUser = "user" FirewallRuleSourceUser = "user"
FirewallRuleSourceImported = "imported" FirewallRuleSourceImported = "imported"
FirewallRuleSourcePanel = "panel"
FirewallRuleSourceSecurity = "security" FirewallRuleSourceSecurity = "security"
FirewallRuleSourceApp = "application"
) )
+4
View File
@@ -1,6 +1,10 @@
package constant package constant
const ( const (
StatusInactive = "Inactive"
StatusUnknown = "Unknown"
StatusDegraded = "Degraded"
StatusInaccessible = "Inaccessible"
StatusRunning = "Running" StatusRunning = "Running"
StatusCanceled = "Canceled" StatusCanceled = "Canceled"
StatusDone = "Done" StatusDone = "Done"
+12
View File
@@ -1,6 +1,7 @@
package cron package cron
import ( import (
"context"
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"math/big" "math/big"
@@ -82,6 +83,17 @@ func Run() {
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil { 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()) 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 var cronJobs []model.Cronjob
if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil { if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil {
+50 -26
View File
@@ -300,6 +300,9 @@ BuildCache: 'Build Cache'
Volume: 'Volume' Volume: 'Volume'
Network: "Network" Network: "Network"
PruneStart: 'Cleaning in progress' PruneStart: 'Cleaning in progress'
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
ContainerBackupPrepare: 'Prepare container backup workspace' ContainerBackupPrepare: 'Prepare container backup workspace'
ContainerBackupStop: 'Stop container before backup' ContainerBackupStop: 'Stop container before backup'
ContainerBackupInspect: 'Backup container inspect and network' ContainerBackupInspect: 'Backup container inspect and network'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again' 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' 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' ErrDockerForwardPolicyDrop: 'This server restricts network forwarding. Container port protection cannot be enabled at this time.'
ErrUFWRuleAdopt: 'Failed to adopt the UFW rule: {{ .detail }}. If the installed UFW version is earlier than 0.35, upgrade UFW and try again'
Firewall: 'Firewall' Firewall: 'Firewall'
FirewallTaskHost: 'Host firewall' FirewallTaskHost: 'Host firewall'
FirewallTaskForwarding: 'Port forwarding' FirewallTaskForwarding: 'Port forwarding'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Create firewall rules'
FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed' FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed'
FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed' FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed'
FirewallCreateRuleSkipped: 'Not executed' FirewallCreateRuleSkipped: 'Not executed'
FirewallCreateBatchStep: 'Submit {{ .count }} rules as a batch to {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; converted to {{ .count }} rules' 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' FirewallCreateRuleExecutionFailed: 'Rule creation failed; 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'
FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains' FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains'
FirewallRestoreRulesStep: 'Restore database rules to {{ .name }}'
FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist' FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist'
FirewallEnableForwardingStep: 'Enable and bind port forwarding chains' FirewallEnableForwardingStep: 'Enable and bind port forwarding chains'
FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules' FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules'
FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies' FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains' FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
FirewallPersistDockerGuardStep: 'Persist Docker port guard status' 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 }}' FileTaskCopy: 'Copy files to {{ .dst }}'
FileTaskMove: 'Move files to {{ .dst }}' FileTaskMove: 'Move files to {{ .dst }}'
FileTaskCompress: 'Compress files to {{ .dst }}' FileTaskCompress: 'Compress files to {{ .dst }}'
@@ -727,21 +716,56 @@ FileTaskDecompress: 'Extract files to {{ .dst }}'
FileTaskSource: 'Source path: {{ .path }}' FileTaskSource: 'Source path: {{ .path }}'
FileTaskFormat: 'Archive format: {{ .format }}' FileTaskFormat: 'Archive format: {{ .format }}'
FileTaskRename: 'Target filename: {{ .name }}' FileTaskRename: 'Target filename: {{ .name }}'
ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}." 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." 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 }}" 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."
ErrIPv6RARisk: "IPv6 forwarding is blocked to prevent possible network loss. See the documentation and system logs for details."
ErrIPv6RACheckFailed: "IPv6 forwarding is blocked because the network configuration could not be checked. See the system logs for details."
ErrSSHPublicKey: "Enter a valid SSH public key" ErrSSHPublicKey: "Enter a valid SSH public key"
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase" ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
ErrSSHKeyMismatch: "The SSH public and private keys do not match" ErrSSHKeyMismatch: "The SSH public and private keys do not match"
NetworkCleanupDeleted: "Deleted network [{{ .name }}] ({{ .id }})" # virtual machine
NetworkCleanupProtected: "Skipped network [{{ .name }}] ({{ .id }}): reserved network" ErrVMNotFound: "Virtual machine does not exist"
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted" ErrVMAlreadyExists: "Virtual machine already exists"
NetworkCleanupUnsupported: "Skipped network [{{ .name }}] ({{ .id }}): special network" ErrVMCountLimit: "The Professional edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade to Enterprise to create more."
NetworkCleanupGone: "Skipped network [{{ .name }}] ({{ .id }}): already removed" ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}"
NetworkCleanupInspectFailed: "Failed to inspect network [{{ .name }}] ({{ .id }}); not deleted" ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
NetworkCleanupRemoveFailed: "Failed to remove network [{{ .name }}] ({{ .id }})" ErrVMHelperNotFound: "1panel-kvm not found"
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}" ErrVMBridgeManaged: "Bridge name is already managed by libvirt or Docker"
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log" ErrVMNetworkInUse: "Network is in use and cannot be deleted"
ErrVMStorageInUse: "Storage pool is in use and cannot be deleted"
ErrVMStorageNameCannotChange: "Storage pool name cannot be changed"
ErrVMStoragePathCannotChange: "Storage pool path cannot be changed"
ErrVMRunning: "Virtual machine is running"
ErrVMLibvirtConnect: "Failed to connect to libvirt: {{ .detail }}"
ErrVMPermission: "No permission to operate virtual machine: {{ .detail }}"
ErrVMNetworkNotFound: "Virtual machine network does not exist: {{ .detail }}"
ErrVMInvalidPath: "Invalid virtual machine path: {{ .detail }}"
ErrVMInvalidInput: "Invalid virtual machine parameter: {{ .detail }}"
ErrVMCommandFailed: "Virtual machine command failed: {{ .err }}"
ErrVMDiskResizeBlocked: "The disk does not meet expansion requirements or is referenced by another device. Check the disk status."
ErrVMDiskReferenced: "This virtual disk is used by another virtual machine or device and cannot be expanded yet."
ErrVMDiskReferenceCheck: "Cannot complete virtual disk reference checks. Expansion is unavailable; check the disks of other virtual machines."
ErrVMSnapshotMetadataSync: "The snapshot was restored, but disk information could not be synced. Refresh and check; do not restore the snapshot again."
VMCreateTask: "Create virtual machine [{{ .name }}]"
VMCreateFromTemplateTask: "Create virtual machine [{{ .name }}] from template"
VMUpdateTask: "Update virtual machine [{{ .name }}]"
VMDiskResizeTask: "Expand disk [{{ .disk }}] of virtual machine [{{ .name }}]"
VMRenameTask: "Rename virtual machine [{{ .name }}] to [{{ .newName }}]"
VMDeleteTask: "Delete virtual machine [{{ .name }}]"
VMTemplateCreateTask: "Save virtual machine [{{ .name }}] as template [{{ .template }}]"
VMTemplateDeleteTask: "Delete virtual machine template [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm not found, force deleting virtual machine [{{ .name }}] metadata only"
VMForceDeleteMetadataAfterLibvirtFailed: "Failed to delete virtual machine [{{ .name }}] from libvirt, force deleting metadata: {{ .err }}"
VMForceDeleteDiskFailed: "Force delete virtual machine disk file [{{ .path }}] failed: {{ .err }}"
VMForceDeleteDiskSuccess: "Force deleted virtual machine disk file [{{ .path }}]"
VMSnapshotCreateTask: "Create snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotCreateStep: "Create virtual machine snapshot [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Recover snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotRecoverStep: "Recover virtual machine snapshot [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Delete snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotDeleteStep: "Delete virtual machine snapshot [{{ .snapshot }}]"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: 'Caché de compilación'
Volume: 'Volumen de almacenamiento' Volume: 'Volumen de almacenamiento'
Network: 'Red' Network: 'Red'
PruneStart: 'Limpiando en progreso espere...' PruneStart: 'Limpiando en progreso espere...'
NetworkCleanupConnected: "Se omitió la red [{{ .name }}] ({{ .id }}): hay contenedores conectados o la red está en uso; no se eliminó"
NetworkCleanupSummary: "Limpieza de redes finalizada: eliminadas {{ .deleted }}, omitidas {{ .skipped }}, fallidas {{ .failed }}"
NetworkCleanupPartialFailure: "No se pudieron limpiar algunas redes; consulte el registro de la tarea"
ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor' ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor'
ContainerBackupStop: 'Detener contenedor antes del respaldo' ContainerBackupStop: 'Detener contenedor antes del respaldo'
ContainerBackupInspect: 'Respaldar inspect y red del contenedor' ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo' 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' 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' ErrDockerForwardPolicyDrop: 'Este servidor restringe el reenvío de red. Por ahora no se puede habilitar la protección de puertos de contenedores.'
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'
Firewall: 'Firewall' Firewall: 'Firewall'
FirewallTaskHost: 'Cortafuegos del host' FirewallTaskHost: 'Cortafuegos del host'
FirewallTaskForwarding: 'Reenvío de puertos' FirewallTaskForwarding: 'Reenvío de puertos'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Crear reglas de cortafuegos'
FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar' FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas' FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas'
FirewallCreateRuleSkipped: 'Sin ejecutar' FirewallCreateRuleSkipped: 'Sin ejecutar'
FirewallCreateBatchStep: 'Enviar {{ .count }} reglas en un lote a {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido en {{ .count }} reglas' 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' FirewallCreateRuleExecutionFailed: 'Error al crear la regla. Los comandos ejecutados no se revirtieron'
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'
FirewallInitializeChainsStep: 'Inicializar y vincular las cadenas base de {{ .name }}' 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' FirewallSyncWhitelistStep: 'Sincronizar la lista de puertos permitidos del firewall'
FirewallEnableForwardingStep: 'Habilitar y vincular las cadenas de reenvío de puertos' 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' 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' 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 }}' FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker' 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 }}' FileTaskCopy: 'Copiar archivos a {{ .dst }}'
FileTaskMove: 'Mover archivos a {{ .dst }}' FileTaskMove: 'Mover archivos a {{ .dst }}'
FileTaskCompress: 'Comprimir archivos en {{ .dst }}' FileTaskCompress: 'Comprimir archivos en {{ .dst }}'
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Extraer archivos a {{ .dst }}'
FileTaskSource: 'Ruta de origen: {{ .path }}' FileTaskSource: 'Ruta de origen: {{ .path }}'
FileTaskFormat: 'Formato del archivo: {{ .format }}' FileTaskFormat: 'Formato del archivo: {{ .format }}'
FileTaskRename: 'Nombre del archivo de destino: {{ .name }}' 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 }}." 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." 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 }}" 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."
ErrIPv6RARisk: "El reenvío IPv6 está bloqueado para evitar una posible pérdida de conectividad. Consulte la documentación y los registros del sistema."
ErrIPv6RACheckFailed: "El reenvío IPv6 está bloqueado porque no se pudo comprobar la configuración de red. Consulte los registros del sistema."
ErrSSHPublicKey: "Introduce una clave pública SSH válida" 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" 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" ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
# máquina virtual
ErrVMNotFound: "La máquina virtual no existe"
ErrVMAlreadyExists: "La máquina virtual ya existe"
ErrVMCountLimit: "La edición Professional permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o actualiza a Enterprise y vuelve a intentarlo."
ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}"
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
ErrVMHelperNotFound: "No se encontró 1panel-kvm"
ErrVMBridgeManaged: "El nombre del bridge ya está administrado por libvirt o Docker"
ErrVMNetworkInUse: "La red está en uso y no se puede eliminar"
ErrVMStorageInUse: "El pool de almacenamiento está en uso y no se puede eliminar"
ErrVMStorageNameCannotChange: "El nombre del pool de almacenamiento no se puede cambiar"
ErrVMStoragePathCannotChange: "La ruta del pool de almacenamiento no se puede cambiar"
ErrVMRunning: "La máquina virtual está en ejecución"
ErrVMLibvirtConnect: "Error al conectar con libvirt: {{ .detail }}"
ErrVMPermission: "Sin permiso para operar la máquina virtual: {{ .detail }}"
ErrVMNetworkNotFound: "La red de la máquina virtual no existe: {{ .detail }}"
ErrVMInvalidPath: "Ruta de máquina virtual no válida: {{ .detail }}"
ErrVMInvalidInput: "Parámetro de máquina virtual no válido: {{ .detail }}"
ErrVMCommandFailed: "Error al ejecutar el comando de la máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "El disco no cumple los requisitos de ampliación o está referenciado por otro dispositivo. Compruebe su estado."
ErrVMDiskReferenced: "Otro equipo virtual o dispositivo utiliza este disco virtual; la ampliación aún no se admite."
ErrVMDiskReferenceCheck: "No se pueden comprobar las referencias al disco virtual. La ampliación no está disponible; revise los discos de otras máquinas virtuales."
ErrVMSnapshotMetadataSync: "La instantánea se restauró, pero falló la sincronización de los datos del disco. Actualice y compruebe; no restaure de nuevo la instantánea."
VMCreateTask: "Crear máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Crear máquina virtual [{{ .name }}] desde plantilla"
VMUpdateTask: "Actualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Ampliar disco [{{ .disk }}] de la máquina virtual [{{ .name }}]"
VMRenameTask: "Renombrar máquina virtual [{{ .name }}] a [{{ .newName }}]"
VMDeleteTask: "Eliminar máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Guardar máquina virtual [{{ .name }}] como plantilla [{{ .template }}]"
VMTemplateDeleteTask: "Eliminar plantilla de máquina virtual [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm no encontrado, eliminando solo los metadatos de la máquina virtual [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "Error al eliminar la máquina virtual [{{ .name }}] de libvirt, eliminando metadatos: {{ .err }}"
VMForceDeleteDiskFailed: "Error al forzar la eliminación del archivo de disco de la máquina virtual [{{ .path }}]: {{ .err }}"
VMForceDeleteDiskSuccess: "Archivo de disco de la máquina virtual [{{ .path }}] eliminado forzosamente"
VMSnapshotCreateTask: "Crear instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
VMSnapshotCreateStep: "Crear instantánea de máquina virtual [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Restaurar instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
VMSnapshotRecoverStep: "Restaurar instantánea de máquina virtual [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Eliminar instantánea [{{ .snapshot }}] de la máquina virtual [{{ .vm }}]"
VMSnapshotDeleteStep: "Eliminar instantánea de máquina virtual [{{ .snapshot }}]"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: 'کش ساخت'
Volume: 'حجم' Volume: 'حجم'
Network: "شبکه" Network: "شبکه"
PruneStart: 'پاک‌سازی در حال انجام است' PruneStart: 'پاک‌سازی در حال انجام است'
NetworkCleanupConnected: "از شبکه [{{ .name }}] ({{ .id }}) صرف‌نظر شد: کانتینر متصل دارد یا در حال استفاده است؛ حذف نشد"
NetworkCleanupSummary: "پاک‌سازی شبکه‌ها پایان یافت: حذف‌شده {{ .deleted }}، ردشده {{ .skipped }}، ناموفق {{ .failed }}"
NetworkCleanupPartialFailure: "پاک‌سازی برخی شبکه‌ها ناموفق بود؛ گزارش وظیفه را بررسی کنید"
ContainerBackupPrepare: 'آماده‌سازی فضای کاری پشتیبان کانتینر' ContainerBackupPrepare: 'آماده‌سازی فضای کاری پشتیبان کانتینر'
ContainerBackupStop: 'توقف کانتینر قبل از پشتیبان‌گیری' ContainerBackupStop: 'توقف کانتینر قبل از پشتیبان‌گیری'
ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان' ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان'
@@ -684,8 +687,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید' ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید'
ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بک‌اند فایروال nftables پشتیبانی کند، سپس داکر را راه‌اندازی مجدد کرده و دوباره تلاش کنید' ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بک‌اند فایروال nftables پشتیبانی کند، سپس داکر را راه‌اندازی مجدد کرده و دوباره تلاش کنید'
ErrDockerForwardPolicyDrop: 'سیاست پیش‌فرض FORWARD برای {{ .family }} برابر DROP است. آن را تغییر دهید و دوباره تلاش کنید' ErrDockerForwardPolicyDrop: 'این سرور هدایت ترافیک شبکه را محدود کرده است. در حال حاضر امکان فعال‌سازی محافظت از پورت کانتینرها وجود ندارد.'
ErrUFWRuleAdopt: 'مدیریت قانون UFW ناموفق بود: {{ .detail }}. اگر نسخه فعلی UFW قدیمی‌تر از 0.35 است، ابتدا UFW را ارتقا دهید و دوباره تلاش کنید'
Firewall: 'فایروال' Firewall: 'فایروال'
FirewallTaskHost: 'دیواره آتش میزبان' FirewallTaskHost: 'دیواره آتش میزبان'
FirewallTaskForwarding: 'هدایت پورت' FirewallTaskForwarding: 'هدایت پورت'
@@ -699,28 +701,15 @@ FirewallCreateRulesStep: 'ایجاد قوانین دیوار آتش'
FirewallCreateRulesResult: 'نتیجه ایجاد: {{ .succeeded }} موفق، {{ .failed }} ناموفق، {{ .skipped }} اجرا نشده' FirewallCreateRulesResult: 'نتیجه ایجاد: {{ .succeeded }} موفق، {{ .failed }} ناموفق، {{ .skipped }} اجرا نشده'
FirewallRuleOperationResult: 'نتیجه عملیات: {{ .succeeded }} موفق، {{ .failed }} ناموفق' FirewallRuleOperationResult: 'نتیجه عملیات: {{ .succeeded }} موفق، {{ .failed }} ناموفق'
FirewallCreateRuleSkipped: 'اجرا نشده' FirewallCreateRuleSkipped: 'اجرا نشده'
FirewallCreateBatchStep: 'ارسال {{ .count }} قانون به صورت دسته‌ای به {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}؛ به {{ .count }} قانون تبدیل شد' FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}؛ به {{ .count }} قانون تبدیل شد'
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند' FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. دستورات اجراشده بازگردانی نشدند'
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'
FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد'
FirewallInitializeChainsStep: 'راه‌اندازی و اتصال زنجیره‌های پایه {{ .name }}' FirewallInitializeChainsStep: 'راه‌اندازی و اتصال زنجیره‌های پایه {{ .name }}'
FirewallRestoreRulesStep: 'بازیابی قوانین پایگاه داده در {{ .name }}'
FirewallSyncWhitelistStep: 'همگام‌سازی فهرست مجاز پورت‌های فایروال' FirewallSyncWhitelistStep: 'همگام‌سازی فهرست مجاز پورت‌های فایروال'
FirewallEnableForwardingStep: 'فعال‌سازی و اتصال زنجیره‌های هدایت پورت' FirewallEnableForwardingStep: 'فعال‌سازی و اتصال زنجیره‌های هدایت پورت'
FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پورت پایگاه داده' FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پورت پایگاه داده'
FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاست‌ها' FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاست‌ها'
FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}' FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}'
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker' FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید."
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید"
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود"
FileTaskCopy: 'کپی فایل‌ها به {{ .dst }}' FileTaskCopy: 'کپی فایل‌ها به {{ .dst }}'
FileTaskMove: 'انتقال فایل‌ها به {{ .dst }}' FileTaskMove: 'انتقال فایل‌ها به {{ .dst }}'
FileTaskCompress: 'فشرده‌سازی فایل‌ها در {{ .dst }}' FileTaskCompress: 'فشرده‌سازی فایل‌ها در {{ .dst }}'
@@ -728,11 +717,56 @@ FileTaskDecompress: 'استخراج فایل‌ها در {{ .dst }}'
FileTaskSource: 'مسیر مبدأ: {{ .path }}' FileTaskSource: 'مسیر مبدأ: {{ .path }}'
FileTaskFormat: 'قالب بایگانی: {{ .format }}' FileTaskFormat: 'قالب بایگانی: {{ .format }}'
FileTaskRename: 'نام فایل مقصد: {{ .name }}' FileTaskRename: 'نام فایل مقصد: {{ .name }}'
ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید." ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید."
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید." ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}"
ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید."
ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخص‌شده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید."
ErrIPv6RARisk: "فعال‌سازی هدایت IPv6 به دلیل خطر قطع شبکه مسدود شد. مستندات و گزارش‌های سیستم را بررسی کنید."
ErrIPv6RACheckFailed: "فعال‌سازی هدایت IPv6 به دلیل عدم امکان بررسی پیکربندی شبکه مسدود شد. جزئیات در گزارش‌های سیستم موجود است."
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید" ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند" ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند"
ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند" ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند"
# ماشین مجازی
ErrVMNotFound: "ماشین مجازی وجود ندارد"
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
ErrVMCountLimit: "در نسخه Professional می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید."
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
ErrVMStoragePathInUse: "مسیر استخر ذخیره‌سازی قبلاً توسط [{{ .name }}] استفاده شده است"
ErrVMHelperNotFound: "1panel-kvm یافت نشد"
ErrVMBridgeManaged: "نام bridge از قبل توسط libvirt یا Docker مدیریت می‌شود"
ErrVMNetworkInUse: "شبکه در حال استفاده است و قابل حذف نیست"
ErrVMStorageInUse: "مخزن ذخیره‌سازی در حال استفاده است و قابل حذف نیست"
ErrVMStorageNameCannotChange: "نام مخزن ذخیره‌سازی قابل تغییر نیست"
ErrVMStoragePathCannotChange: "مسیر مخزن ذخیره‌سازی قابل تغییر نیست"
ErrVMRunning: "ماشین مجازی در حال اجرا است"
ErrVMLibvirtConnect: "اتصال به libvirt ناموفق بود: {{ .detail }}"
ErrVMPermission: "مجوز عملیات ماشین مجازی وجود ندارد: {{ .detail }}"
ErrVMNetworkNotFound: "شبکه ماشین مجازی وجود ندارد: {{ .detail }}"
ErrVMInvalidPath: "مسیر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMInvalidInput: "پارامتر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMCommandFailed: "اجرای دستور ماشین مجازی ناموفق بود: {{ .err }}"
ErrVMDiskResizeBlocked: "دیسک شرایط افزایش ظرفیت را ندارد یا دستگاه دیگری به آن ارجاع می‌دهد. وضعیت دیسک را بررسی کنید."
ErrVMDiskReferenced: "این دیسک مجازی توسط ماشین مجازی یا دستگاه دیگری استفاده می‌شود؛ افزایش ظرفیت فعلاً پشتیبانی نمی‌شود."
ErrVMDiskReferenceCheck: "بررسی ارجاع‌های دیسک مجازی کامل نشد. افزایش ظرفیت ممکن نیست؛ دیسک‌های سایر ماشین‌های مجازی را بررسی کنید."
ErrVMSnapshotMetadataSync: "اسنپ‌شات بازیابی شد، اما همگام‌سازی اطلاعات دیسک ناموفق بود. تازه‌سازی و بررسی کنید؛ نیازی به بازیابی دوباره اسنپ‌شات نیست."
VMCreateTask: "ایجاد ماشین مجازی [{{ .name }}]"
VMCreateFromTemplateTask: "ایجاد ماشین مجازی [{{ .name }}] از قالب"
VMUpdateTask: "به‌روزرسانی ماشین مجازی [{{ .name }}]"
VMDiskResizeTask: "افزایش ظرفیت دیسک [{{ .disk }}] ماشین مجازی [{{ .name }}]"
VMRenameTask: "تغییر نام ماشین مجازی [{{ .name }}] به [{{ .newName }}]"
VMDeleteTask: "حذف ماشین مجازی [{{ .name }}]"
VMTemplateCreateTask: "ذخیره ماشین مجازی [{{ .name }}] به عنوان قالب [{{ .template }}]"
VMTemplateDeleteTask: "حذف قالب ماشین مجازی [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm پیدا نشد، فقط فراداده ماشین مجازی [{{ .name }}] به اجبار حذف می‌شود"
VMForceDeleteMetadataAfterLibvirtFailed: "حذف ماشین مجازی [{{ .name }}] از libvirt ناموفق بود، فراداده به اجبار حذف می‌شود: {{ .err }}"
VMForceDeleteDiskFailed: "حذف اجباری فایل دیسک ماشین مجازی [{{ .path }}] ناموفق بود: {{ .err }}"
VMForceDeleteDiskSuccess: "فایل دیسک ماشین مجازی [{{ .path }}] به اجبار حذف شد"
VMSnapshotCreateTask: "ایجاد اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotCreateStep: "ایجاد اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
VMSnapshotRecoverTask: "بازیابی اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotRecoverStep: "بازیابی اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
VMSnapshotDeleteTask: "حذف اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotDeleteStep: "حذف اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: 'ビルドキャッシュ'
Volume: 'ストレージボリューム' Volume: 'ストレージボリューム'
Network: 'ネットワーク' Network: 'ネットワーク'
PruneStart: 'クリーンアップ中、しばらくお待ちください...' PruneStart: 'クリーンアップ中、しばらくお待ちください...'
NetworkCleanupConnected: "ネットワーク [{{ .name }}] ({{ .id }}) をスキップしました:コンテナが接続されているか使用中のため、削除しませんでした"
NetworkCleanupSummary: "ネットワークのクリーンアップが完了しました:削除 {{ .deleted }} 件、スキップ {{ .skipped }} 件、失敗 {{ .failed }} 件"
NetworkCleanupPartialFailure: "一部のネットワークをクリーンアップできませんでした。タスクログを確認してください"
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備' ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
ContainerBackupStop: 'バックアップ前にコンテナを停止' ContainerBackupStop: 'バックアップ前にコンテナを停止'
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ' ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください' ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください' ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD の既定ポリシーが DROP です。変更してから再試行してください' ErrDockerForwardPolicyDrop: 'このサーバーではネットワーク転送が制限されているため、現在コンテナのポート保護を有効にできません。'
ErrUFWRuleAdopt: 'UFW ルールの管理に失敗しました:{{ .detail }}。現在の UFW バージョンが 0.35 未満の場合は、UFW をアップグレードしてから再試行してください'
Firewall: 'ファイアウォール' Firewall: 'ファイアウォール'
FirewallTaskHost: 'ホストファイアウォール' FirewallTaskHost: 'ホストファイアウォール'
FirewallTaskForwarding: 'ポート転送' FirewallTaskForwarding: 'ポート転送'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'ファイアウォールルールを作成'
FirewallCreateRulesResult: '作成結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件' FirewallCreateRulesResult: '作成結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件' FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件'
FirewallCreateRuleSkipped: '未実行' FirewallCreateRuleSkipped: '未実行'
FirewallCreateBatchStep: '{{ .backend }} に {{ .count }} 件のルールを一括送信'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }};{{ .count }} 件のルールに変換' FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }};{{ .count }} 件のルールに変換'
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません' FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。実行済みのコマンドはロールバックしません'
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
FirewallSyncRuleUnchanged: '一致済み、変更不要'
FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました'
FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド' FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド'
FirewallRestoreRulesStep: 'データベースルールを {{ .name }} に復元'
FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期' FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期'
FirewallEnableForwardingStep: 'ポート転送チェーンを有効化してバインド' FirewallEnableForwardingStep: 'ポート転送チェーンを有効化してバインド'
FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルールを復元' FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルールを復元'
FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認' FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認'
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド' FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存' FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"
FileTaskCopy: 'ファイルを {{ .dst }} にコピー' FileTaskCopy: 'ファイルを {{ .dst }} にコピー'
FileTaskMove: 'ファイルを {{ .dst }} に移動' FileTaskMove: 'ファイルを {{ .dst }} に移動'
FileTaskCompress: 'ファイルを {{ .dst }} に圧縮' FileTaskCompress: 'ファイルを {{ .dst }} に圧縮'
@@ -727,11 +716,56 @@ FileTaskDecompress: 'ファイルを {{ .dst }} に展開'
FileTaskSource: '元のパス:{{ .path }}' FileTaskSource: '元のパス:{{ .path }}'
FileTaskFormat: '圧縮形式:{{ .format }}' FileTaskFormat: '圧縮形式:{{ .format }}'
FileTaskRename: '保存先ファイル名:{{ .name }}' FileTaskRename: '保存先ファイル名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。" ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。" ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}" ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。"
ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。"
ErrIPv6RARisk: "ネットワーク切断のおそれがあるため、IPv6 転送の有効化をブロックしました。ドキュメントとシステムログをご確認ください。"
ErrIPv6RACheckFailed: "ネットワーク設定を確認できないため、IPv6 転送の有効化をブロックしました。詳細はシステムログをご確認ください。"
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください" ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です" ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません" ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
# 仮想マシン
ErrVMNotFound: "仮想マシンが存在しません"
ErrVMAlreadyExists: "仮想マシンは既に存在します"
ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。"
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
ErrVMHelperNotFound: "1panel-kvm が見つかりません"
ErrVMBridgeManaged: "ブリッジ名はすでに libvirt または Docker によって管理されています"
ErrVMNetworkInUse: "ネットワークは使用中のため削除できません"
ErrVMStorageInUse: "ストレージプールは使用中のため削除できません"
ErrVMStorageNameCannotChange: "ストレージプール名は変更できません"
ErrVMStoragePathCannotChange: "ストレージプールのパスは変更できません"
ErrVMRunning: "仮想マシンは実行中です"
ErrVMLibvirtConnect: "libvirt への接続に失敗しました: {{ .detail }}"
ErrVMPermission: "仮想マシンを操作する権限がありません: {{ .detail }}"
ErrVMNetworkNotFound: "仮想マシンネットワークが存在しません: {{ .detail }}"
ErrVMInvalidPath: "仮想マシンのパスが無効です: {{ .detail }}"
ErrVMInvalidInput: "仮想マシンのパラメータが無効です: {{ .detail }}"
ErrVMCommandFailed: "仮想マシンコマンドの実行に失敗しました: {{ .err }}"
ErrVMDiskResizeBlocked: "ディスクが拡張条件を満たしていないか、他のデバイスから参照されています。ディスクの状態を確認してください。"
ErrVMDiskReferenced: "この仮想ディスクは他の仮想マシンまたはデバイスで使用されているため、現在は拡張できません。"
ErrVMDiskReferenceCheck: "仮想ディスクの参照を確認できないため拡張できません。他の仮想マシンのディスク状態を確認してください。"
ErrVMSnapshotMetadataSync: "スナップショットは復元されましたが、ディスク情報の同期に失敗しました。更新して確認してください。再度復元する必要はありません。"
VMCreateTask: "仮想マシン [{{ .name }}] を作成"
VMCreateFromTemplateTask: "テンプレートから仮想マシン [{{ .name }}] を作成"
VMUpdateTask: "仮想マシン [{{ .name }}] を更新"
VMDiskResizeTask: "仮想マシン [{{ .name }}] のディスク [{{ .disk }}] を拡張"
VMRenameTask: "仮想マシン [{{ .name }}] を [{{ .newName }}] にリネーム"
VMDeleteTask: "仮想マシン [{{ .name }}] を削除"
VMTemplateCreateTask: "仮想マシン [{{ .name }}] をテンプレート [{{ .template }}] として保存"
VMTemplateDeleteTask: "仮想マシンテンプレート [{{ .name }}] を削除"
VMForceDeleteMetadataOnly: "1panel-kvm が見つかりません。仮想マシン [{{ .name }}] のメタデータのみを強制削除します"
VMForceDeleteMetadataAfterLibvirtFailed: "libvirt から仮想マシン [{{ .name }}] を削除できませんでした。メタデータを強制削除します: {{ .err }}"
VMForceDeleteDiskFailed: "仮想マシンディスクファイル [{{ .path }}] の強制削除に失敗しました: {{ .err }}"
VMForceDeleteDiskSuccess: "仮想マシンディスクファイル [{{ .path }}] を強制削除しました"
VMSnapshotCreateTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を作成"
VMSnapshotCreateStep: "仮想マシンのスナップショット [{{ .snapshot }}] を作成"
VMSnapshotRecoverTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を復元"
VMSnapshotRecoverStep: "仮想マシンのスナップショット [{{ .snapshot }}] を復元"
VMSnapshotDeleteTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を削除"
VMSnapshotDeleteStep: "仮想マシンのスナップショット [{{ .snapshot }}] を削除"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: '빌드 캐시'
Volume: '스토리지 볼륨' Volume: '스토리지 볼륨'
Network: '네트워크' Network: '네트워크'
PruneStart: '정리 중입니다. 잠시만 기다려주세요...' PruneStart: '정리 중입니다. 잠시만 기다려주세요...'
NetworkCleanupConnected: "네트워크 [{{ .name }}] ({{ .id }}) 건너뜀: 연결된 컨테이너가 있거나 사용 중이므로 삭제하지 않음"
NetworkCleanupSummary: "네트워크 정리 완료: 삭제 {{ .deleted }}개, 건너뜀 {{ .skipped }}개, 실패 {{ .failed }}개"
NetworkCleanupPartialFailure: "일부 네트워크를 정리하지 못했습니다. 작업 로그를 확인하세요"
ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비' ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비'
ContainerBackupStop: '백업 전에 컨테이너 중지' ContainerBackupStop: '백업 전에 컨테이너 중지'
ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업' ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요' ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요' ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD 기본 정책이 DROP입니다. 정책을 조정한 후 다시 시도하세요' ErrDockerForwardPolicyDrop: '현재 서버에서 네트워크 전달이 제한되어 컨테이너 포트 보호를 활성화할 수 없습니다.'
ErrUFWRuleAdopt: 'UFW 규칙 관리에 실패했습니다: {{ .detail }}. 현재 UFW 버전이 0.35 미만이면 UFW를 업그레이드한 후 다시 시도하세요'
Firewall: '방화벽' Firewall: '방화벽'
FirewallTaskHost: '호스트 방화벽' FirewallTaskHost: '호스트 방화벽'
FirewallTaskForwarding: '포트 포워딩' FirewallTaskForwarding: '포트 포워딩'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: '방화벽 규칙 생성'
FirewallCreateRulesResult: '생성 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개' FirewallCreateRulesResult: '생성 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
FirewallRuleOperationResult: '작업 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개' FirewallRuleOperationResult: '작업 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개'
FirewallCreateRuleSkipped: '미실행' FirewallCreateRuleSkipped: '미실행'
FirewallCreateBatchStep: '{{ .backend }}에 규칙 {{ .count }}개 일괄 제출'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; 규칙 {{ .count }}개로 변환' FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; 규칙 {{ .count }}개로 변환'
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다' FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 실행된 명령은 롤백하지 않습니다'
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다'
FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩' FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩'
FirewallRestoreRulesStep: '데이터베이스 규칙을 {{ .name }}에 복원'
FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화' FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화'
FirewallEnableForwardingStep: '포트 전달 체인 활성화 및 바인딩' FirewallEnableForwardingStep: '포트 전달 체인 활성화 및 바인딩'
FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복원' FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복원'
FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인' FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인'
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩' FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장' FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"
FileTaskCopy: '{{ .dst }}에 파일 복사' FileTaskCopy: '{{ .dst }}에 파일 복사'
FileTaskMove: '{{ .dst }}로 파일 이동' FileTaskMove: '{{ .dst }}로 파일 이동'
FileTaskCompress: '{{ .dst }}에 파일 압축' FileTaskCompress: '{{ .dst }}에 파일 압축'
@@ -727,11 +716,56 @@ FileTaskDecompress: '{{ .dst }}에 압축 해제'
FileTaskSource: '원본 경로: {{ .path }}' FileTaskSource: '원본 경로: {{ .path }}'
FileTaskFormat: '압축 형식: {{ .format }}' FileTaskFormat: '압축 형식: {{ .format }}'
FileTaskRename: '대상 파일 이름: {{ .name }}' FileTaskRename: '대상 파일 이름: {{ .name }}'
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요." ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요." ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요."
ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요."
ErrIPv6RARisk: "네트워크 연결이 끊길 위험이 있어 IPv6 전달 활성화를 차단했습니다. 문서와 시스템 로그를 확인하세요."
ErrIPv6RACheckFailed: "네트워크 구성을 확인할 수 없어 IPv6 전달 활성화를 차단했습니다. 자세한 내용은 시스템 로그를 확인하세요."
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요" ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다" ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다" ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
# 가상 머신
ErrVMNotFound: "가상 머신이 존재하지 않습니다"
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요."
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
ErrVMBridgeManaged: "브리지 이름이 이미 libvirt 또는 Docker에서 관리되고 있습니다"
ErrVMNetworkInUse: "네트워크가 사용 중이므로 삭제할 수 없습니다"
ErrVMStorageInUse: "스토리지 풀이 사용 중이므로 삭제할 수 없습니다"
ErrVMStorageNameCannotChange: "스토리지 풀 이름은 변경할 수 없습니다"
ErrVMStoragePathCannotChange: "스토리지 풀 경로는 변경할 수 없습니다"
ErrVMRunning: "가상 머신이 실행 중입니다"
ErrVMLibvirtConnect: "libvirt 연결 실패: {{ .detail }}"
ErrVMPermission: "가상 머신 작업 권한이 없습니다: {{ .detail }}"
ErrVMNetworkNotFound: "가상 머신 네트워크가 존재하지 않습니다: {{ .detail }}"
ErrVMInvalidPath: "가상 머신 경로가 유효하지 않습니다: {{ .detail }}"
ErrVMInvalidInput: "가상 머신 매개변수가 유효하지 않습니다: {{ .detail }}"
ErrVMCommandFailed: "가상 머신 명령 실행 실패: {{ .err }}"
ErrVMDiskResizeBlocked: "디스크가 확장 조건을 충족하지 않거나 다른 장치에서 참조하고 있습니다. 디스크 상태를 확인하세요."
ErrVMDiskReferenced: "다른 가상 머신 또는 장치가 이 가상 디스크를 사용 중이므로 현재 확장할 수 없습니다."
ErrVMDiskReferenceCheck: "가상 디스크 참조를 확인할 수 없어 확장할 수 없습니다. 다른 가상 머신의 디스크 상태를 확인하세요."
ErrVMSnapshotMetadataSync: "스냅샷은 복원되었지만 디스크 정보 동기화에 실패했습니다. 새로 고쳐 확인하세요. 스냅샷을 다시 복원할 필요는 없습니다."
VMCreateTask: "가상 머신 [{{ .name }}] 생성"
VMCreateFromTemplateTask: "템플릿에서 가상 머신 [{{ .name }}] 생성"
VMUpdateTask: "가상 머신 [{{ .name }}] 업데이트"
VMDiskResizeTask: "가상 머신 [{{ .name }}]의 디스크 [{{ .disk }}] 확장"
VMRenameTask: "가상 머신 [{{ .name }}]을(를) [{{ .newName }}](으)로 이름 변경"
VMDeleteTask: "가상 머신 [{{ .name }}] 삭제"
VMTemplateCreateTask: "가상 머신 [{{ .name }}]을(를) 템플릿 [{{ .template }}](으)로 저장"
VMTemplateDeleteTask: "가상 머신 템플릿 [{{ .name }}] 삭제"
VMForceDeleteMetadataOnly: "1panel-kvm을 찾을 수 없어 가상 머신 [{{ .name }}] 메타데이터만 강제 삭제합니다"
VMForceDeleteMetadataAfterLibvirtFailed: "libvirt에서 가상 머신 [{{ .name }}] 삭제 실패, 메타데이터 강제 삭제: {{ .err }}"
VMForceDeleteDiskFailed: "가상 머신 디스크 파일 [{{ .path }}] 강제 삭제 실패: {{ .err }}"
VMForceDeleteDiskSuccess: "가상 머신 디스크 파일 [{{ .path }}]을(를) 강제 삭제했습니다"
VMSnapshotCreateTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 생성"
VMSnapshotCreateStep: "가상 머신 스냅샷 [{{ .snapshot }}] 생성"
VMSnapshotRecoverTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 복구"
VMSnapshotRecoverStep: "가상 머신 스냅샷 [{{ .snapshot }}] 복구"
VMSnapshotDeleteTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 삭제"
VMSnapshotDeleteStep: "가상 머신 스냅샷 [{{ .snapshot }}] 삭제"
+51 -17
View File
@@ -290,6 +290,9 @@ BuildCache: 'ແຄດການສ້າງ'
Volume: 'ວໍລຸມ (Volume)' Volume: 'ວໍລຸມ (Volume)'
Network: "ເຄືອຂ່າຍ" Network: "ເຄືອຂ່າຍ"
PruneStart: 'ກຳລັງດຳເນີນການທຳຄວາມສະອາດ' PruneStart: 'ກຳລັງດຳເນີນການທຳຄວາມສະອາດ'
NetworkCleanupConnected: "ຂ້າມເຄືອຂ່າຍ [{{ .name }}] ({{ .id }}): ມີຄອນເທນເນີເຊື່ອມຕໍ່ຢູ່ ຫຼື ເຄືອຂ່າຍກຳລັງຖືກໃຊ້ງານ; ບໍ່ໄດ້ລຶບ"
NetworkCleanupSummary: "ການລ້າງເຄືອຂ່າຍສຳເລັດ: ລຶບແລ້ວ {{ .deleted }}, ຂ້າມ {{ .skipped }}, ລົ້ມເຫຼວ {{ .failed }}"
NetworkCleanupPartialFailure: "ບໍ່ສາມາດລ້າງບາງເຄືອຂ່າຍໄດ້; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
ContainerBackupPrepare: 'ກຽມພື້ນທີ່ເຮັດວຽກສຳລັບສຳຮອງຂໍ້ມູນຄອນເທນເນີ' ContainerBackupPrepare: 'ກຽມພື້ນທີ່ເຮັດວຽກສຳລັບສຳຮອງຂໍ້ມູນຄອນເທນເນີ'
ContainerBackupStop: 'ຢຸດຄອນເທນເນີກ່ອນການສຳຮອງຂໍ້ມູນ' ContainerBackupStop: 'ຢຸດຄອນເທນເນີກ່ອນການສຳຮອງຂໍ້ມູນ'
ContainerBackupInspect: 'ກວດສອບຄອນເທນເນີ ແລະ ເຄືອຂ່າຍສຳລັບການສຳຮອງຂໍ້ມູນ' ContainerBackupInspect: 'ກວດສອບຄອນເທນເນີ ແລະ ເຄືອຂ່າຍສຳລັບການສຳຮອງຂໍ້ມູນ'
@@ -674,8 +677,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ' ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerNftablesChainUnavailable: 'ບໍ່ພົບ chain ໄຟວໍ nftables IPv4 ຂອງ Docker. ກະລຸນາກວດສອບວ່າ Docker ລຸ້ນປັດຈຸບັນຮອງຮັບ backend ໄຟວໍ nftables, ເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ' ErrDockerNftablesChainUnavailable: 'ບໍ່ພົບ chain ໄຟວໍ nftables IPv4 ຂອງ Docker. ກະລຸນາກວດສອບວ່າ Docker ລຸ້ນປັດຈຸບັນຮອງຮັບ backend ໄຟວໍ nftables, ເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerForwardPolicyDrop: 'ນະໂຍບາຍເລີ່ມຕົ້ນ FORWARD ຂອງ {{ .family }} ແມ່ນ DROP. ກະລຸນາປັບແລ້ວລອງອີກຄັ້ງ' ErrDockerForwardPolicyDrop: 'ເຊີບເວີນີ້ຈຳກັດການສົ່ງຕໍ່ເຄືອຂ່າຍ. ຍັງບໍ່ສາມາດເປີດໃຊ້ການປ້ອງກັນພອດຄອນເທນເນີໄດ້ໃນຕອນນີ້.'
ErrUFWRuleAdopt: 'ຈັດການກົດ UFW ບໍ່ສຳເລັດ: {{ .detail }}. ຖ້າ UFW ລຸ້ນປັດຈຸບັນຕ່ຳກວ່າ 0.35, ກະລຸນາອັບເກຣດ UFW ແລ້ວລອງໃໝ່'
Firewall: 'ໄຟວອລ' Firewall: 'ໄຟວອລ'
FirewallTaskHost: 'ໄຟວໍໂຮສ' FirewallTaskHost: 'ໄຟວໍໂຮສ'
FirewallTaskForwarding: 'ການສົ່ງຕໍ່ພອດ' FirewallTaskForwarding: 'ການສົ່ງຕໍ່ພອດ'
@@ -689,28 +691,15 @@ FirewallCreateRulesStep: 'ສ້າງກົດໄຟວໍ'
FirewallCreateRulesResult: 'ຜົນການສ້າງ: ສຳເລັດ {{ .succeeded }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}' FirewallCreateRulesResult: 'ຜົນການສ້າງ: ສຳເລັດ {{ .succeeded }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
FirewallRuleOperationResult: 'ຜົນການດຳເນີນການ: ສຳເລັດ {{ .succeeded }} ລາຍການ, ລົ້ມເຫຼວ {{ .failed }} ລາຍການ' FirewallRuleOperationResult: 'ຜົນການດຳເນີນການ: ສຳເລັດ {{ .succeeded }} ລາຍການ, ລົ້ມເຫຼວ {{ .failed }} ລາຍການ'
FirewallCreateRuleSkipped: 'ບໍ່ໄດ້ດຳເນີນການ' FirewallCreateRuleSkipped: 'ບໍ່ໄດ້ດຳເນີນການ'
FirewallCreateBatchStep: 'ສົ່ງ {{ .count }} ກົດເປັນຊຸດໄປຫາ {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ແປງເປັນ {{ .count }} ກົດ' FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ແປງເປັນ {{ .count }} ກົດ'
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ' FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ຄຳສັ່ງທີ່ດຳເນີນການແລ້ວບໍ່ໄດ້ຖືກຍ້ອນກັບ'
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ'
FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}' FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}'
FirewallRestoreRulesStep: 'ກູ້ຄືນກົດຖານຂໍ້ມູນໄປຫາ {{ .name }}'
FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ' FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ'
FirewallEnableForwardingStep: 'ເປີດໃຊ້ ແລະ ຜູກ chain ສົ່ງຕໍ່ພອດ' FirewallEnableForwardingStep: 'ເປີດໃຊ້ ແລະ ຜູກ chain ສົ່ງຕໍ່ພອດ'
FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕໍ່ພອດຈາກຖານຂໍ້ມູນ' FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕໍ່ພອດຈາກຖານຂໍ້ມູນ'
FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ' FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ'
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}' FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker' FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"
FileTaskCopy: 'ສຳເນົາໄຟລ໌ໄປທີ່ {{ .dst }}' FileTaskCopy: 'ສຳເນົາໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskMove: 'ຍ້າຍໄຟລ໌ໄປທີ່ {{ .dst }}' FileTaskMove: 'ຍ້າຍໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskCompress: 'ບີບອັດໄຟລ໌ໄປທີ່ {{ .dst }}' FileTaskCompress: 'ບີບອັດໄຟລ໌ໄປທີ່ {{ .dst }}'
@@ -718,11 +707,56 @@ FileTaskDecompress: 'ແຕກໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskSource: 'ເສັ້ນທາງຕົ້ນທາງ: {{ .path }}' FileTaskSource: 'ເສັ້ນທາງຕົ້ນທາງ: {{ .path }}'
FileTaskFormat: 'ຮູບແບບໄຟລ໌ບີບອັດ: {{ .format }}' FileTaskFormat: 'ຮູບແບບໄຟລ໌ບີບອັດ: {{ .format }}'
FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}' FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}." ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker." ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່."
ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່."
ErrIPv6RARisk: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເພື່ອປ້ອງກັນຄວາມສ່ຽງທີ່ເຄືອຂ່າຍຈະຂາດການເຊື່ອມຕໍ່. ເບິ່ງເອກະສານ ແລະ ບັນທຶກລະບົບ."
ErrIPv6RACheckFailed: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເນື່ອງຈາກບໍ່ສາມາດກວດສອບການຕັ້ງຄ່າເຄືອຂ່າຍໄດ້. ເບິ່ງລາຍລະອຽດໃນບັນທຶກລະບົບ."
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ" ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ" ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ" ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ"
# virtual machine
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ."
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
ErrVMBridgeManaged: "ຊື່ບຣິດຈ໌ຖືກຈັດການໂດຍ libvirt ຫຼື Docker ແລ້ວ"
ErrVMNetworkInUse: "ເຄືອຂ່າຍກຳລັງຖືກໃຊ້ງານ ແລະ ບໍ່ສາມາດລຶບໄດ້"
ErrVMStorageInUse: "ພູນຈັດເກັບກຳລັງຖືກໃຊ້ງານ ແລະ ບໍ່ສາມາດລຶບໄດ້"
ErrVMStorageNameCannotChange: "ບໍ່ສາມາດປ່ຽນຊື່ພູນຈັດເກັບໄດ້"
ErrVMStoragePathCannotChange: "ບໍ່ສາມາດປ່ຽນເສັ້ນທາງພູນຈັດເກັບໄດ້"
ErrVMRunning: "ເຄື່ອງສະເໝືອນກຳລັງເຮັດວຽກ"
ErrVMLibvirtConnect: "ເຊື່ອມຕໍ່ກັບ libvirt ບໍ່ສຳເລັດ: {{ .detail }}"
ErrVMPermission: "ບໍ່ມີສິດດຳເນີນການກັບເຄື່ອງສະເໝືອນ: {{ .detail }}"
ErrVMNetworkNotFound: "ບໍ່ມີເຄືອຂ່າຍເຄື່ອງສະເໝືອນ: {{ .detail }}"
ErrVMInvalidPath: "ເສັ້ນທາງເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMInvalidInput: "ພາລາມິເຕີເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMCommandFailed: "ຄຳສັ່ງເຄື່ອງສະເໝືອນລົ້ມເຫຼວ: {{ .err }}"
ErrVMDiskResizeBlocked: "ດິສກ໌ບໍ່ຕອບສະໜອງເງື່ອນໄຂການຂະຫຍາຍ ຫຼື ຖືກອ້າງອີງໂດຍອຸປະກອນອື່ນ. ກະລຸນາກວດສອບສະຖານະດິສກ໌."
ErrVMDiskReferenced: "ດິສກ໌ເສມືອນນີ້ຖືກໃຊ້ໂດຍເຄື່ອງເສມືອນ ຫຼື ອຸປະກອນອື່ນ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ."
ErrVMDiskReferenceCheck: "ບໍ່ສາມາດກວດສອບການອ້າງອີງດິສກ໌ເສມືອນໄດ້. ຍັງຂະຫຍາຍບໍ່ໄດ້; ກະລຸນາກວດສອບດິສກ໌ຂອງເຄື່ອງເສມືອນອື່ນ."
ErrVMSnapshotMetadataSync: "ກູ້ຄືນສະແນັບຊັອດແລ້ວ ແຕ່ຊິງຂໍ້ມູນດິສກ໌ບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໃໝ່ ແລະ ກວດສອບ; ບໍ່ຈຳເປັນຕ້ອງກູ້ຄືນສະແນັບຊັອດອີກ."
VMCreateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMCreateFromTemplateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກແມ່ແບບ"
VMUpdateTask: "ອັບເດດເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMDiskResizeTask: "ຂະຫຍາຍດິສກ໌ [{{ .disk }}] ຂອງເຄື່ອງເສມືອນ [{{ .name }}]"
VMRenameTask: "ປ່ຽນຊື່ເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນ [{{ .newName }}]"
VMDeleteTask: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMTemplateCreateTask: "ບັນທຶກເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນແມ່ແບບ [{{ .template }}]"
VMTemplateDeleteTask: "ລຶບແມ່ແບບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMForceDeleteMetadataOnly: "ບໍ່ພົບ 1panel-kvm, ຈະບັງຄັບລຶບສະເພາະເມຕາດາຕາຂອງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກ libvirt ບໍ່ສຳເລັດ, ຈະບັງຄັບລຶບເມຕາດາຕາ: {{ .err }}"
VMForceDeleteDiskFailed: "ບັງຄັບລຶບໄຟລ໌ດິສກ໌ເຄື່ອງສະເໝືອນ [{{ .path }}] ບໍ່ສຳເລັດ: {{ .err }}"
VMForceDeleteDiskSuccess: "ບັງຄັບລຶບໄຟລ໌ດິສກ໌ເຄື່ອງສະເໝືອນ [{{ .path }}] ສຳເລັດແລ້ວ"
VMSnapshotCreateTask: "ສ້າງສະແນັບຊັອດ [{{ .snapshot }}] ໃຫ້ເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotCreateStep: "ສ້າງສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
VMSnapshotRecoverTask: "ກູ້ຄືນສະແນັບຊັອດ [{{ .snapshot }}] ໃຫ້ເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotRecoverStep: "ກູ້ຄືນສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
VMSnapshotDeleteTask: "ລຶບສະແນັບຊັອດ [{{ .snapshot }}] ຂອງເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotDeleteStep: "ລຶບສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: 'Cache binaan'
Volume: 'Jilid storan' Volume: 'Jilid storan'
Network: 'Rangkaian' Network: 'Rangkaian'
PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...' PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...'
NetworkCleanupConnected: "Rangkaian [{{ .name }}] ({{ .id }}) dilangkau: terdapat kontena yang disambungkan atau rangkaian sedang digunakan; tidak dipadamkan"
NetworkCleanupSummary: "Pembersihan rangkaian selesai: dipadamkan {{ .deleted }}, dilangkau {{ .skipped }}, gagal {{ .failed }}"
NetworkCleanupPartialFailure: "Sesetengah rangkaian tidak dapat dibersihkan; semak log tugas"
ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena' ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena'
ContainerBackupStop: 'Hentikan kontena sebelum sandaran' ContainerBackupStop: 'Hentikan kontena sebelum sandaran'
ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena' ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi' 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' 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' ErrDockerForwardPolicyDrop: 'Pelayan ini mengehadkan pemajuan rangkaian. Perlindungan port kontena tidak dapat diaktifkan buat masa ini.'
ErrUFWRuleAdopt: 'Gagal mengurus peraturan UFW: {{ .detail }}. Jika versi UFW semasa lebih lama daripada 0.35, tingkatkan UFW dan cuba lagi'
Firewall: 'Firewall' Firewall: 'Firewall'
FirewallTaskHost: 'Tembok api hos' FirewallTaskHost: 'Tembok api hos'
FirewallTaskForwarding: 'Pemajuan port' FirewallTaskForwarding: 'Pemajuan port'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Cipta peraturan tembok api'
FirewallCreateRulesResult: 'Hasil penciptaan: {{ .succeeded }} berjaya, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan' FirewallCreateRulesResult: 'Hasil penciptaan: {{ .succeeded }} berjaya, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
FirewallRuleOperationResult: 'Hasil operasi: {{ .succeeded }} berjaya, {{ .failed }} gagal' FirewallRuleOperationResult: 'Hasil operasi: {{ .succeeded }} berjaya, {{ .failed }} gagal'
FirewallCreateRuleSkipped: 'Tidak dilaksanakan' FirewallCreateRuleSkipped: 'Tidak dilaksanakan'
FirewallCreateBatchStep: 'Hantar {{ .count }} peraturan secara kelompok ke {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ditukar kepada {{ .count }} peraturan' 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' FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. 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'
FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}' FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}'
FirewallRestoreRulesStep: 'Pulihkan peraturan pangkalan data ke {{ .name }}'
FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall' FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall'
FirewallEnableForwardingStep: 'Aktifkan dan ikat rantai pemajuan port' FirewallEnableForwardingStep: 'Aktifkan dan ikat rantai pemajuan port'
FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan data' FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan data'
FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi' FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}' FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker' 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 }}' FileTaskCopy: 'Salin fail ke {{ .dst }}'
FileTaskMove: 'Pindahkan fail ke {{ .dst }}' FileTaskMove: 'Pindahkan fail ke {{ .dst }}'
FileTaskCompress: 'Mampatkan fail ke {{ .dst }}' FileTaskCompress: 'Mampatkan fail ke {{ .dst }}'
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Ekstrak fail ke {{ .dst }}'
FileTaskSource: 'Laluan sumber: {{ .path }}' FileTaskSource: 'Laluan sumber: {{ .path }}'
FileTaskFormat: 'Format arkib: {{ .format }}' FileTaskFormat: 'Format arkib: {{ .format }}'
FileTaskRename: 'Nama fail sasaran: {{ .name }}' FileTaskRename: 'Nama fail sasaran: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}." 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." 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 }}" 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."
ErrIPv6RARisk: "Pemajuan IPv6 disekat bagi mengelakkan risiko kehilangan sambungan rangkaian. Rujuk dokumentasi dan log sistem."
ErrIPv6RACheckFailed: "Pemajuan IPv6 disekat kerana konfigurasi rangkaian tidak dapat diperiksa. Lihat log sistem untuk butiran."
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah" ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal" 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" ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan"
# mesin maya
ErrVMNotFound: "Mesin maya tidak wujud"
ErrVMAlreadyExists: "Mesin maya sudah wujud"
ErrVMCountLimit: "Edisi Professional membenarkan penciptaan sehingga {{ .limit }} mesin maya. Padamkan mesin maya sedia ada atau naik taraf kepada Enterprise dan cuba lagi."
ErrVMImageAlreadyExists: "Fail imej mesin maya sudah wujud: {{ .detail }}"
ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm tidak ditemui"
ErrVMBridgeManaged: "Nama bridge telah diurus oleh libvirt atau Docker"
ErrVMNetworkInUse: "Rangkaian sedang digunakan dan tidak boleh dipadamkan"
ErrVMStorageInUse: "Kumpulan storan sedang digunakan dan tidak boleh dipadamkan"
ErrVMStorageNameCannotChange: "Nama kumpulan storan tidak boleh diubah"
ErrVMStoragePathCannotChange: "Laluan kumpulan storan tidak boleh diubah"
ErrVMRunning: "Mesin maya sedang berjalan"
ErrVMLibvirtConnect: "Gagal menyambung ke libvirt: {{ .detail }}"
ErrVMPermission: "Tiada kebenaran untuk mengendalikan mesin maya: {{ .detail }}"
ErrVMNetworkNotFound: "Rangkaian mesin maya tidak wujud: {{ .detail }}"
ErrVMInvalidPath: "Laluan mesin maya tidak sah: {{ .detail }}"
ErrVMInvalidInput: "Parameter mesin maya tidak sah: {{ .detail }}"
ErrVMCommandFailed: "Arahan mesin maya gagal: {{ .err }}"
ErrVMDiskResizeBlocked: "Cakera tidak memenuhi syarat pembesaran atau dirujuk oleh peranti lain. Semak status cakera."
ErrVMDiskReferenced: "Cakera maya ini digunakan oleh mesin maya atau peranti lain; pembesaran belum disokong."
ErrVMDiskReferenceCheck: "Rujukan cakera maya tidak dapat disahkan. Pembesaran tidak tersedia; semak cakera mesin maya lain."
ErrVMSnapshotMetadataSync: "Syot kilat telah dipulihkan, tetapi maklumat cakera gagal disegerakkan. Muat semula dan semak; tidak perlu memulihkan syot kilat sekali lagi."
VMCreateTask: "Cipta mesin maya [{{ .name }}]"
VMCreateFromTemplateTask: "Cipta mesin maya [{{ .name }}] daripada templat"
VMUpdateTask: "Kemas kini mesin maya [{{ .name }}]"
VMDiskResizeTask: "Besarkan cakera [{{ .disk }}] mesin maya [{{ .name }}]"
VMRenameTask: "Namakan semula mesin maya [{{ .name }}] kepada [{{ .newName }}]"
VMDeleteTask: "Padam mesin maya [{{ .name }}]"
VMTemplateCreateTask: "Simpan mesin maya [{{ .name }}] sebagai templat [{{ .template }}]"
VMTemplateDeleteTask: "Padam templat mesin maya [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm tidak ditemui, memadam metadata mesin maya [{{ .name }}] sahaja secara paksa"
VMForceDeleteMetadataAfterLibvirtFailed: "Gagal memadam mesin maya [{{ .name }}] daripada libvirt, memadam metadata secara paksa: {{ .err }}"
VMForceDeleteDiskFailed: "Gagal memadam fail cakera mesin maya [{{ .path }}] secara paksa: {{ .err }}"
VMForceDeleteDiskSuccess: "Fail cakera mesin maya [{{ .path }}] telah dipadam secara paksa"
VMSnapshotCreateTask: "Cipta syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
VMSnapshotCreateStep: "Cipta syot kilat mesin maya [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Pulihkan syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
VMSnapshotRecoverStep: "Pulihkan syot kilat mesin maya [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Padam syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
VMSnapshotDeleteStep: "Padam syot kilat mesin maya [{{ .snapshot }}]"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: 'Cache de construção'
Volume: 'Volume de armazenamento' Volume: 'Volume de armazenamento'
Network: 'Rede' Network: 'Rede'
PruneStart: 'Limpando em andamento aguarde...' PruneStart: 'Limpando em andamento aguarde...'
NetworkCleanupConnected: "Rede [{{ .name }}] ({{ .id }}) ignorada: há contêineres conectados ou a rede está em uso; não foi excluída"
NetworkCleanupSummary: "Limpeza de redes concluída: excluídas {{ .deleted }}, ignoradas {{ .skipped }}, falhas {{ .failed }}"
NetworkCleanupPartialFailure: "Não foi possível limpar algumas redes; consulte o log da tarefa"
ContainerBackupPrepare: 'Preparar área de backup do contêiner' ContainerBackupPrepare: 'Preparar área de backup do contêiner'
ContainerBackupStop: 'Parar contêiner antes do backup' ContainerBackupStop: 'Parar contêiner antes do backup'
ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner' ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente' 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' 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' ErrDockerForwardPolicyDrop: 'Este servidor restringe o encaminhamento de rede. Não é possível ativar a proteção de portas de contêineres no momento.'
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'
Firewall: 'Firewall' Firewall: 'Firewall'
FirewallTaskHost: 'Firewall do host' FirewallTaskHost: 'Firewall do host'
FirewallTaskForwarding: 'Encaminhamento de portas' FirewallTaskForwarding: 'Encaminhamento de portas'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Criar regras de firewall'
FirewallCreateRulesResult: 'Resultado da criação: {{ .succeeded }} com sucesso, {{ .failed }} com falha, {{ .skipped }} não executadas' 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' FirewallRuleOperationResult: 'Resultado da operação: {{ .succeeded }} com sucesso, {{ .failed }} com falha'
FirewallCreateRuleSkipped: 'Não executada' FirewallCreateRuleSkipped: 'Não executada'
FirewallCreateBatchStep: 'Enviar {{ .count }} regras em lote para {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido em {{ .count }} regras' 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' FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. 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'
FirewallInitializeChainsStep: 'Inicializar e vincular as cadeias base do {{ .name }}' 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' FirewallSyncWhitelistStep: 'Sincronizar a lista de portas permitidas do firewall'
FirewallEnableForwardingStep: 'Habilitar e vincular as cadeias de encaminhamento de portas' FirewallEnableForwardingStep: 'Habilitar e vincular as cadeias de encaminhamento de portas'
FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de portas do banco de dados' FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de portas do banco de dados'
FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as políticas' FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as políticas'
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}' FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker' 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 }}' FileTaskCopy: 'Copiar arquivos para {{ .dst }}'
FileTaskMove: 'Mover arquivos para {{ .dst }}' FileTaskMove: 'Mover arquivos para {{ .dst }}'
FileTaskCompress: 'Compactar arquivos em {{ .dst }}' FileTaskCompress: 'Compactar arquivos em {{ .dst }}'
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Extrair arquivos para {{ .dst }}'
FileTaskSource: 'Caminho de origem: {{ .path }}' FileTaskSource: 'Caminho de origem: {{ .path }}'
FileTaskFormat: 'Formato do arquivo: {{ .format }}' FileTaskFormat: 'Formato do arquivo: {{ .format }}'
FileTaskRename: 'Nome do arquivo de destino: {{ .name }}' 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 }}." 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." 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 }}" 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."
ErrIPv6RARisk: "O encaminhamento IPv6 foi bloqueado para evitar uma possível perda de conexão. Consulte a documentação e os logs do sistema."
ErrIPv6RACheckFailed: "O encaminhamento IPv6 foi bloqueado porque não foi possível verificar a configuração de rede. Consulte os logs do sistema."
ErrSSHPublicKey: "Informe uma chave pública SSH válida" ErrSSHPublicKey: "Informe uma chave pública SSH válida"
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original" 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" ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem"
# máquina virtual
ErrVMNotFound: "Máquina virtual não existe"
ErrVMAlreadyExists: "Máquina virtual já existe"
ErrVMCountLimit: "A edição Professional permite criar até {{ .limit }} máquinas virtuais. Exclua uma máquina virtual existente ou atualize para Enterprise e tente novamente."
ErrVMImageAlreadyExists: "A imagem da máquina virtual já existe: {{ .detail }}"
ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm não encontrado"
ErrVMBridgeManaged: "O nome da bridge já é gerenciado pelo libvirt ou Docker"
ErrVMNetworkInUse: "A rede está em uso e não pode ser excluída"
ErrVMStorageInUse: "O pool de armazenamento está em uso e não pode ser excluído"
ErrVMStorageNameCannotChange: "O nome do pool de armazenamento não pode ser alterado"
ErrVMStoragePathCannotChange: "O caminho do pool de armazenamento não pode ser alterado"
ErrVMRunning: "Máquina virtual está em execução"
ErrVMLibvirtConnect: "Falha ao conectar ao libvirt: {{ .detail }}"
ErrVMPermission: "Sem permissão para operar a máquina virtual: {{ .detail }}"
ErrVMNetworkNotFound: "Rede da máquina virtual não existe: {{ .detail }}"
ErrVMInvalidPath: "Caminho da máquina virtual inválido: {{ .detail }}"
ErrVMInvalidInput: "Parâmetro da máquina virtual inválido: {{ .detail }}"
ErrVMCommandFailed: "Falha no comando da máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "O disco não atende aos requisitos de expansão ou é referenciado por outro dispositivo. Verifique seu estado."
ErrVMDiskReferenced: "Este disco virtual é usado por outra máquina virtual ou dispositivo e ainda não pode ser expandido."
ErrVMDiskReferenceCheck: "Não foi possível verificar as referências ao disco virtual. A expansão está indisponível; verifique os discos das outras máquinas virtuais."
ErrVMSnapshotMetadataSync: "O snapshot foi restaurado, mas a sincronização dos dados do disco falhou. Atualize e verifique; não restaure o snapshot novamente."
VMCreateTask: "Criar máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Criar máquina virtual [{{ .name }}] a partir do modelo"
VMUpdateTask: "Atualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Expandir disco [{{ .disk }}] da máquina virtual [{{ .name }}]"
VMRenameTask: "Renomear máquina virtual [{{ .name }}] para [{{ .newName }}]"
VMDeleteTask: "Excluir máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Salvar máquina virtual [{{ .name }}] como modelo [{{ .template }}]"
VMTemplateDeleteTask: "Excluir modelo de máquina virtual [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm não encontrado, excluindo à força apenas os metadados da máquina virtual [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "Falha ao excluir a máquina virtual [{{ .name }}] do libvirt, excluindo metadados à força: {{ .err }}"
VMForceDeleteDiskFailed: "Falha ao excluir à força o arquivo de disco da máquina virtual [{{ .path }}]: {{ .err }}"
VMForceDeleteDiskSuccess: "Arquivo de disco da máquina virtual [{{ .path }}] excluído à força"
VMSnapshotCreateTask: "Criar snapshot [{{ .snapshot }}] para a máquina virtual [{{ .vm }}]"
VMSnapshotCreateStep: "Criar snapshot da máquina virtual [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Restaurar snapshot [{{ .snapshot }}] da máquina virtual [{{ .vm }}]"
VMSnapshotRecoverStep: "Restaurar snapshot da máquina virtual [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Excluir snapshot [{{ .snapshot }}] da máquina virtual [{{ .vm }}]"
VMSnapshotDeleteStep: "Excluir snapshot da máquina virtual [{{ .snapshot }}]"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: 'Кэш сборки'
Volume: 'Том хранилища' Volume: 'Том хранилища'
Network: 'Сеть' Network: 'Сеть'
PruneStart: 'Очистка выполняется, пожалуйста, подождите...' PruneStart: 'Очистка выполняется, пожалуйста, подождите...'
NetworkCleanupConnected: "Сеть [{{ .name }}] ({{ .id }}) пропущена: подключены контейнеры или сеть используется; не удалена"
NetworkCleanupSummary: "Очистка сетей завершена: удалено {{ .deleted }}, пропущено {{ .skipped }}, ошибок {{ .failed }}"
NetworkCleanupPartialFailure: "Не удалось очистить некоторые сети; проверьте журнал задачи"
ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера' ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера'
ContainerBackupStop: 'Остановить контейнер перед резервным копированием' ContainerBackupStop: 'Остановить контейнер перед резервным копированием'
ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера' ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку' ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку' ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку'
ErrDockerForwardPolicyDrop: 'Политика FORWARD по умолчанию для {{ .family }} — DROP. Измените её и повторите попытку' ErrDockerForwardPolicyDrop: 'На этом сервере ограничена пересылка сетевого трафика. Сейчас невозможно включить защиту портов контейнеров.'
ErrUFWRuleAdopt: 'Не удалось принять правило UFW под управление: {{ .detail }}. Если текущая версия UFW ниже 0.35, обновите UFW и повторите попытку'
Firewall: 'Межсетевой экран' Firewall: 'Межсетевой экран'
FirewallTaskHost: 'Межсетевой экран хоста' FirewallTaskHost: 'Межсетевой экран хоста'
FirewallTaskForwarding: 'Перенаправление портов' FirewallTaskForwarding: 'Перенаправление портов'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Создание правил межсетевого
FirewallCreateRulesResult: 'Результат создания: успешно — {{ .succeeded }}, ошибок — {{ .failed }}, не выполнено — {{ .skipped }}' FirewallCreateRulesResult: 'Результат создания: успешно — {{ .succeeded }}, ошибок — {{ .failed }}, не выполнено — {{ .skipped }}'
FirewallRuleOperationResult: 'Результат операции: успешно — {{ .succeeded }}, ошибок — {{ .failed }}' FirewallRuleOperationResult: 'Результат операции: успешно — {{ .succeeded }}, ошибок — {{ .failed }}'
FirewallCreateRuleSkipped: 'Не выполнено' FirewallCreateRuleSkipped: 'Не выполнено'
FirewallCreateBatchStep: 'Отправка {{ .count }} правил одним пакетом в {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; преобразовано в {{ .count }} правил' FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; преобразовано в {{ .count }} правил'
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены' FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Выполненные команды не отменены'
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}'
FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}' FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}'
FirewallRestoreRulesStep: 'Восстановить правила базы данных в {{ .name }}'
FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана' FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана'
FirewallEnableForwardingStep: 'Включить и привязать цепочки перенаправления портов' FirewallEnableForwardingStep: 'Включить и привязать цепочки перенаправления портов'
FirewallRestoreForwardingRulesStep: 'Восстановить правила перенаправления портов из базы данных' FirewallRestoreForwardingRulesStep: 'Восстановить правила перенаправления портов из базы данных'
FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики' FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики'
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}' FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker' FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"
FileTaskCopy: 'Копирование файлов в {{ .dst }}' FileTaskCopy: 'Копирование файлов в {{ .dst }}'
FileTaskMove: 'Перемещение файлов в {{ .dst }}' FileTaskMove: 'Перемещение файлов в {{ .dst }}'
FileTaskCompress: 'Сжатие файлов в {{ .dst }}' FileTaskCompress: 'Сжатие файлов в {{ .dst }}'
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Распаковка файлов в {{ .dst }}'
FileTaskSource: 'Исходный путь: {{ .path }}' FileTaskSource: 'Исходный путь: {{ .path }}'
FileTaskFormat: 'Формат архива: {{ .format }}' FileTaskFormat: 'Формат архива: {{ .format }}'
FileTaskRename: 'Имя целевого файла: {{ .name }}' FileTaskRename: 'Имя целевого файла: {{ .name }}'
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}." ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1." ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило."
ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку."
ErrIPv6RARisk: "Включение переадресации IPv6 заблокировано из-за риска потери связи. См. документацию и системные журналы."
ErrIPv6RACheckFailed: "Включение переадресации IPv6 заблокировано: не удалось проверить настройки сети. Подробности в системных журналах."
ErrSSHPublicKey: "Введите действительный открытый ключ SSH" ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза" ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу" ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу"
# виртуальная машина
ErrVMNotFound: "Виртуальная машина не существует"
ErrVMAlreadyExists: "Виртуальная машина уже существует"
ErrVMCountLimit: "В редакции Professional можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на Enterprise и повторите попытку."
ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}"
ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm не найден"
ErrVMBridgeManaged: "Имя моста уже управляется libvirt или Docker"
ErrVMNetworkInUse: "Сеть используется и не может быть удалена"
ErrVMStorageInUse: "Пул хранилища используется и не может быть удалён"
ErrVMStorageNameCannotChange: "Имя пула хранилища нельзя изменить"
ErrVMStoragePathCannotChange: "Путь к пулу хранилища нельзя изменить"
ErrVMRunning: "Виртуальная машина запущена"
ErrVMLibvirtConnect: "Не удалось подключиться к libvirt: {{ .detail }}"
ErrVMPermission: "Нет разрешения на операцию с виртуальной машиной: {{ .detail }}"
ErrVMNetworkNotFound: "Сеть виртуальной машины не существует: {{ .detail }}"
ErrVMInvalidPath: "Недопустимый путь виртуальной машины: {{ .detail }}"
ErrVMInvalidInput: "Недопустимые параметры виртуальной машины: {{ .detail }}"
ErrVMCommandFailed: "Ошибка выполнения команды виртуальной машины: {{ .err }}"
ErrVMDiskResizeBlocked: "Диск не соответствует условиям расширения или используется другим устройством. Проверьте состояние диска."
ErrVMDiskReferenced: "Этот виртуальный диск используется другой виртуальной машиной или устройством; расширение пока недоступно."
ErrVMDiskReferenceCheck: "Не удалось проверить ссылки на виртуальный диск. Расширение недоступно; проверьте диски других виртуальных машин."
ErrVMSnapshotMetadataSync: "Снимок восстановлен, но синхронизация данных диска не удалась. Обновите данные и проверьте их; повторно восстанавливать снимок не нужно."
VMCreateTask: "Создать виртуальную машину [{{ .name }}]"
VMCreateFromTemplateTask: "Создать виртуальную машину [{{ .name }}] из шаблона"
VMUpdateTask: "Обновить виртуальную машину [{{ .name }}]"
VMDiskResizeTask: "Расширить диск [{{ .disk }}] виртуальной машины [{{ .name }}]"
VMRenameTask: "Переименовать виртуальную машину [{{ .name }}] в [{{ .newName }}]"
VMDeleteTask: "Удалить виртуальную машину [{{ .name }}]"
VMTemplateCreateTask: "Сохранить виртуальную машину [{{ .name }}] как шаблон [{{ .template }}]"
VMTemplateDeleteTask: "Удалить шаблон виртуальной машины [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm не найден, принудительно удаляются только метаданные виртуальной машины [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "Не удалось удалить виртуальную машину [{{ .name }}] из libvirt, принудительно удаляются метаданные: {{ .err }}"
VMForceDeleteDiskFailed: "Не удалось принудительно удалить файл диска виртуальной машины [{{ .path }}]: {{ .err }}"
VMForceDeleteDiskSuccess: "Файл диска виртуальной машины [{{ .path }}] принудительно удален"
VMSnapshotCreateTask: "Создать снимок [{{ .snapshot }}] для виртуальной машины [{{ .vm }}]"
VMSnapshotCreateStep: "Создать снимок виртуальной машины [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Восстановить снимок [{{ .snapshot }}] виртуальной машины [{{ .vm }}]"
VMSnapshotRecoverStep: "Восстановить снимок виртуальной машины [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Удалить снимок [{{ .snapshot }}] виртуальной машины [{{ .vm }}]"
VMSnapshotDeleteStep: "Удалить снимок виртуальной машины [{{ .snapshot }}]"
+51 -17
View File
@@ -300,6 +300,9 @@ BuildCache: 'Derleme önbelleği'
Volume: 'Depolama hacmi' Volume: 'Depolama hacmi'
Network: 'Ağ' Network: 'Ağ'
PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...' PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...'
NetworkCleanupConnected: "Ağ [{{ .name }}] ({{ .id }}) atlandı: bağlı konteynerler var veya ağ kullanımda; silinmedi"
NetworkCleanupSummary: "Ağ temizliği tamamlandı: silinen {{ .deleted }}, atlanan {{ .skipped }}, başarısız {{ .failed }}"
NetworkCleanupPartialFailure: "Bazı ağlar temizlenemedi; görev günlüğünü kontrol edin"
ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla' ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla'
ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur' ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur'
ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle' ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall # Firewall
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Docker’ı yeniden başlatıp tekrar deneyin' 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' 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' ErrDockerForwardPolicyDrop: 'Bu sunucu ağ yönlendirmesini kısıtlıyor. Konteyner port koruması şu anda etkinleştirilemiyor.'
ErrUFWRuleAdopt: 'UFW kuralı yönetilemedi: {{ .detail }}. Geçerli UFW sürümü 0.35’ten eskiyse UFW’yi yükseltip tekrar deneyin'
Firewall: 'Güvenlik duvarı' Firewall: 'Güvenlik duvarı'
FirewallTaskHost: 'Ana makine güvenlik duvarı' FirewallTaskHost: 'Ana makine güvenlik duvarı'
FirewallTaskForwarding: 'Port yönlendirme' FirewallTaskForwarding: 'Port yönlendirme'
@@ -698,28 +700,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' 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' FirewallRuleOperationResult: 'İşlem sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız'
FirewallCreateRuleSkipped: 'Yürütülmedi' 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ü' 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ı' FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. 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'
FirewallInitializeChainsStep: '{{ .name }} temel zincirlerini başlat ve bağla' 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' FirewallSyncWhitelistStep: 'Güvenlik duvarı bağlantı noktası izin listesini eşitle'
FirewallEnableForwardingStep: 'Bağlantı noktası yönlendirme zincirlerini etkinleştir ve bağla' FirewallEnableForwardingStep: 'Bağlantı noktası yönlendirme zincirlerini etkinleştir ve bağla'
FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirme kurallarını geri yükle' FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirme kurallarını geri yükle'
FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkelerini denetle' FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkelerini denetle'
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla' FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet' 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' FileTaskCopy: 'Dosyaları {{ .dst }} konumuna kopyala'
FileTaskMove: 'Dosyaları {{ .dst }} konumuna taşı' FileTaskMove: 'Dosyaları {{ .dst }} konumuna taşı'
FileTaskCompress: 'Dosyaları {{ .dst }} konumuna sıkıştır' FileTaskCompress: 'Dosyaları {{ .dst }} konumuna sıkıştır'
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Dosyaları {{ .dst }} konumuna çıkar'
FileTaskSource: 'Kaynak yol: {{ .path }}' FileTaskSource: 'Kaynak yol: {{ .path }}'
FileTaskFormat: 'Arşiv biçimi: {{ .format }}' FileTaskFormat: 'Arşiv biçimi: {{ .format }}'
FileTaskRename: 'Hedef dosya adı: {{ .name }}' FileTaskRename: 'Hedef dosya adı: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin." 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." 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 }}" 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."
ErrIPv6RARisk: "Ağ bağlantısının kesilme riski nedeniyle IPv6 yönlendirmesi engellendi. Belgeleri ve sistem günlüklerini inceleyin."
ErrIPv6RACheckFailed: "Ağ yapılandırması denetlenemediği için IPv6 yönlendirmesi engellendi. Ayrıntılar için sistem günlüklerini inceleyin."
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin" ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir" ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor" ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor"
# sanal makine
ErrVMNotFound: "Sanal makine mevcut değil"
ErrVMAlreadyExists: "Sanal makine zaten mevcut"
ErrVMCountLimit: "Professional sürümü en fazla {{ .limit }} sanal makine oluşturulmasına izin verir. Mevcut bir sanal makineyi silin veya Enterprise sürümüne yükseltip tekrar deneyin."
ErrVMImageAlreadyExists: "Sanal makine imaj dosyası zaten mevcut: {{ .detail }}"
ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor"
ErrVMHelperNotFound: "1panel-kvm bulunamadı"
ErrVMBridgeManaged: "Bridge adı zaten libvirt veya Docker tarafından yönetiliyor"
ErrVMNetworkInUse: "Ağ kullanımda ve silinemez"
ErrVMStorageInUse: "Depolama havuzu kullanımda ve silinemez"
ErrVMStorageNameCannotChange: "Depolama havuzunun adı değiştirilemez"
ErrVMStoragePathCannotChange: "Depolama havuzunun yolu değiştirilemez"
ErrVMRunning: "Sanal makine çalışıyor"
ErrVMLibvirtConnect: "libvirt bağlantısı başarısız: {{ .detail }}"
ErrVMPermission: "Sanal makine işlemi için izin yok: {{ .detail }}"
ErrVMNetworkNotFound: "Sanal makine ağı mevcut değil: {{ .detail }}"
ErrVMInvalidPath: "Geçersiz sanal makine yolu: {{ .detail }}"
ErrVMInvalidInput: "Geçersiz sanal makine parametresi: {{ .detail }}"
ErrVMCommandFailed: "Sanal makine komutu başarısız: {{ .err }}"
ErrVMDiskResizeBlocked: "Disk genişletme koşullarını karşılamıyor veya başka bir aygıt tarafından kullanılıyor. Disk durumunu kontrol edin."
ErrVMDiskReferenced: "Bu sanal disk başka bir sanal makine veya aygıt tarafından kullanılıyor; genişletme henüz desteklenmiyor."
ErrVMDiskReferenceCheck: "Sanal disk başvuruları doğrulanamadı. Genişletme kullanılamıyor; diğer sanal makinelerin disklerini kontrol edin."
ErrVMSnapshotMetadataSync: "Anlık görüntü geri yüklendi ancak disk bilgileri eşitlenemedi. Yenileyip kontrol edin; anlık görüntüyü tekrar geri yüklemeyin."
VMCreateTask: "Sanal makine [{{ .name }}] oluştur"
VMCreateFromTemplateTask: "Şablondan sanal makine [{{ .name }}] oluştur"
VMUpdateTask: "Sanal makine [{{ .name }}] güncelle"
VMDiskResizeTask: "[{{ .name }}] sanal makinesinin [{{ .disk }}] diskini genişlet"
VMRenameTask: "Sanal makine [{{ .name }}] adını [{{ .newName }}] olarak değiştir"
VMDeleteTask: "Sanal makine [{{ .name }}] sil"
VMTemplateCreateTask: "Sanal makine [{{ .name }}] öğesini şablon [{{ .template }}] olarak kaydet"
VMTemplateDeleteTask: "Sanal makine şablonu [{{ .name }}] sil"
VMForceDeleteMetadataOnly: "1panel-kvm bulunamadı, yalnızca sanal makine [{{ .name }}] meta verileri zorla siliniyor"
VMForceDeleteMetadataAfterLibvirtFailed: "Sanal makine [{{ .name }}] libvirt üzerinden silinemedi, meta veriler zorla siliniyor: {{ .err }}"
VMForceDeleteDiskFailed: "Sanal makine disk dosyası [{{ .path }}] zorla silinemedi: {{ .err }}"
VMForceDeleteDiskSuccess: "Sanal makine disk dosyası [{{ .path }}] zorla silindi"
VMSnapshotCreateTask: "Sanal makine [{{ .vm }}] için anlık görüntü [{{ .snapshot }}] oluştur"
VMSnapshotCreateStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] oluştur"
VMSnapshotRecoverTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snapshot }}] geri yükle"
VMSnapshotRecoverStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] geri yükle"
VMSnapshotDeleteTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snapshot }}] sil"
VMSnapshotDeleteStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] sil"
+50 -26
View File
@@ -300,6 +300,9 @@ BuildCache: '構建快取'
Volume: '磁碟區' Volume: '磁碟區'
Network: '網路' Network: '網路'
PruneStart: '清理中,請稍候...' PruneStart: '清理中,請稍候...'
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
ContainerBackupPrepare: '準備容器備份工作目錄' ContainerBackupPrepare: '準備容器備份工作目錄'
ContainerBackupStop: '備份前停止容器' ContainerBackupStop: '備份前停止容器'
ContainerBackupInspect: '備份容器 Inspect 與網路資訊' ContainerBackupInspect: '備份容器 Inspect 與網路資訊'
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: '火山方舟 Coding Plan'
# 防火牆 # 防火牆
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試' ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試' ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試'
ErrDockerForwardPolicyDrop: '偵測到 {{ .family }} FORWARD 預設策略為 DROP,請調整後重試' ErrDockerForwardPolicyDrop: '目前伺服器限制了網路轉送,暫時無法啟用容器連接埠防護。'
ErrUFWRuleAdopt: 'UFW 規則納管失敗:{{ .detail }}。如果目前 UFW 版本低於 0.35,請先升級 UFW 後再試'
Firewall: '防火牆' Firewall: '防火牆'
FirewallTaskHost: '主機防火牆' FirewallTaskHost: '主機防火牆'
FirewallTaskForwarding: '連接埠轉發' FirewallTaskForwarding: '連接埠轉發'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: '建立防火牆規則'
FirewallCreateRulesResult: '建立結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條' FirewallCreateRulesResult: '建立結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條' FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條'
FirewallCreateRuleSkipped: '未執行' FirewallCreateRuleSkipped: '未執行'
FirewallCreateBatchStep: '向 {{ .backend }} 批次提交 {{ .count }} 條規則'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};轉換為 {{ .count }} 條規則' FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};轉換為 {{ .count }} 條規則'
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復' FirewallCreateRuleExecutionFailed: '規則建立失敗,已執行的命令不回復'
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
FirewallSyncRuleUnchanged: '已一致,無需變更'
FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則'
FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈' FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈'
FirewallRestoreRulesStep: '恢復資料庫規則至 {{ .name }}'
FirewallSyncWhitelistStep: '同步防火牆連接埠白名單' FirewallSyncWhitelistStep: '同步防火牆連接埠白名單'
FirewallEnableForwardingStep: '啟用並綁定連接埠轉發鏈' FirewallEnableForwardingStep: '啟用並綁定連接埠轉發鏈'
FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則' FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則'
FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略' FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈' FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態' FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"
FileTaskCopy: '複製檔案至 {{ .dst }}' FileTaskCopy: '複製檔案至 {{ .dst }}'
FileTaskMove: '移動檔案至 {{ .dst }}' FileTaskMove: '移動檔案至 {{ .dst }}'
FileTaskCompress: '壓縮檔案至 {{ .dst }}' FileTaskCompress: '壓縮檔案至 {{ .dst }}'
@@ -727,21 +716,56 @@ FileTaskDecompress: '解壓檔案至 {{ .dst }}'
FileTaskSource: '來源路徑:{{ .path }}' FileTaskSource: '來源路徑:{{ .path }}'
FileTaskFormat: '壓縮格式:{{ .format }}' FileTaskFormat: '壓縮格式:{{ .format }}'
FileTaskRename: '目標檔名:{{ .name }}' FileTaskRename: '目標檔名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。" ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。" ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}" ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。"
ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。"
ErrIPv6RARisk: "IPv6 轉送可能導致網路中斷,已阻止啟用。請參閱文件,詳情見系統日誌。"
ErrIPv6RACheckFailed: "無法檢查 IPv6 網路設定,已阻止啟用轉送。詳情見系統日誌。"
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰" ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼" ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符" ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
NetworkCleanupDeleted: "網路 [{{ .name }}] ({{ .id }}) 已刪除" # 虛擬機
NetworkCleanupProtected: "跳過網路 [{{ .name }}] ({{ .id }}):預設保留網路,未刪除" ErrVMNotFound: "虛擬機不存在"
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除" ErrVMAlreadyExists: "虛擬機已存在"
NetworkCleanupUnsupported: "跳過網路 [{{ .name }}] ({{ .id }}):特殊網路,未刪除" ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。"
NetworkCleanupGone: "跳過網路 [{{ .name }}] ({{ .id }}):網路已不存在" ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}"
NetworkCleanupInspectFailed: "網路 [{{ .name }}] ({{ .id }}) 檢查失敗,未刪除" ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用"
NetworkCleanupRemoveFailed: "網路 [{{ .name }}] ({{ .id }}) 刪除失敗" ErrVMHelperNotFound: "1panel-kvm 不存在"
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}" ErrVMBridgeManaged: "橋接名稱已被 libvirt 或 Docker 管理"
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌" ErrVMNetworkInUse: "網路正在被使用,無法刪除"
ErrVMStorageInUse: "儲存池正在被使用,無法刪除"
ErrVMStorageNameCannotChange: "儲存池名稱無法修改"
ErrVMStoragePathCannotChange: "儲存池路徑無法修改"
ErrVMRunning: "虛擬機正在執行"
ErrVMLibvirtConnect: "連接 libvirt 失敗: {{ .detail }}"
ErrVMPermission: "沒有虛擬機操作權限: {{ .detail }}"
ErrVMNetworkNotFound: "虛擬機網路不存在: {{ .detail }}"
ErrVMInvalidPath: "虛擬機路徑非法: {{ .detail }}"
ErrVMInvalidInput: "虛擬機參數非法: {{ .detail }}"
ErrVMCommandFailed: "虛擬機指令執行失敗: {{ .err }}"
ErrVMDiskResizeBlocked: "磁碟不符合擴容條件,或被其他裝置引用,請檢查磁碟狀態"
ErrVMDiskReferenced: "此虛擬硬碟被其他虛擬機或裝置使用,暫不支援擴容"
ErrVMDiskReferenceCheck: "無法完成虛擬硬碟引用檢查,暫不支援擴容,請檢查其他虛擬機的磁碟狀態"
ErrVMSnapshotMetadataSync: "快照已還原,但磁碟資訊同步失敗,請重新整理檢查,無需重複還原快照"
VMCreateTask: "建立虛擬機 [{{ .name }}]"
VMCreateFromTemplateTask: "從範本建立虛擬機 [{{ .name }}]"
VMUpdateTask: "更新虛擬機 [{{ .name }}]"
VMDiskResizeTask: "擴容虛擬機 [{{ .name }}] 磁碟 [{{ .disk }}]"
VMRenameTask: "重新命名虛擬機 [{{ .name }}] 為 [{{ .newName }}]"
VMDeleteTask: "刪除虛擬機 [{{ .name }}]"
VMTemplateCreateTask: "轉存虛擬機 [{{ .name }}] 為範本 [{{ .template }}]"
VMTemplateDeleteTask: "刪除虛擬機範本 [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm 不存在,僅強制刪除虛擬機 [{{ .name }}] 元資料"
VMForceDeleteMetadataAfterLibvirtFailed: "從 libvirt 刪除虛擬機 [{{ .name }}] 失敗,強制刪除元資料: {{ .err }}"
VMForceDeleteDiskFailed: "強制刪除虛擬機磁碟檔案 [{{ .path }}] 失敗: {{ .err }}"
VMForceDeleteDiskSuccess: "已強制刪除虛擬機磁碟檔案 [{{ .path }}]"
VMSnapshotCreateTask: "建立虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotCreateStep: "建立虛擬機快照 [{{ .snapshot }}]"
VMSnapshotRecoverTask: "復原虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotRecoverStep: "復原虛擬機快照 [{{ .snapshot }}]"
VMSnapshotDeleteTask: "刪除虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotDeleteStep: "刪除虛擬機快照 [{{ .snapshot }}]"
+50 -26
View File
@@ -300,6 +300,9 @@ BuildCache: "构建缓存"
Volume: "存储卷" Volume: "存储卷"
Network: "网络" Network: "网络"
PruneStart: "清理中,请稍候..." PruneStart: "清理中,请稍候..."
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
ContainerBackupPrepare: "准备容器备份工作目录" ContainerBackupPrepare: "准备容器备份工作目录"
ContainerBackupStop: "备份前停止容器" ContainerBackupStop: "备份前停止容器"
ContainerBackupInspect: "备份容器 Inspect 与网络信息" ContainerBackupInspect: "备份容器 Inspect 与网络信息"
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: "火山方舟 Coding Plan"
# 防火墙 # 防火墙
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试" ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试" ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试"
ErrDockerForwardPolicyDrop: '检测到 {{ .family }} FORWARD 默认策略为 DROP,请调整后重试' ErrDockerForwardPolicyDrop: '当前服务器限制了网络转发,暂时无法启用容器端口防护。'
ErrUFWRuleAdopt: "UFW 规则纳管失败:{{ .detail }}。如果当前 UFW 版本低于 0.35,请先升级 UFW 后重试"
Firewall: "防火墙" Firewall: "防火墙"
FirewallTaskHost: '主机防火墙' FirewallTaskHost: '主机防火墙'
FirewallTaskForwarding: '端口转发' FirewallTaskForwarding: '端口转发'
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: '创建防火墙规则'
FirewallCreateRulesResult: '创建结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条' FirewallCreateRulesResult: '创建结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
FirewallRuleOperationResult: '操作结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条' FirewallRuleOperationResult: '操作结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条'
FirewallCreateRuleSkipped: '未执行' FirewallCreateRuleSkipped: '未执行'
FirewallCreateBatchStep: '向 {{ .backend }} 批量提交 {{ .count }} 条规则'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};转换为 {{ .count }} 条规则' FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};转换为 {{ .count }} 条规则'
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚' FirewallCreateRuleExecutionFailed: '规则创建失败,已执行的命令不回滚'
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
FirewallSyncRuleUnchanged: '已一致,无需变更'
FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则"
FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链" FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链"
FirewallRestoreRulesStep: "恢复数据库规则到 {{ .name }}"
FirewallSyncWhitelistStep: "同步防火墙端口白名单" FirewallSyncWhitelistStep: "同步防火墙端口白名单"
FirewallEnableForwardingStep: "启用并绑定端口转发链" FirewallEnableForwardingStep: "启用并绑定端口转发链"
FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则" FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则"
FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略" FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链" FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态" FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"
FileTaskCopy: '复制文件到 {{ .dst }}' FileTaskCopy: '复制文件到 {{ .dst }}'
FileTaskMove: '移动文件到 {{ .dst }}' FileTaskMove: '移动文件到 {{ .dst }}'
FileTaskCompress: '压缩文件到 {{ .dst }}' FileTaskCompress: '压缩文件到 {{ .dst }}'
@@ -727,21 +716,56 @@ FileTaskDecompress: '解压文件到 {{ .dst }}'
FileTaskSource: '源路径:{{ .path }}' FileTaskSource: '源路径:{{ .path }}'
FileTaskFormat: '压缩格式:{{ .format }}' FileTaskFormat: '压缩格式:{{ .format }}'
FileTaskRename: '目标文件名:{{ .name }}' FileTaskRename: '目标文件名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。" ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。" ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}" ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。"
ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。"
ErrIPv6RARisk: "IPv6 转发存在断网风险,已阻止启用。请参阅文档,详情见系统日志。"
ErrIPv6RACheckFailed: "无法检查 IPv6 网络配置,已阻止启用转发。详情见系统日志。"
ErrSSHPublicKey: "请输入有效的 SSH 公钥" ErrSSHPublicKey: "请输入有效的 SSH 公钥"
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码" ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配" ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
NetworkCleanupDeleted: "网络 [{{ .name }}] ({{ .id }}) 已删除" # 虚拟机
NetworkCleanupProtected: "跳过网络 [{{ .name }}] ({{ .id }}):默认保留网络,未删除" ErrVMNotFound: "虚拟机不存在"
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除" ErrVMAlreadyExists: "虚拟机已存在"
NetworkCleanupUnsupported: "跳过网络 [{{ .name }}] ({{ .id }}):特殊网络,未删除" ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。"
NetworkCleanupGone: "跳过网络 [{{ .name }}] ({{ .id }}):网络已不存在" ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}"
NetworkCleanupInspectFailed: "网络 [{{ .name }}] ({{ .id }}) 检查失败,未删除" ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用"
NetworkCleanupRemoveFailed: "网络 [{{ .name }}] ({{ .id }}) 删除失败" ErrVMHelperNotFound: "1panel-kvm 不存在"
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}" ErrVMBridgeManaged: "桥接名称已被 libvirt 或 Docker 管理"
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志" ErrVMNetworkInUse: "网络正在被使用,无法删除"
ErrVMStorageInUse: "存储池正在被使用,无法删除"
ErrVMStorageNameCannotChange: "存储池名称无法修改"
ErrVMStoragePathCannotChange: "存储池路径无法修改"
ErrVMRunning: "虚拟机正在运行"
ErrVMLibvirtConnect: "连接 libvirt 失败: {{ .detail }}"
ErrVMPermission: "没有虚拟机操作权限: {{ .detail }}"
ErrVMNetworkNotFound: "虚拟机网络不存在: {{ .detail }}"
ErrVMInvalidPath: "虚拟机路径非法: {{ .detail }}"
ErrVMInvalidInput: "虚拟机参数非法: {{ .detail }}"
ErrVMCommandFailed: "虚拟机命令执行失败: {{ .err }}"
ErrVMDiskResizeBlocked: "磁盘不符合扩容条件,或被其他设备引用,请检查磁盘状态"
ErrVMDiskReferenced: "该虚拟硬盘被其他虚拟机或设备使用,暂不支持扩容"
ErrVMDiskReferenceCheck: "无法完成虚拟硬盘引用检查,暂不支持扩容,请检查其他虚拟机的磁盘状态"
ErrVMSnapshotMetadataSync: "快照已恢复,但磁盘信息同步失败,请刷新检查,无需重复恢复快照"
VMCreateTask: "创建虚拟机 [{{ .name }}]"
VMCreateFromTemplateTask: "从模板创建虚拟机 [{{ .name }}]"
VMUpdateTask: "更新虚拟机 [{{ .name }}]"
VMDiskResizeTask: "扩容虚拟机 [{{ .name }}] 磁盘 [{{ .disk }}]"
VMRenameTask: "重命名虚拟机 [{{ .name }}] 为 [{{ .newName }}]"
VMDeleteTask: "删除虚拟机 [{{ .name }}]"
VMTemplateCreateTask: "转存虚拟机 [{{ .name }}] 为模板 [{{ .template }}]"
VMTemplateDeleteTask: "删除虚拟机模板 [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm 不存在,仅强制删除虚拟机 [{{ .name }}] 元数据"
VMForceDeleteMetadataAfterLibvirtFailed: "从 libvirt 删除虚拟机 [{{ .name }}] 失败,强制删除元数据: {{ .err }}"
VMForceDeleteDiskFailed: "强制删除虚拟机磁盘文件 [{{ .path }}] 失败: {{ .err }}"
VMForceDeleteDiskSuccess: "已强制删除虚拟机磁盘文件 [{{ .path }}]"
VMSnapshotCreateTask: "创建虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotCreateStep: "创建虚拟机快照 [{{ .snapshot }}]"
VMSnapshotRecoverTask: "恢复虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotRecoverStep: "恢复虚拟机快照 [{{ .snapshot }}]"
VMSnapshotDeleteTask: "删除虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotDeleteStep: "删除虚拟机快照 [{{ .snapshot }}]"
+23 -22
View File
@@ -6,20 +6,26 @@ import (
"os" "os"
"time" "time"
"errors"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations" "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"
"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/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"gorm.io/gorm"
) )
func Init() { func Init() {
ctx := context.Background() 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() client, err := service.NewSelectedSystemFirewallClient()
if err != nil { if err != nil {
global.LOG.Errorf("select system firewall provider failed, err: %v", err) 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 { if err := migrations.TransferFirewalldSSHService(ctx, client, service.NewIFirewallService().SyncPortWhitelist); err != nil {
global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err) global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err)
} }
if initialize {
initDockerPortGuard(ctx)
}
}() }()
if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil { families := []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err) 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 return
} }
if err := migrationutils.TransferLegacyHostFirewallRuleOwnership(ctx, clientName, service.AdoptLegacyHostFirewallRuleOwnership); err != nil { if ipv6Status == constant.StatusDisable {
global.LOG.Warnf("transfer legacy host firewall rule ownership failed, err: %v", err) global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 host chains and saved rules during startup")
} families = families[:1]
if err := migrationutils.TransferFirewallForwarding(ctx); err != nil { nftFamilies = nftFamilies[:1]
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)
} }
initialize = needInit() initialize = needInit()
if !initialize { if !initialize {
repairIptablesBaseChains(clientName) repairIptablesBaseChains(clientName, families...)
return return
} }
InitPingStatus() InitPingStatus()
global.LOG.Info("initializing firewall settings...") global.LOG.Info("initializing firewall settings...")
if clientName == "nftables" { 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) global.LOG.Errorf("restore nftables rules failed, err: %v", err)
} }
status, _ := repo.NewISettingRepo().GetValueByKey("IptablesStatus") status, _ := repo.NewISettingRepo().GetValueByKey("IptablesStatus")
if status == constant.StatusEnable { 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) 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) global.LOG.Errorf("load required firewall ports failed, err: %v", err)
return 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) global.LOG.Errorf("restore iptables base chains failed, err: %v", err)
return return
} }
@@ -94,7 +95,7 @@ func Init() {
} }
func repairIptablesBaseChains(clientName string) { func repairIptablesBaseChains(clientName string, families ...string) {
if clientName != constant.FirewallProviderIptables { if clientName != constant.FirewallProviderIptables {
return return
} }
@@ -108,7 +109,7 @@ func repairIptablesBaseChains(clientName string) {
global.LOG.Warnf("load required firewall ports for base chain repair failed, err: %v", err) global.LOG.Warnf("load required firewall ports for base chain repair failed, err: %v", err)
return 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) global.LOG.Warnf("repair iptables base chains failed, err: %v", err)
} }
} }
@@ -120,7 +121,7 @@ func initDockerPortGuard(ctx context.Context) {
) )
var restoreErr error var restoreErr error
for attempt := 1; attempt <= attempts; attempt++ { for attempt := 1; attempt <= attempts; attempt++ {
restoreErr = service.ReconcileDockerPortGuard(ctx) restoreErr = service.RestoreDockerPortGuard(ctx)
if restoreErr == nil { if restoreErr == nil {
return return
} }
+1 -2
View File
@@ -108,8 +108,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddFirewallRuleTable, migrations.AddFirewallRuleTable,
migrations.InitDockerPortGuardStatus, migrations.InitDockerPortGuardStatus,
migrations.NormalizeFirewallBackendSelections, migrations.NormalizeFirewallBackendSelections,
migrations.SimplifyFirewallRulePolicy, migrations.MigrateFirewallDescriptions,
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources, migrations.MigrateFirewallPortWhitelistSources,
migrations.AddAcceleratorMetrics, migrations.AddAcceleratorMetrics,
migrations.AddVLLMMonitor, migrations.AddVLLMMonitor,
+4 -70
View File
@@ -59,7 +59,6 @@ var AddTable = &gormigrate.Migration{
&model.Favorite{}, &model.Favorite{},
&model.FileShare{}, &model.FileShare{},
&model.Host{}, &model.Host{},
&model.FirewallRule{},
&model.Ftp{}, &model.Ftp{},
&model.ImageRepo{}, &model.ImageRepo{},
&model.ScriptLibrary{}, &model.ScriptLibrary{},
@@ -1784,7 +1783,7 @@ var AddComposePinned = &gormigrate.Migration{
var AddFirewallRuleTable = &gormigrate.Migration{ var AddFirewallRuleTable = &gormigrate.Migration{
ID: "20260819-add-firewall-v2-tables", ID: "20260819-add-firewall-v2-tables",
Migrate: func(tx *gorm.DB) error { 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{ var MigrateFirewallDescriptions = &gormigrate.Migration{
ID: "20260826-simplify-firewall-rule-policy", ID: "20260921-migrate-firewall-descriptions",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.FirewallRule{}) { return tx.Transaction(migrationutils.MigrateHostFirewallDescriptions)
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")
}, },
} }
@@ -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 package utils
import ( import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"slices"
"sort"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant" "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/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" "gorm.io/gorm"
) )
const hostFirewallTransferMigrationID = "host-firewall-transfer"
var errUnsupportedLegacyHostFirewallRule = errors.New("unsupported legacy host firewall rule") var errUnsupportedLegacyHostFirewallRule = errors.New("unsupported legacy host firewall rule")
type legacyHostFirewallRecord struct { type legacyHostFirewallRecord struct {
@@ -37,126 +33,225 @@ type legacyHostFirewallRecord struct {
Description string Description string
} }
func TransferHostFirewall(ctx context.Context, provider string) error { type legacyFirewallRuleDescription struct {
if global.DB == nil { UUID string
return errors.New("host firewall transfer database is required") Provider string
} ScopeKey string
return transferHostFirewall(ctx, global.DB, filter.Provider(strings.ToLower(strings.TrimSpace(provider)))) 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 { func MigrateHostFirewallDescriptions(db *gorm.DB) error {
if global.DB == nil { providers := []filter.Provider{filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW}
return errors.New("host firewall transfer database is required") 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( addRules := func(rules []filter.FirewallRule, description string) bool {
ctx, matched := false
global.DB, for _, rule := range rules {
filter.Provider(strings.ToLower(strings.TrimSpace(provider))), var err error
transfer, switch rule.Scope.Provider {
) case filter.ProviderFirewalld:
} rule, err = (&filterfirewalld.Adapter{}).PrepareRule(rule)
case filter.ProviderUFW:
func transferLegacyHostFirewallRuleOwnership( rule, err = (&filterufw.Adapter{}).PrepareRule(rule)
ctx context.Context, }
db *gorm.DB, if err != nil {
provider filter.Provider, continue
transfer func(context.Context) error, }
) error { id, err := filter.DescriptionID(filter.ObservedRule{Rule: rule, ParseStatus: filter.ParseStatusSupported})
if !legacyHostFirewallOwnershipProvider(provider) { if err == nil {
return 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") { if db.Migrator().HasTable("firewalls") {
var records []legacyHostFirewallRecord var records []legacyHostFirewallRecord
if err := db.WithContext(ctx).Table("firewalls").Order("id ASC").Find(&records).Error; err != nil { if err := db.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 {
return err return err
} }
if legacyHostFirewallOwnershipProvider(provider) { for _, record := range records {
return nil 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 { func legacyFirewallDescriptionRules(record legacyFirewallRuleDescription, provider filter.Provider) ([]filter.FirewallRule, error) {
converted := make([]model.FirewallRule, 0, len(records)) if record.CompatibilityError != "" {
byIdentity := make(map[string]int) return nil, errUnsupportedLegacyHostFirewallRule
for _, record := range records { }
rules, err := legacyHostFirewallRules(record, provider) 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 err != nil {
if global.LOG != nil { return nil, err
global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err)
}
continue
} }
for _, rule := range rules { result = append(result, expanded...)
item, err := hostFirewallRuleModel(rule) if source == "" && (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && strings.HasPrefix(record.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) {
if err != nil { for _, item := range expanded {
if global.LOG != nil { item.Scope.Chain = filter.BasicBeforeChain
global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err) result = append(result, item)
}
continue
} }
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) { func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Provider) ([]filter.FirewallRule, error) {
@@ -196,7 +291,7 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr
switch provider { switch provider {
case filter.ProviderIptables: case filter.ProviderIptables:
rule.Scope = filter.Scope{ 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, Chain: legacyIptablesChain(record), Direction: filter.DirectionInput,
} }
rule.NativeKind = filter.NativeKindRule rule.NativeKind = filter.NativeKindRule
@@ -337,84 +432,3 @@ func legacyIPOrPrefix(value string) bool {
_, err := netip.ParsePrefix(value) _, err := netip.ParsePrefix(value)
return err == nil 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
}
+15
View File
@@ -20,6 +20,12 @@ func Init() {
if err := validator.RegisterValidation("password", checkPasswordPattern); err != nil { if err := validator.RegisterValidation("password", checkPasswordPattern); err != nil {
panic(err) panic(err)
} }
if err := validator.RegisterValidation("vm_name", checkVMNamePattern); err != nil {
panic(err)
}
if err := validator.RegisterValidation("vm_common", checkVMCommonPattern); err != nil {
panic(err)
}
global.VALID = validator global.VALID = validator
} }
@@ -55,3 +61,12 @@ func checkPasswordPattern(fl validator.FieldLevel) bool {
return false return false
} }
func checkVMNamePattern(fl validator.FieldLevel) bool {
value := fl.Field().String()
return re.GetRegex(re.VMNameValidationPattern).MatchString(value)
}
func checkVMCommonPattern(fl validator.FieldLevel) bool {
value := fl.Field().String()
return re.GetRegex(re.VMCommonPattern).MatchString(value)
}
-1
View File
@@ -77,7 +77,6 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.GET("/network", baseApi.ListNetwork) baRouter.GET("/network", baseApi.ListNetwork)
baRouter.POST("/network/del", baseApi.DeleteNetwork) baRouter.POST("/network/del", baseApi.DeleteNetwork)
baRouter.POST("/network/clean", baseApi.CleanNetworks)
baRouter.POST("/network/search", baseApi.SearchNetwork) baRouter.POST("/network/search", baseApi.SearchNetwork)
baRouter.POST("/network", baseApi.CreateNetwork) baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.GET("/volume", baseApi.ListVolume) baRouter.GET("/volume", baseApi.ListVolume)
+3 -5
View File
@@ -28,6 +28,8 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort) hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings) hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend) 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", baseApi.CreateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist) hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist) 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/operate", baseApi.OperateForwardingRules)
hostRouter.POST("/firewall/forward/enable", baseApi.EnableForwarding) hostRouter.POST("/firewall/forward/enable", baseApi.EnableForwarding)
hostRouter.POST("/firewall/rules/search", baseApi.SearchFirewallRules) 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/reset", baseApi.ResetFirewallRules)
hostRouter.POST("/firewall/rules/native/detail", baseApi.LoadFirewallNativeDetail) 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", 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/update", baseApi.UpdateFirewallRule)
hostRouter.POST("/firewall/rules/delete", baseApi.DeleteFirewallRules) hostRouter.POST("/firewall/rules/delete", baseApi.DeleteFirewallRules)
hostRouter.POST("/firewall/rules/reorder", baseApi.ReorderFirewallRule) 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.POST("/firewall/filter/operate", baseApi.OperateFilterChain)
hostRouter.GET("/firewall/docker/ports", baseApi.ListDockerPortGuard) hostRouter.GET("/firewall/docker/ports", baseApi.ListDockerPortGuard)
hostRouter.GET("/firewall/docker/endpoints", baseApi.ListDockerPublishedPorts) 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/operate", baseApi.OperateDockerPortGuard)
hostRouter.POST("/firewall/docker/policies/batch", baseApi.UpsertDockerPortGuardPolicies) hostRouter.POST("/firewall/docker/policies/batch", baseApi.UpsertDockerPortGuardPolicies)
hostRouter.POST("/firewall/docker/policies/delete/batch", baseApi.DeleteDockerPortGuardPolicies) hostRouter.POST("/firewall/docker/policies/delete/batch", baseApi.DeleteDockerPortGuardPolicies)
+13
View File
@@ -251,6 +251,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
cmdItem.SysProcAttr = &syscall.SysProcAttr{ cmdItem.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, Setpgid: true,
} }
cmdItem.Cancel = func() error {
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
return cmdItem.Process.Kill()
}
cmds = append(cmds, cmdItem) cmds = append(cmds, cmdItem)
} }
return cmds return cmds
@@ -335,6 +339,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, 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} customWriter := &CustomWriter{taskItem: c.taskItem}
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
@@ -398,6 +408,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
}() }()
select { select {
case err := <-done: case err := <-done:
if newContext != nil && newContext.Err() != nil {
return "", c.pipeResultErr(newContext, err)
}
if err != nil { if err != nil {
return handleErr(&stdout, &stderr, c.IgnoreExist1, err) return handleErr(&stdout, &stderr, c.IgnoreExist1, err)
} }
-108
View File
@@ -1,108 +0,0 @@
package docker
import (
"context"
"sort"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type NetworkCleanupClient interface {
NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error)
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
NetworkInspect(context.Context, string, network.InspectOptions) (network.Inspect, error)
NetworkRemove(context.Context, string) error
}
func CleanUnusedNetworks(ctx context.Context, cli NetworkCleanupClient, onResult ...func(string, dto.NetworkCleanupItem)) (*dto.NetworkCleanupReport, error) {
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, err
}
used := make(map[string]bool)
for _, c := range containers {
if c.NetworkSettings == nil {
continue
}
for name, endpoint := range c.NetworkSettings.Networks {
used[name] = true
if endpoint != nil {
used[endpoint.NetworkID] = true
}
}
}
report := &dto.NetworkCleanupReport{Deleted: []dto.NetworkCleanupItem{}, Skipped: []dto.NetworkCleanupItem{}, Failed: []dto.NetworkCleanupItem{}}
record := func(status string, item dto.NetworkCleanupItem) {
switch status {
case "deleted":
report.Deleted = append(report.Deleted, item)
case "skipped":
report.Skipped = append(report.Skipped, item)
case "failed":
report.Failed = append(report.Failed, item)
}
for _, notify := range onResult {
if notify != nil {
notify(status, item)
}
}
}
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
for _, n := range networks {
if err := ctx.Err(); err != nil {
return report, err
}
item := dto.NetworkCleanupItem{ID: n.ID, Name: n.Name}
switch {
case n.Name == "none" || n.Name == "host" || n.Name == "bridge" || n.Name == "1panel-network":
item.Reason = "protected"
case n.Scope != "local" || n.Ingress || n.ConfigOnly:
item.Reason = "unsupported_network"
case used[n.Name] || used[n.ID]:
item.Reason = "container_connected"
}
if item.Reason != "" {
record("skipped", item)
continue
}
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
if err != nil {
if errdefs.IsNotFound(err) {
item.Reason = "already_removed"
record("skipped", item)
} else {
item.Reason = "inspect_failed"
record("failed", item)
}
continue
}
if len(inspected.Containers) > 0 {
item.Reason = "container_connected"
record("skipped", item)
continue
}
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
switch {
case errdefs.IsNotFound(err):
item.Reason = "already_removed"
record("skipped", item)
case errdefs.IsConflict(err):
item.Reason = "network_in_use"
record("skipped", item)
default:
item.Reason = "remove_failed"
record("failed", item)
}
continue
}
record("deleted", item)
}
return report, nil
}
@@ -1,72 +0,0 @@
package docker
import (
"context"
"errors"
"reflect"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type cleanupFake struct {
networks []network.Inspect
containers []container.Summary
inspected map[string]network.Inspect
inspectErrors, removeErrors map[string]error
listError error
removed []string
}
func (f *cleanupFake) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return f.networks, nil
}
func (f *cleanupFake) ContainerList(_ context.Context, o container.ListOptions) ([]container.Summary, error) {
if !o.All {
panic("must include stopped containers")
}
return f.containers, f.listError
}
func (f *cleanupFake) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
return f.inspected[id], f.inspectErrors[id]
}
func (f *cleanupFake) NetworkRemove(_ context.Context, id string) error {
f.removed = append(f.removed, id)
return f.removeErrors[id]
}
func TestNetworkCleanupProtectsAndReports(t *testing.T) {
f := &cleanupFake{inspected: map[string]network.Inspect{"attached": {Containers: map[string]network.EndpointResource{"container": {}}}}, inspectErrors: map[string]error{"unknown": errors.New("inspect failure")}, removeErrors: map[string]error{"race": errdefs.Conflict(errors.New("has active endpoints")), "gone": errdefs.NotFound(errors.New("gone")), "failed": errors.New("denied")}}
for _, name := range []string{"none", "host", "bridge", "1panel-network", "configured", "attached", "stopped", "free", "unknown", "race", "gone", "failed"} {
f.networks = append(f.networks, network.Inspect{ID: name, Name: name, Scope: "local"})
}
// Compose ownership labels do not protect an otherwise unused network.
for i := range f.networks {
if f.networks[i].Name == "configured" {
f.networks[i].Labels = map[string]string{"com.docker.compose.project": "demo", "com.docker.compose.network": "default"}
}
}
f.containers = []container.Summary{{State: "exited", NetworkSettings: &container.NetworkSettingsSummary{Networks: map[string]*network.EndpointSettings{"stopped": {NetworkID: "stopped"}}}}}
report, err := CleanUnusedNetworks(context.Background(), f)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(f.removed, []string{"configured", "failed", "free", "gone", "race"}) {
t.Fatal(f.removed)
}
if len(report.Deleted) != 2 || report.Deleted[0].Name != "configured" || report.Deleted[1].Name != "free" || len(report.Failed) != 2 || len(report.Skipped) != 8 {
t.Fatalf("%+v", report)
}
}
func TestNetworkCleanupDiscoveryFailureDeletesNothing(t *testing.T) {
f := &cleanupFake{networks: []network.Inspect{{ID: "free", Name: "free", Scope: "local"}}, listError: errors.New("cannot list containers")}
if _, err := CleanUnusedNetworks(context.Background(), f); err == nil {
t.Fatal("expected error")
}
if len(f.removed) > 0 {
t.Fatal(f.removed)
}
}
+5 -4
View File
@@ -1,6 +1,7 @@
package docker_guard package docker_guard
import ( import (
"context"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"slices" "slices"
@@ -13,8 +14,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
) )
func ReadDNATRules(backend, family string) DNATRules { func ReadDNATRules(ctx context.Context, backend, family string) DNATRules {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C")) manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
if backend == constant.FirewallProviderNftables { if backend == constant.FirewallProviderNftables {
tableFamily := "ip" tableFamily := "ip"
if family == constant.FirewallFamilyIPv6 { if family == constant.FirewallFamilyIPv6 {
@@ -45,8 +46,8 @@ func ReadDNATRules(backend, family string) DNATRules {
return DNATRules{Output: output, Inspected: err == nil} return DNATRules{Output: output, Inspected: err == nil}
} }
func ReadProxyEndpoints() ProxyEndpoints { func ReadProxyEndpoints(ctx context.Context) ProxyEndpoints {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C")) manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=") output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
if err != nil { if err != nil {
return ProxyEndpoints{} return ProxyEndpoints{}
+90 -64
View File
@@ -1,10 +1,11 @@
package docker_guard package docker_guard
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"sort" "slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -12,6 +13,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall" 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/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
) )
@@ -22,26 +24,27 @@ type Runner interface {
Exists(executable string) bool 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) 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...) stdout, err := manager.RunWithOptionalSudoAndStdout(executable, args...)
err = errors.Join(err, r.ctx.Err())
if err != nil { if err != nil {
return stdout, fmt.Errorf("command=%s %s failed: %w", executable, strings.Join(args, " "), err) return stdout, fmt.Errorf("command=%s %s failed: %w", executable, strings.Join(args, " "), err)
} }
return stdout, nil 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) executable = dockerGuardExecutable(executable)
if executable == "nft" { 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...) 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 { func (commandRunner) Exists(executable string) bool {
@@ -74,9 +77,12 @@ type Iptables struct {
var mutationMu sync.Mutex 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() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := CheckIPv4Forwarding(); err != nil { 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 { if err := m.ensureFamily("iptables", true); err != nil {
return err return err
} }
if m.runner.Exists("ip6tables") { if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") {
available, err := m.chainExists("ip6tables", DockerChain) available, err := m.chainExists("ip6tables", DockerChain)
if err != nil { if err != nil {
return &FamilyError{Family: FamilyIPv6, Err: fmt.Errorf("inspect %s chain: %w", DockerChain, err)} 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() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.bindExistingFamily("iptables", true); err != nil { if err := m.bindExistingFamily("iptables", true); err != nil {
return err return err
} }
if m.runner.Exists("ip6tables") { if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") {
if err := m.bindExistingFamily("ip6tables", false); err != nil { if err := m.bindExistingFamily("ip6tables", false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err} return &FamilyError{Family: FamilyIPv6, Err: err}
} }
@@ -126,13 +135,16 @@ func (m *Iptables) ReplacePolicies(policies []Policy, inventory PolicyInventory)
} }
func (m *Iptables) ListPolicies() (PolicyInventory, error) { 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} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
continue continue
} }
exists, err := m.chainExists(executable, Chain) exists, err := m.chainExists(executable, Chain)
if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil { if err != nil {
return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)} 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} return PolicyInventory{}, &FamilyError{Family: family, Err: err}
} }
inventory.Policies = append(inventory.Policies, parsed.Policies...) inventory.Policies = append(inventory.Policies, parsed.Policies...)
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...) for key, orders := range parsed.RuleOrders {
for key, orders := range parsed.ManagedRuleOrders { inventory.RuleOrders[key] = append([]int64(nil), orders...)
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
} }
} }
return inventory, nil return inventory, nil
} }
func (m *Iptables) Unbind() error { func (m *Iptables) Unbind(families ...string) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.unbindFamily("iptables"); err != nil { if len(families) == 0 {
return err families = []string{FamilyIPv4, FamilyIPv6}
} }
if m.runner.Exists("ip6tables") { for _, family := range families {
if err := m.unbindFamily("ip6tables"); err != nil { if err := m.unbindFamily(executableForFamily(family)); err != nil {
return err return err
} }
} }
@@ -315,13 +326,19 @@ func (m *Iptables) restoreLifecycle(executable string, rules [][]string) error {
return nil return nil
} }
func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) error { func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory, families ...string) error {
for _, family := range []string{FamilyIPv4, FamilyIPv6} { if len(families) == 0 {
families = []string{FamilyIPv4, FamilyIPv6}
}
for _, family := range families {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
continue continue
} }
exists, err := m.chainExists(executable, Chain) 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 { if err != nil {
return &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)} 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 return nil
} }
type orderedIPTablesRule struct {
order int64
index int
rules [][]string
}
func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInventory) [][]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) maxOrder := int64(0)
index := 0 for _, orders := range inventory.RuleOrders {
for _, orders := range inventory.ManagedRuleOrders {
for _, order := range orders { for _, order := range orders {
if order > maxOrder { maxOrder = max(maxOrder, order)
maxOrder = order
}
} }
} }
for _, item := range inventory.ReadOnly { for _, policy := range policies {
for _, native := range item.NativeRules { for _, rule := range policy.NativeRules {
if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain { maxOrder = max(maxOrder, rule.Order)
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 _, policy := range policies {
if policy.Family != family { if policy.Family != family {
continue 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) compiled := compilePolicy(policy)
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID] orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled { for ruleIndex, rule := range compiled {
order := int64(0) order := int64(0)
if ruleIndex < len(orders) { if ruleIndex < len(orders) {
@@ -389,21 +399,10 @@ func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInve
maxOrder++ maxOrder++
order = maxOrder order = maxOrder
} }
segments = append(segments, orderedIPTablesRule{order: order, index: index, rules: [][]string{rule}}) segments = append(segments, orderedPolicyRule{order: order, rule: rule})
index++
} }
} }
sort.SliceStable(segments, func(left, right int) bool { return sortPolicyRules(segments)
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
} }
func buildRestoreScript(rules [][]string) (string, error) { func buildRestoreScript(rules [][]string) (string, error) {
@@ -436,14 +435,20 @@ func compilePolicy(policy Policy) [][]string {
} }
base = append(base, "--ctorigdstport", strconv.Itoa(int(policy.HostPort))) base = append(base, "--ctorigdstport", strconv.Itoa(int(policy.HostPort)))
comment := "1panel-docker:" + policy.UUID comment := "1panel-docker:" + policy.UUID
if policy.Mode == ModeAll { if policy.Mode == ModeAll || policy.Mode == ModeAcceptAll {
return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", "DROP")} target := "DROP"
if policy.Mode == ModeAcceptAll {
target = "ACCEPT"
}
return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", target)}
} }
target := "DROP" target := "DROP"
capacity := len(policy.Sources) capacity := len(policy.Sources)
if policy.Mode == ModeAllow { if policy.Mode == ModeAllow {
target = "RETURN" target = "RETURN"
capacity++ capacity++
} else if policy.Mode == ModeAcceptSources {
target = "ACCEPT"
} }
rules := make([][]string, 0, capacity) rules := make([][]string, 0, capacity)
for _, source := range policy.Sources { 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) { func (m *Iptables) run(executable string, args ...string) (string, error) {
commandArgs := append([]string{"-w", "-t", "filter"}, args...) 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 { func executableForFamily(family string) string {
@@ -533,3 +542,20 @@ func isWildcardHost(family, hostIP string) bool {
return (family == FamilyIPv4 && (hostIP == "" || hostIP == "0.0.0.0")) || return (family == FamilyIPv4 && (hostIP == "" || hostIP == "0.0.0.0")) ||
(family == FamilyIPv6 && (hostIP == "" || hostIP == "::")) (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)
}
+100 -71
View File
@@ -1,13 +1,15 @@
package docker_guard package docker_guard
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"sort" "slices"
"strconv" "strconv"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
) )
@@ -23,9 +25,12 @@ type Nftables struct {
runner Runner 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() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
@@ -34,26 +39,33 @@ func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) erro
if err := CheckIPv4Forwarding(); err != nil { if err := CheckIPv4Forwarding(); err != nil {
return err return err
} }
if err := m.checkForwardPolicy(); err != nil { if err := m.checkForwardPolicy(families...); err != nil {
return err return err
} }
if err := m.ensureFamily(FamilyIPv4, true); err != nil { if err := m.ensureFamily(FamilyIPv4, true); err != nil {
return err return err
} }
if err := m.ensureFamily(FamilyIPv6, false); err != nil { if slices.Contains(families, FamilyIPv6) {
return &FamilyError{Family: FamilyIPv6, Err: err} 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() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.bindExistingFamily(FamilyIPv4, true); err != nil { if err := m.bindExistingFamily(FamilyIPv4, true); err != nil {
return err return err
} }
if err := m.bindExistingFamily(FamilyIPv6, false); err != nil { if slices.Contains(families, FamilyIPv6) {
return &FamilyError{Family: FamilyIPv6, Err: err} if err := m.bindExistingFamily(FamilyIPv6, false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err}
}
} }
return nil return nil
} }
@@ -68,11 +80,11 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) {
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return PolicyInventory{}, nil 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} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
output, err := nftables_helper.ReadChain(m.run, tableFamily, NftTable, NftChain) 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 continue
} }
if err != nil { if err != nil {
@@ -83,18 +95,20 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) {
return PolicyInventory{}, &FamilyError{Family: family, Err: err} return PolicyInventory{}, &FamilyError{Family: family, Err: err}
} }
inventory.Policies = append(inventory.Policies, parsed.Policies...) inventory.Policies = append(inventory.Policies, parsed.Policies...)
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...) for key, orders := range parsed.RuleOrders {
for key, orders := range parsed.ManagedRuleOrders { inventory.RuleOrders[key] = append([]int64(nil), orders...)
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
} }
} }
return inventory, nil 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() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range families {
if err := m.unbindFamily(family); err != nil { if err := m.unbindFamily(family); err != nil {
return &FamilyError{Family: family, Err: err} return &FamilyError{Family: family, Err: err}
} }
@@ -106,12 +120,16 @@ func (m *Nftables) Cleanup() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return nil return errors.New("nft is not installed")
} }
commands := make([][]string, 0, 2) commands := make([][]string, 0, 2)
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family) 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 continue
} }
commands = append(commands, []string{"delete", "table", tableFamily, NftTable}) commands = append(commands, []string{"delete", "table", tableFamily, NftTable})
@@ -160,7 +178,7 @@ func (m *Nftables) Status(family string) FamilyStatus {
} }
} }
if !baseExists || !guardExists { if !baseExists || !guardExists {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing, Partial: baseExists || guardExists}
} }
status := FamilyStatus{State: StatusNotEffective, Initialized: true} status := FamilyStatus{State: StatusNotEffective, Initialized: true}
rules := baseRules.String() rules := baseRules.String()
@@ -260,11 +278,14 @@ func (m *Nftables) ensureJump(family string) error {
return m.runBatch(commands) 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") { if !m.runner.Exists("nft") {
return nil return nil
} }
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range families {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
if !m.objectExists("chain", tableFamily, NftTable, NftChain) { if !m.objectExists("chain", tableFamily, NftTable, NftChain) {
continue continue
@@ -284,44 +305,37 @@ func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) e
return nil return nil
} }
type orderedNftRule struct {
order int64
index int
rules [][]string
}
func orderedNftRules(family string, policies []Policy, inventory PolicyInventory) [][]string { func orderedNftRules(family string, policies []Policy, inventory PolicyInventory) [][]string {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
segments := make([]orderedNftRule, 0, len(policies)+len(inventory.ReadOnly)) segments := make([]orderedPolicyRule, 0, len(policies))
maxOrder := int64(0) maxOrder := int64(0)
index := 0 for _, orders := range inventory.RuleOrders {
for _, orders := range inventory.ManagedRuleOrders {
for _, order := range orders { for _, order := range orders {
if order > maxOrder { maxOrder = max(maxOrder, order)
maxOrder = order
}
} }
} }
for _, item := range inventory.ReadOnly { for _, policy := range policies {
for _, native := range item.NativeRules { for _, rule := range policy.NativeRules {
if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" { maxOrder = max(maxOrder, rule.Order)
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 _, policy := range policies {
if policy.Family != family { if policy.Family != family {
continue 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) compiled := compileNftPolicy(policy)
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID] orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled { for ruleIndex, rule := range compiled {
order := int64(0) order := int64(0)
if ruleIndex < len(orders) { if ruleIndex < len(orders) {
@@ -330,27 +344,16 @@ func orderedNftRules(family string, policies []Policy, inventory PolicyInventory
maxOrder++ maxOrder++
order = maxOrder order = maxOrder
} }
segments = append(segments, orderedNftRule{order: order, index: index, rules: [][]string{rule}}) segments = append(segments, orderedPolicyRule{order: order, rule: rule})
index++
} }
} }
sort.SliceStable(segments, func(left, right int) bool { return sortPolicyRules(segments)
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
} }
func quoteNftTokens(tokens []string) []string { func quoteNftTokens(tokens []string) []string {
quoted := make([]string, 0, len(tokens)) quoted := make([]string, 0, len(tokens))
for _, token := range tokens { for index, token := range tokens {
if strings.ContainsAny(token, " \t\\\"'") && !strings.HasPrefix(token, `"`) { if (index > 0 && tokens[index-1] == "comment" || strings.ContainsAny(token, " \t\\\"'")) && !strings.HasPrefix(token, `"`) {
quoted = append(quoted, strconv.Quote(token)) quoted = append(quoted, strconv.Quote(token))
continue continue
} }
@@ -367,15 +370,22 @@ func compileNftPolicy(policy Policy) [][]string {
base = append(base, "ct", "original", addressKeyword, "daddr", policy.HostIP) base = append(base, "ct", "original", addressKeyword, "daddr", policy.HostIP)
} }
base = append(base, "ct", "original", "proto-dst", strconv.Itoa(int(policy.HostPort))) base = append(base, "ct", "original", "proto-dst", strconv.Itoa(int(policy.HostPort)))
comment := strconv.Quote("1panel-docker:" + policy.UUID) marker := "1panel-docker:" + policy.UUID
if policy.Mode == ModeAll { comment := strconv.Quote(marker)
return [][]string{append(append([]string{}, base...), "drop", "comment", comment)} 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" target := "drop"
capacity := len(policy.Sources) capacity := len(policy.Sources)
if policy.Mode == ModeAllow { if policy.Mode == ModeAllow {
target = "return" target = "return"
capacity++ capacity++
} else if policy.Mode == ModeAcceptSources {
target = "accept"
} }
rules := make([][]string, 0, capacity) rules := make([][]string, 0, capacity)
for _, source := range policy.Sources { for _, source := range policy.Sources {
@@ -506,11 +516,14 @@ func nftHasFirstUniqueJump(output string) bool {
return false return false
} }
func (m *Nftables) checkForwardPolicy() error { func (m *Nftables) checkForwardPolicy(families ...string) error {
for _, family := range []struct{ command, name string }{ for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4}, {"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6}, {"ip6tables", FamilyIPv6},
} { } {
if len(families) > 0 && !slices.Contains(families, family.name) {
continue
}
if !m.runner.Exists(family.command) { if !m.runner.Exists(family.command) {
continue continue
} }
@@ -526,11 +539,7 @@ func (m *Nftables) checkForwardPolicy() error {
} }
found = true found = true
if fields[2] == "DROP" { if fields[2] == "DROP" {
label := "IPv4" return &FamilyError{Family: family.name, Err: buserr.New("ErrDockerForwardPolicyDrop")}
if family.name == FamilyIPv6 {
label = "IPv6"
}
return &FamilyError{Family: family.name, Err: buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": label}, nil)}
} }
if fields[2] != "ACCEPT" { if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])} 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 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)
}
+144 -39
View File
@@ -1,8 +1,11 @@
package docker_guard package docker_guard
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/google/uuid"
"net/netip" "net/netip"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -12,9 +15,7 @@ import (
type observedPolicy struct { type observedPolicy struct {
policy Policy policy Policy
sequence int64 orders []int64
nativeRules []NativeRule
managedOrders []int64
dropAll bool dropAll bool
droppedSource []string droppedSource []string
allowedSource []string allowedSource []string
@@ -22,6 +23,106 @@ type observedPolicy struct {
acceptAll bool 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) { func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
groups := make(map[string]*observedPolicy) groups := make(map[string]*observedPolicy)
order := make([]string, 0) order := make([]string, 0)
@@ -35,33 +136,26 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
if err != nil { if err != nil {
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err) return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
} }
managed := strings.Contains(line, "1panel-docker:")
if !managed && !hasAcceptAction(tokens) {
continue
}
sequence++ sequence++
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family) fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
if err != nil { if err != nil {
return PolicyInventory{}, err return PolicyInventory{}, err
} }
identity := fragment.UUID if action == "" || (fragment.HostPort == 0 && action == "return") {
if action == "accept" { continue
identity = action
} }
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] group, exists := groups[key]
if !exists { if !exists {
group = &observedPolicy{policy: fragment, sequence: sequence} group = &observedPolicy{policy: fragment}
groups[key] = group groups[key] = group
order = append(order, key) order = append(order, key)
} }
switch { switch {
case action == "accept" && source != "": case action == "accept" && source != "":
group.acceptedSource = append(group.acceptedSource, source) group.acceptedSource = append(group.acceptedSource, source)
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "accept": case action == "accept":
group.acceptAll = true group.acceptAll = true
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "return" && source != "": case action == "return" && source != "":
group.allowedSource = append(group.allowedSource, source) group.allowedSource = append(group.allowedSource, source)
case action == "drop" && source != "": case action == "drop" && source != "":
@@ -71,21 +165,19 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
default: default:
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action) return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
} }
if action != "accept" { group.policy.NativeRules = append(group.policy.NativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
group.managedOrders = append(group.managedOrders, sequence) 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 { for _, key := range order {
group := groups[key] 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 { 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: case len(group.allowedSource) > 0:
group.policy.Mode = ModeAllow group.policy.Mode = ModeAllow
group.policy.Sources = uniqueSortedStrings(group.allowedSource) group.policy.Sources = uniqueSortedStrings(group.allowedSource)
@@ -97,8 +189,16 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
default: default:
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID) 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.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 return inventory, nil
} }
@@ -185,7 +285,10 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
action = tokens[index] 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") return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
} }
if action == "accept" && policy.Protocol == "" { if action == "accept" && policy.Protocol == "" {
@@ -194,18 +297,6 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
return policy, source, action, nil 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 { func normalizeObservedHost(value string) string {
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() { if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
return prefix.Addr().String() return prefix.Addr().String()
@@ -241,3 +332,17 @@ func uniqueSortedStrings(values []string) []string {
sort.Strings(result) sort.Strings(result)
return 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
}
+23 -21
View File
@@ -18,6 +18,8 @@ const (
ModeSources = "deny_sources" ModeSources = "deny_sources"
ModeAllow = "allow_sources" ModeAllow = "allow_sources"
ModeAll = "deny_all" ModeAll = "deny_all"
ModeAcceptSources = "accept_sources"
ModeAcceptAll = "accept_all"
StatusEffective = "effective" StatusEffective = "effective"
StatusDisabled = "disabled" StatusDisabled = "disabled"
StatusNotEffective = "not_effective" StatusNotEffective = "not_effective"
@@ -35,7 +37,12 @@ type FamilyError struct {
Err error 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 } func (e *FamilyError) Unwrap() error { return e.Err }
type ProxyEndpoint struct { type ProxyEndpoint struct {
@@ -55,16 +62,18 @@ type DNATRules struct {
} }
type Policy struct { type Policy struct {
UUID string UUID string
Family string Family string
HostIP string HostIP string
HostPort uint16 HostPort uint16
Protocol string Protocol string
Mode string Mode string
Sources []string Sources []string
NativeRules []NativeRule `json:",omitempty"`
} }
type FamilyStatus struct { type FamilyStatus struct {
Partial bool
State string State string
Reason string Reason string
Initialized bool Initialized bool
@@ -78,24 +87,17 @@ type NativeRule struct {
Tokens []string `json:"tokens"` Tokens []string `json:"tokens"`
} }
type ReadOnlyPolicy struct {
Policy Policy
Action string
Sequence int64
NativeRules []NativeRule
}
type PolicyInventory struct { type PolicyInventory struct {
Policies []Policy Policies []Policy
ReadOnly []ReadOnlyPolicy RuleOrders map[string][]int64
ManagedRuleOrders map[string][]int64
} }
type Runtime interface { type Runtime interface {
Initialize([]Policy, PolicyInventory) error Initialize([]Policy, PolicyInventory, ...string) error
Bind() error Bind(...string) error
OperateFamily(string, bool) error
ReplacePolicies([]Policy, PolicyInventory) error ReplacePolicies([]Policy, PolicyInventory) error
Unbind() error Unbind(...string) error
Cleanup() error Cleanup() error
Initialized(string) (bool, error) Initialized(string) (bool, error)
Status(string) FamilyStatus Status(string) FamilyStatus
+11 -11
View File
@@ -9,28 +9,29 @@ var (
ErrAdapterUnavailable = errors.New("firewall rule adapter is unavailable") ErrAdapterUnavailable = errors.New("firewall rule adapter is unavailable")
ErrInventoryUnavailable = errors.New("firewall rule inventory is unavailable") ErrInventoryUnavailable = errors.New("firewall rule inventory is unavailable")
ErrFamilyUnavailable = errors.New("firewall address family is unavailable") ErrFamilyUnavailable = errors.New("firewall address family is unavailable")
ErrRuleNotFound = errors.New("firewall rule does not exist")
) )
type ChangeOperation string type ChangeOperation string
const ( const (
ChangeCreate ChangeOperation = "create" ChangeCreate ChangeOperation = "create"
ChangeAdopt ChangeOperation = "adopt"
ChangeUpdate ChangeOperation = "update" ChangeUpdate ChangeOperation = "update"
ChangeDelete ChangeOperation = "delete" ChangeDelete ChangeOperation = "delete"
ChangeReorder ChangeOperation = "reorder" ChangeReorder ChangeOperation = "reorder"
) )
type RuleChange struct { type RuleChange struct {
CommandOnly bool `json:"-"` Target *ObservedRule `json:"-"`
UnmarkedAdopted bool `json:"-"` Raw string `json:"raw,omitempty"`
Operation ChangeOperation `json:"operation"` CommandOnly bool `json:"-"`
Before *FirewallRule `json:"before,omitempty"` Operation ChangeOperation `json:"operation"`
After *FirewallRule `json:"after,omitempty"` Before *FirewallRule `json:"before,omitempty"`
Locator *Locator `json:"locator,omitempty"` After *FirewallRule `json:"after,omitempty"`
PreviousMarker string `json:"previousMarker,omitempty"` Locator *Locator `json:"locator,omitempty"`
Append bool `json:"append,omitempty"` PreviousMarker string `json:"previousMarker,omitempty"`
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"` Append bool `json:"append,omitempty"`
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
} }
type NativeCommand struct { type NativeCommand struct {
@@ -69,7 +70,6 @@ func (p CommandBatch) CreatesOnly() bool {
type Adapter interface { type Adapter interface {
Provider() Provider Provider() Provider
Capabilities(context.Context) (Capabilities, error)
ListRules(context.Context, Scope) (RuleSet, error) ListRules(context.Context, Scope) (RuleSet, error)
BuildCommands(RuleSet, []RuleChange) (CommandBatch, error) BuildCommands(RuleSet, []RuleChange) (CommandBatch, error)
RunCommands(context.Context, CommandBatch) error RunCommands(context.Context, CommandBatch) error
+27 -22
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp"
"strings" "strings"
) )
@@ -53,28 +54,6 @@ func RuleMatchKey(rule FirewallRule) (string, error) {
return normalizedRuleKey(normalized) 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) { func normalizedRuleKey(normalized FirewallRule) (string, error) {
identity := ruleIdentity{ identity := ruleIdentity{
Scope: normalized.Scope.Key(), Scope: normalized.Scope.Key(),
@@ -231,3 +210,29 @@ func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) b
wantKey, wantErr := RuleKey(expected) wantKey, wantErr := RuleKey(expected)
return gotErr == nil && wantErr == nil && gotKey == wantKey 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
}
+4 -61
View File
@@ -1,70 +1,13 @@
package filter 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 { type PositionRange struct {
Min int `json:"min"` Min int `json:"min"`
Max int `json:"max"` Max int `json:"max"`
} }
type InventoryItem struct { type InventoryItem struct {
Incompatible bool `json:"incompatible,omitempty"` Rule FirewallRule `json:"rule"`
Error string `json:"error,omitempty"` Observed *ObservedRule `json:"observed"`
Rule FirewallRule `json:"rule"` IsWhitelist bool `json:"isWhitelist"`
Observed *ObservedRule `json:"observed,omitempty"` DescriptionID string `json:"descriptionID"`
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{}
} }
-9
View File
@@ -88,9 +88,7 @@ const (
type ScopeNoticeCode string type ScopeNoticeCode string
const ( const (
ScopeNoticeDefaultScopeMismatch ScopeNoticeCode = "default_scope_mismatch"
ScopeNoticeManagedScopeInactive ScopeNoticeCode = "managed_scope_inactive" ScopeNoticeManagedScopeInactive ScopeNoticeCode = "managed_scope_inactive"
ScopeNoticeUnmanagedActiveScopes ScopeNoticeCode = "unmanaged_active_scopes"
ScopeNoticeRuntimePermanentMismatch ScopeNoticeCode = "runtime_permanent_mismatch" ScopeNoticeRuntimePermanentMismatch ScopeNoticeCode = "runtime_permanent_mismatch"
ScopeNoticeManagedScopeMissing ScopeNoticeCode = "managed_scope_missing" ScopeNoticeManagedScopeMissing ScopeNoticeCode = "managed_scope_missing"
ScopeNoticeFamilyUnavailable ScopeNoticeCode = "family_unavailable" ScopeNoticeFamilyUnavailable ScopeNoticeCode = "family_unavailable"
@@ -303,10 +301,3 @@ type RuleSet struct {
Rules []ObservedRule `json:"rules"` Rules []ObservedRule `json:"rules"`
Notices []ScopeNotice `json:"notices,omitempty"` Notices []ScopeNotice `json:"notices,omitempty"`
} }
type Capabilities struct {
Marker bool
OwnedChains bool
ExplicitPosition bool
ExplicitPriority bool
}
@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -47,17 +46,6 @@ func NewAdapterWithBackend(reader CommandReader, writer CommandWriter) *Adapter
func (a *Adapter) Provider() filter.Provider { return filter.ProviderFirewalld } 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 { func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error {
if rule.Priority == nil || *rule.Priority == 0 { if rule.Priority == nil || *rule.Priority == 0 {
return nil return nil
@@ -215,8 +203,13 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err
return errors.New("firewalld writer is required") return errors.New("firewalld writer is required")
} }
executed, alreadyEnabled := 0, 0 executed, alreadyEnabled := 0, 0
var missing error
for index, command := range commands.Commands { for index, command := range commands.Commands {
err := a.writer.Run(ctx, command) err := a.writer.Run(ctx, command)
if errors.Is(err, filter.ErrRuleNotFound) && plan.CommandOnly {
missing = err
continue
}
if errors.Is(err, ErrAlreadyEnabled) { if errors.Is(err, ErrAlreadyEnabled) {
alreadyEnabled++ alreadyEnabled++
err = nil 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) { if commands.Operation == filter.ChangeCreate && alreadyEnabled > 0 && alreadyEnabled == len(commands.Commands) {
return ErrAlreadyEnabled return ErrAlreadyEnabled
} }
return nil return missing
} }
func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error { 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) { func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) {
rule := change.After if change.Operation == filter.ChangeCreate && change.Raw != "" && change.After != nil {
if change.Operation == filter.ChangeDelete { raw := strings.TrimSpace(change.Raw)
rule = change.Before 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 { if rule == nil {
return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) 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 { switch change.Operation {
case filter.ChangeCreate: case filter.ChangeCreate:
plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove")) 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: case filter.ChangeUpdate:
target, targetErr := validateMutationTarget(snapshot, change, normalized, true) target, targetErr := validateMutationTarget(snapshot, change)
if targetErr != nil { if targetErr != nil {
return filter.RuleCommands{}, targetErr 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")) addCommands, removeNewCommands := ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove"))
plan.Commands = append(removeCommands, addCommands...) plan.Commands = append(removeCommands, addCommands...)
plan.RollbackCommands = append(restoreCommands, removeNewCommands...) 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: default:
return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) 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) return "--" + operation + "-rich-rule=" + canonicalRichRule(rule)
} }
func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, normalized filter.FirewallRule, requireOwned bool) (filter.ObservedRule, error) { func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange) (filter.ObservedRule, error) {
if change.Locator == nil || change.Locator.Canonical == "" { target, err := filter.LocateRule(snapshot, change.Locator)
return filter.ObservedRule{}, fmt.Errorf("%w: firewalld mutation requires canonical locator", filter.ErrInvalidRule) if err != nil {
} return filter.ObservedRule{}, err
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
} }
if target.ParseStatus != filter.ParseStatusSupported { if target.ParseStatus != filter.ParseStatusSupported {
return filter.ObservedRule{}, filter.ErrRuleStale return filter.ObservedRule{}, filter.ErrUnsupportedScope
} }
want := normalized if change.Before == nil {
if requireOwned { return filter.ObservedRule{}, filter.ErrInvalidRule
if change.Before == nil || change.Before.UUID == "" { }
return filter.ObservedRule{}, fmt.Errorf("%w: managed previous rule is required", filter.ErrInvalidRule) want, err := (&Adapter{}).PrepareRule(*change.Before)
} if err != nil {
prepared, err := (&Adapter{}).PrepareRule(*change.Before) return filter.ObservedRule{}, err
if err != nil {
return filter.ObservedRule{}, err
}
want = prepared
} }
wantKey, wantErr := filter.RuleKey(want) wantKey, wantErr := filter.RuleKey(want)
targetKey, targetErr := filter.RuleKey(target.Rule) targetKey, targetErr := filter.RuleKey(target.Rule)
@@ -665,6 +671,7 @@ type mergedObject struct {
func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filter.ObservedRule, error) { func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filter.ObservedRule, error) {
objects := make(map[string]*mergedObject) objects := make(map[string]*mergedObject)
var ordered []*mergedObject
richObjects := make(map[string]*mergedObject) richObjects := make(map[string]*mergedObject)
add := func(rule filter.ObservedRule, runtimeState bool) *mergedObject { add := func(rule filter.ObservedRule, runtimeState bool) *mergedObject {
key := string(rule.Rule.NativeKind) + "\x00" + rule.Locator.Canonical key := string(rule.Rule.NativeKind) + "\x00" + rule.Locator.Canonical
@@ -673,6 +680,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt
copy := rule copy := rule
object = &mergedObject{rule: copy} object = &mergedObject{rule: copy}
objects[key] = object objects[key] = object
ordered = append(ordered, object)
} }
if runtimeState { if runtimeState {
object.runtime = true object.runtime = true
@@ -716,7 +724,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt
} }
rules := make([]filter.ObservedRule, 0, len(objects)) rules := make([]filter.ObservedRule, 0, len(objects))
for _, object := range objects { for _, object := range ordered {
switch { switch {
case object.runtime && object.permanent: case object.runtime && object.permanent:
object.rule.Persistence = filter.PersistenceStatusConverged object.rule.Persistence = filter.PersistenceStatusConverged
@@ -727,17 +735,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt
} }
rules = append(rules, object.rule) 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 return rules, nil
} }
@@ -1009,21 +1007,6 @@ func sameStringSet(left, right []string) bool {
return true 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 { func richOrderBucket(priority int, action filter.Action) string {
if priority < 0 { if priority < 0 {
return filter.OrderBucketRichPre return filter.OrderBucketRichPre
@@ -1049,7 +1032,7 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
if err := validateSystemCommand(command); err != nil { if err := validateSystemCommand(command); err != nil {
return err return err
} }
options, removals, alreadyEnabled := 0, 0, 0 options, removals, alreadyEnabled, missing := 0, 0, 0, 0
for _, arg := range command.Args { for _, arg := range command.Args {
if strings.HasPrefix(arg, "--add-") || strings.HasPrefix(arg, "--remove-") { if strings.HasPrefix(arg, "--add-") || strings.HasPrefix(arg, "--remove-") {
options++ options++
@@ -1078,6 +1061,7 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
case strings.HasPrefix(line, "Warning: ALREADY_ENABLED:"): case strings.HasPrefix(line, "Warning: ALREADY_ENABLED:"):
alreadyEnabled++ alreadyEnabled++
case strings.HasPrefix(line, "Warning: NOT_ENABLED:"): case strings.HasPrefix(line, "Warning: NOT_ENABLED:"):
missing++
if removals != options { if removals != options {
return fmt.Errorf("%w: %s", filter.ErrRuleStale, stderr.String()) 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 { if alreadyEnabled > 0 && alreadyEnabled == options {
return ErrAlreadyEnabled return ErrAlreadyEnabled
} }
if missing > 0 {
return fmt.Errorf("%w: %s", filter.ErrRuleNotFound, stderr.String())
}
return nil 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) 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 { func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
rule, err := filter.NormalizeRule(rule) rule, err := filter.NormalizeRule(rule)
if err != nil { if err != nil {
@@ -163,8 +157,7 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh
if createOnly { if createOnly {
return compileCreateBatch(snapshot, changes) return compileCreateBatch(snapshot, changes)
} }
externalDelete := len(changes) == 1 && changes[0].Locator == nil && (changes[0].UnmarkedAdopted || changes[0].PreviousMarker != "") if deleteOnly {
if deleteOnly && !externalDelete {
return compileDeleteBatch(snapshot, changes) return compileDeleteBatch(snapshot, changes)
} }
if len(changes) != 1 { if len(changes) != 1 {
@@ -221,14 +214,15 @@ func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f
var script strings.Builder var script strings.Builder
fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table) fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
for _, change := range changes { for _, change := range changes {
rulePlan, err := compileChange(snapshot, change) if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() || change.Target.Raw == "" {
if err != nil { return filter.CommandBatch{}, filter.ErrInvalidRule
return filter.CommandBatch{}, err
} }
line, err := restoreRuleLine(snapshot.Scope, *rulePlan.Previous) line, err := restoreRuleLine(snapshot.Scope, *change.Target)
if err != nil { if err != nil {
return filter.CommandBatch{}, err 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.WriteString(strings.Replace(line, "-A ", "-D ", 1))
script.WriteByte('\n') script.WriteByte('\n')
rulePlan.Commands, rulePlan.RollbackCommands = nil, nil 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{{ plan.Rules[0].Commands = []filter.NativeCommand{{
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: script.String(), 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 return plan, nil
} }
@@ -246,6 +248,17 @@ func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f
var script strings.Builder var script strings.Builder
fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table) fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
for _, change := range changes { 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) rulePlan, err := compileChange(snapshot, change)
if err != nil { if err != nil {
return filter.CommandBatch{}, err 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) return filter.RuleCommands{}, fmt.Errorf("%w: protocol %q does not match %s", filter.ErrInvalidRule, normalized.Protocol, normalized.Scope.Family)
} }
marker := "1panel-rule:" + normalized.UUID 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 position := len(snapshot.Rules) + 1
verb := "-I" verb := "-I"
var target filter.ObservedRule 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) return filter.RuleCommands{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule)
} }
position = insertionPosition(snapshot, normalized) 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: case filter.ChangeUpdate:
position, target, err = validateMutationTarget(snapshot, change, normalized, marker) position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
if err != nil { if err != nil {
@@ -450,7 +447,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
case filter.ChangeCreate: case filter.ChangeCreate:
rollbackArgs = append(rollbackArgs, "-D", snapshot.Scope.Chain) rollbackArgs = append(rollbackArgs, "-D", snapshot.Scope.Chain)
rollbackArgs = append(rollbackArgs, compileRuleArgs(normalized, marker)...) 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, "-R", snapshot.Scope.Chain, strconv.Itoa(position))
rollbackArgs = append(rollbackArgs, compileObservedRuleArgs(target)...) rollbackArgs = append(rollbackArgs, compileObservedRuleArgs(target)...)
case filter.ChangeDelete: 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, RuleUUID: rule.UUID, Operation: operation, Previous: &previous, Expected: expected,
} }
if position == targetPosition { if position == targetPosition {
plan.Expected = previous
return plan return plan
} }
executable := executableForFamily(snapshot.Scope.Family) 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 := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(targetPosition)}
insertArgs = append(insertArgs, compileRuleArgs(rule, marker)...) insertArgs = append(insertArgs, compileRuleArgs(rule, marker)...)
restoreArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(position)} 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 { if wantErr != nil || observedErr != nil || wantKey != observedKey {
return 0, filter.ObservedRule{}, filter.ErrRuleStale return 0, filter.ObservedRule{}, filter.ErrRuleStale
} }
if change.Operation != filter.ChangeAdopt && observed.Marker != marker && if observed.Marker != change.PreviousMarker {
!(change.Operation == filter.ChangeDelete && change.UnmarkedAdopted && observed.Marker == "") {
return 0, filter.ObservedRule{}, filter.ErrRuleStale return 0, filter.ObservedRule{}, filter.ErrRuleStale
} }
return position, observed, nil return position, observed, nil
@@ -654,11 +651,15 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
if err != nil { if err != nil {
return err 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 != "" { if command.Stdin != "" {
options = append(options, cmd.WithStdin(strings.NewReader(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 { 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) 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 { func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
rule, err := filter.NormalizeRule(rule) rule, err := filter.NormalizeRule(rule)
if err != nil { 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) return filter.CommandBatch{}, fmt.Errorf("%w: nftables mutation requires exactly one change", filter.ErrInvalidRule)
} }
change := changes[0] 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) return filter.CommandBatch{}, fmt.Errorf("%w: unsupported nftables mutation %s", filter.ErrInvalidRule, change.Operation)
} }
expected, previous, err := compileChange(snapshot, change) expected, previous, err := compileChange(snapshot, change)
@@ -180,7 +174,9 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh
target := *expected.Locator.Position target := *expected.Locator.Position
var script string var script string
if target == *previous.Locator.Position { 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) script = fmt.Sprintf("replace rule %s handle %s %s\n", chain, handle, expected.Raw)
if strings.ContainsAny(previous.Raw, "\r\n") || previous.Raw == "" { if strings.ContainsAny(previous.Raw, "\r\n") || previous.Raw == "" {
return filter.CommandBatch{}, fmt.Errorf("%w: invalid native nftables rule", filter.ErrInvalidRule) 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} plan := filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: true}
var script strings.Builder var script strings.Builder
for _, change := range changes { for _, change := range changes {
expected, previous, err := compileChange(snapshot, change) if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() || change.Target.Raw == "" {
if err != nil { return filter.CommandBatch{}, filter.ErrInvalidRule
return filter.CommandBatch{}, err
} }
handle := previous.Locator.NativeID handle := change.Target.Locator.NativeID
if _, err := strconv.ParseUint(handle, 10, 64); err != nil || change.Locator.NativeID != handle { if _, err := strconv.ParseUint(handle, 10, 64); err != nil {
return filter.CommandBatch{}, filter.ErrRuleStale 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) 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{ 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()}} 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} plan := filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: true}
var script strings.Builder var script strings.Builder
for _, change := range changes { 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 { if change.After == nil {
return filter.CommandBatch{}, fmt.Errorf("%w: create rule is required", filter.ErrInvalidRule) 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) return fmt.Errorf("unexpected nftables executable %q", command.Executable)
} }
if command.Stdin != "" { 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)). return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).
RunWithOptionalSudo(command.Executable, command.Args...) RunWithOptionalSudo(command.Executable, command.Args...)
@@ -9,12 +9,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/re" "github.com/1Panel-dev/1Panel/agent/utils/re"
) )
var ErrAlreadyEnabled = errors.New("ufw rule already exists")
type CommandReader interface { type CommandReader interface {
filter.CommentRuleReader filter.CommentRuleReader
Read(context.Context, ...string) (string, error) 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) 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) { func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, comment string) ([]filter.ObservedRule, error) {
var rules []filter.ObservedRule var rules []filter.ObservedRule
var output string var output string
@@ -101,7 +95,7 @@ func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope,
return nil, err return nil, err
} }
} }
rules = append(rules, parseNumberedRules(scope, output)...) rules = append(rules, ParseRules(scope, output)...)
} }
return rules, nil return rules, nil
} }
@@ -150,7 +144,7 @@ func (a *Adapter) ListRuleScopes(ctx context.Context, scopes []filter.Scope) ([]
notices := statusNotices(numbered) notices := statusNotices(numbered)
snapshots := make([]filter.RuleSet, 0, len(normalizedScopes)) snapshots := make([]filter.RuleSet, 0, len(normalizedScopes))
for _, scope := range normalizedScopes { for _, scope := range normalizedScopes {
snapshot, err := filter.NewRuleSet(scope, parseNumberedRules(scope, numbered)) snapshot, err := filter.NewRuleSet(scope, ParseRules(scope, numbered))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -215,35 +209,11 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err
executed := 0 executed := 0
for index, command := range plan.Rules[0].Commands { for index, command := range plan.Rules[0].Commands {
err := a.writer.Run(ctx, command) 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 err != nil {
if plan.CommandOnly { if plan.CommandOnly {
return fmt.Errorf("execute UFW rule: %w", err) return fmt.Errorf("execute UFW rule: %w", err)
} }
if !plan.CreatesOnly() { cause := fmt.Errorf("execute UFW rule: %w", err)
probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
if a.failedCommandApplied(probeCtx, plan.Rules[0], index) {
executed = index + 1
}
cancel()
}
cause := ufwApplyError(plan.Rules[0], fmt.Errorf("execute UFW rule: %w", err))
return a.compensate(ctx, plan.Rules[0], executed, cause) return a.compensate(ctx, plan.Rules[0], executed, cause)
} }
executed = index + 1 executed = index + 1
@@ -251,39 +221,6 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err
return nil 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 { func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
if err := validateBackendPlan(plan); err != nil { if err := validateBackendPlan(plan); err != nil {
return err return err
@@ -317,17 +254,71 @@ func validateBackendPlan(plan filter.CommandBatch) error {
if err := validateScope(plan.Scope); err != nil { if err := validateScope(plan.Scope); err != nil {
return err 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 fmt.Errorf("%w: incomplete ufw rollback plan", filter.ErrInvalidRule)
} }
return nil return nil
} }
func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) { func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) {
rule := change.After if change.Operation == filter.ChangeCreate && change.Raw != "" {
if change.Operation == filter.ChangeDelete { observed := ParseRules(snapshot.Scope, change.Raw)
rule = change.Before 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 { if rule == nil {
return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) 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) return filter.RuleCommands{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule)
} }
marker := "1panel-rule:" + normalized.UUID 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) position := insertionPosition(snapshot, normalized)
expected := observedForRule(normalized, marker, position) expected := observedForRule(normalized, marker, position)
plan := filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected} 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.Commands = []filter.NativeCommand{command}
plan.RollbackCommands = []filter.NativeCommand{deleteRuleCommand(normalized, marker)} 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: case filter.ChangeUpdate, filter.ChangeReorder:
if change.Before == nil { if change.Before == nil {
return filter.RuleCommands{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule) 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 { if err != nil {
return filter.RuleCommands{}, err return filter.RuleCommands{}, err
} }
target, targetErr := validateMutationTarget(snapshot, change, before, marker, true) target, targetErr := validateMutationTarget(snapshot, change, before)
if targetErr != nil { if targetErr != nil {
return filter.RuleCommands{}, targetErr 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), positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd),
deleteRuleCommand(normalized, marker), 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: default:
return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) 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 return nil
} }
func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, desired filter.FirewallRule, marker string, requireOwned bool) (filter.ObservedRule, error) { func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, desired filter.FirewallRule) (filter.ObservedRule, error) {
if change.Locator == nil || change.Locator.Position == nil { target, err := filter.LocateRule(snapshot, change.Locator)
return filter.ObservedRule{}, fmt.Errorf("%w: ufw mutation requires a numbered locator", filter.ErrInvalidRule) if err != nil {
return filter.ObservedRule{}, err
} }
if change.Locator.Provider != "" && change.Locator.Provider != filter.ProviderUFW { if target.Marker != strings.TrimSpace(change.PreviousMarker) {
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 {
return filter.ObservedRule{}, filter.ErrRuleStale 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 { 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) wantKey, wantErr := filter.RuleKey(desired)
if err != nil { gotKey, gotErr := filter.RuleKey(target.Rule)
return filter.ObservedRule{}, err if wantErr != nil || gotErr != nil || wantKey != gotKey {
}
gotKey, err := filter.RuleKey(target.Rule)
if err != nil {
return filter.ObservedRule{}, err
}
if change.Operation == filter.ChangeAdopt && wantKey != gotKey {
return filter.ObservedRule{}, filter.ErrRuleStale return filter.ObservedRule{}, filter.ErrRuleStale
} }
return target, nil return target, nil
@@ -680,29 +575,6 @@ func (a *Adapter) rollback(ctx context.Context, plan filter.RuleCommands, execut
return rollbackErr 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 { func hasScopeNotice(notices []filter.ScopeNotice, code filter.ScopeNoticeCode) bool {
for _, notice := range notices { for _, notice := range notices {
if notice.Code == code { 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) rules := make([]filter.ObservedRule, 0)
for _, rawLine := range strings.Split(output, "\n") { for _, rawLine := range strings.Split(output, "\n") {
raw := strings.TrimSpace(rawLine) raw := strings.TrimSpace(rawLine)
@@ -802,9 +674,13 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
return opaque() return opaque()
} }
sourceProtocol := "all"
sourceAddress, ok := parseSource(source) sourceAddress, ok := parseSource(source)
if !ok { if !ok {
return opaque() sourceAddress, sourceProtocol, ok = parseAddressProtocol(source)
if !ok {
return opaque()
}
} }
partialRule.SourceAddress = sourceAddress partialRule.SourceAddress = sourceAddress
if ruleAction == "" { if ruleAction == "" {
@@ -827,6 +703,12 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
Persistence: filter.PersistenceStatusConverged, Persistence: filter.PersistenceStatusConverged,
} }
} }
if sourceProtocol != "all" {
if protocol != "all" && protocol != sourceProtocol || destinationPort != "" {
return opaque()
}
protocol = sourceProtocol
}
partialRule.Protocol = protocol partialRule.Protocol = protocol
partialRule.DestinationAddress = destinationAddress partialRule.DestinationAddress = destinationAddress
partialRule.DestinationPort = destinationPort partialRule.DestinationPort = destinationPort
@@ -861,15 +743,9 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
return opaque() return opaque()
} }
locator.Canonical = canonicalRule(normalized) 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{ return filter.ObservedRule{
Rule: normalized, Locator: locator, Marker: marker, ParseStatus: parseStatus, Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported,
UncertainFields: uncertainFields, Raw: raw, Persistence: filter.PersistenceStatusConverged, Raw: raw, Persistence: filter.PersistenceStatusConverged,
} }
} }
@@ -937,6 +813,9 @@ func familyForNumberedRule(destination, source string) filter.Family {
func containsIPv6Address(value string) bool { func containsIPv6Address(value string) bool {
for _, token := range strings.Fields(value) { for _, token := range strings.Fields(value) {
token = strings.Trim(token, "(),") token = strings.Trim(token, "(),")
if address, _, ok := parseAddressProtocol(token); ok {
token = address
}
if prefix, err := netip.ParsePrefix(token); err == nil && prefix.Addr().Is6() { if prefix, err := netip.ParsePrefix(token); err == nil && prefix.Addr().Is6() {
return true return true
} }
@@ -1225,11 +1104,14 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
output, err := cmd.NewCommandMgr( output, err := cmd.NewCommandMgr(
cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"), cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"),
).RunWithOptionalSudoAndStdout(command.Executable, command.Args...) ).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 { if err != nil {
return err return err
} }
if strings.Contains(output, "Could not delete non-existent rule") { if strings.Contains(output, "Skipping adding existing rule") {
return fmt.Errorf("%w: %s", filter.ErrRuleStale, strings.TrimSpace(output)) return fmt.Errorf("%w: %s", ErrAlreadyEnabled, strings.TrimSpace(output))
} }
return nil return nil
} }
+12 -125
View File
@@ -2,28 +2,20 @@ package filter
import ( import (
"errors" "errors"
"fmt"
"net/netip"
"slices" "slices"
"strconv"
"strings" "strings"
) )
var ( var (
ErrRuleConflict = errors.New("firewall rule has identical conditions and an opposing action")
ErrRuleStale = errors.New("firewall rule state is stale") ErrRuleStale = errors.New("firewall rule state is stale")
ErrRuleOperation = errors.New("firewall rule operation is not allowed") 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) { func ProtectRuleSet(snapshot RuleSet, ports []PortWhitelist) (RuleSet, error) {
rules := slices.Clone(snapshot.Rules) rules := slices.Clone(snapshot.Rules)
whitelist := NewPortWhitelistIndex(ports) whitelist := NewPortWhitelistIndex(ports)
for index := range rules { for index := range rules {
if rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule) { rules[index].Protected = rules[index].Protected || rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule)
rules[index].Protected = true
}
} }
protected := snapshot protected := snapshot
protected.Rules = rules protected.Rules = rules
@@ -71,19 +63,11 @@ func NewPortWhitelistIndex(ports []PortWhitelist) PortWhitelistIndex {
return index return index
} }
func RuleMatchesPortWhitelist(rule FirewallRule, ports []PortWhitelist) bool {
return NewPortWhitelistIndex(ports).Matches(rule)
}
func (index PortWhitelistIndex) Matches(rule FirewallRule) bool { func (index PortWhitelistIndex) Matches(rule FirewallRule) bool {
rule, err := NormalizeRule(rule) rule, err := NormalizeRule(rule)
if err != nil || rule.Action != ActionAccept || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" || len(rule.ConnectionStates) != 0 { if err != nil || rule.Action != ActionAccept || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" || len(rule.ConnectionStates) != 0 {
return false 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} families := []Family{rule.Scope.Family}
if rule.Scope.Family == FamilyInet { if rule.Scope.Family == FamilyInet {
families = []Family{FamilyIPv4, FamilyIPv6} families = []Family{FamilyIPv4, FamilyIPv6}
@@ -144,118 +128,21 @@ func SameLocator(left, right Locator) bool {
return left.Canonical != "" && left.Canonical == right.Canonical return left.Canonical != "" && left.Canonical == right.Canonical
} }
func MatchObservedByRuleKey(observed []ObservedRule, rule FirewallRule) ([]ObservedRule, error) { func LocateRule(snapshot RuleSet, locator *Locator) (ObservedRule, error) {
wanted, err := RuleKey(rule) if locator == nil || locator.Provider != snapshot.Scope.Provider || locator.ScopeKey != snapshot.Scope.Key() {
if err != nil { return ObservedRule{}, ErrInvalidRule
return nil, err
} }
matches := make([]ObservedRule, 0, 1) var matches []ObservedRule
for _, candidate := range observed { for _, observed := range snapshot.Rules {
if candidate.ParseStatus != ParseStatusSupported { if SameLocator(observed.Locator, *locator) {
continue 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 { 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 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 CreateRules(context.Context, []Rule) error
DeleteRules(context.Context, []Rule) error DeleteRules(context.Context, []Rule) error
ReplaceRules(rules []Rule) error ReplaceRules(rules []Rule) error
Enable() error Enable(...string) error
OperateFamily(string, bool) error
UnbindFamily(string) error
Cleanup() error Cleanup() error
InitStatus() (bool, bool, error) InitStatus() (bool, bool, error)
FamilyStatus(family string) (bool, bool, error) FamilyStatus(family string) (bool, bool, error)
FamilyState(family string) (bool, bool, bool, error)
Replay() error Replay() error
} }
+129 -59
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -13,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall" 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/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
@@ -29,33 +31,35 @@ type iptablesBackend interface {
LoadIPv6RulesFromFile(table, chain, fileName string) error LoadIPv6RulesFromFile(table, chain, fileName string) error
} }
type systemIptablesBackend struct{} type systemIptablesBackend struct{ ctx context.Context }
func (systemIptablesBackend) IPv6Available() bool { func (systemIptablesBackend) IPv6Available() bool {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
return err == nil && commands.IPv6Available() return err == nil && commands.IPv6Available()
} }
func (systemIptablesBackend) Run(table string, args ...string) error { func (b systemIptablesBackend) Run(table string, args ...string) error {
return iptables_helper.Run(table, args...) _, 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" { 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 { func (b systemIptablesBackend) RunIPv6(table string, args ...string) error {
return iptables_helper.RunIPv6(table, args...) _, 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" { 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 { 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()) != "" { if err == nil && strings.TrimSpace(stderr.String()) != "" {
err = fmt.Errorf("firewall command warning: %s", 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 { 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 { type Iptables struct {
ctx context.Context
provider string provider string
backend iptablesBackend backend iptablesBackend
system forwardingSystem system forwardingSystem
} }
func NewIptables(provider string) *Iptables { func NewIptables(ctx context.Context, provider string) *Iptables {
return &Iptables{ return &Iptables{
ctx: ctx,
provider: provider, provider: provider,
backend: systemIptablesBackend{}, backend: systemIptablesBackend{ctx: ctx},
system: defaultForwardingSystem{}, system: defaultForwardingSystem{},
} }
} }
@@ -130,6 +136,9 @@ func (l *Iptables) List() ([]Rule, error) {
return rules, nil return rules, nil
} }
stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-S") stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-S")
if errors.Is(err, filter.ErrFamilyUnavailable) {
return rules, nil
}
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err) return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err)
} }
@@ -156,31 +165,25 @@ func (l *Iptables) ReplaceRules(rules []Rule) error {
} }
continue continue
} }
if family == FamilyIPv6 { initialized, _, err := l.FamilyStatus(family)
if len(byFamily[family]) > 0 { if family == FamilyIPv6 && len(byFamily[family]) == 0 && errors.Is(err, filter.ErrFamilyUnavailable) {
if err := ensureForwardingSysctls(l.system, true); err != nil { continue
failures = append(failures, err)
continue
}
} else {
initialized, _, err := l.familyInitStatus(family)
if err != nil {
failures = append(failures, err)
continue
}
if !initialized {
continue
}
}
} }
if err := l.batchEnsureChains(family); err != nil { if err != nil {
return err 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) script, err := buildIptablesForwardScript(byFamily[family], OperationAdd, true)
if err != nil { if err != nil {
return err 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) 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 == "" { if family == "" {
family = FamilyIPv4 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) 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" return target != "" && target != "127.0.0.1" && target != "localhost"
} }
func (l *Iptables) Enable() error { func (l *Iptables) Enable(families ...string) error {
if err := ensureForwardingSysctls(l.system, false); err != nil { if err := l.OperateFamily(FamilyIPv4, true); err != nil {
return err 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 list := l.backend.RunWithStd
if family == FamilyIPv6 { if family == FamilyIPv6 {
list = l.backend.RunIPv6WithStd list = l.backend.RunIPv6WithStd
@@ -330,11 +329,16 @@ func (l *Iptables) batchEnsureChains(family string) error {
} }
outputs[table] = output 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) script := buildIptablesForwardLifecycleScript(outputs, true)
if script == "" { if script == "" {
return nil 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 fmt.Errorf("batch initialize %s forwarding chains: %w", family, err)
} }
return nil return nil
@@ -359,7 +363,7 @@ func (l *Iptables) Cleanup() error {
} }
script := buildIptablesForwardLifecycleScript(outputs, false) script := buildIptablesForwardLifecycleScript(outputs, false)
if script != "" { 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) 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 return ipv4Init, ipv4Bind, nil
} }
ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6) ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6)
if errors.Is(err, filter.ErrFamilyUnavailable) {
return ipv4Init, ipv4Bind, nil
}
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
@@ -525,6 +532,14 @@ func (l *Iptables) InitStatus() (bool, bool, error) {
} }
func (l *Iptables) familyInitStatus(family string) (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" sysctlPath := "/proc/sys/net/ipv4/ip_forward"
label := "IPv4" label := "IPv4"
list := l.backend.RunWithStd list := l.backend.RunWithStd
@@ -535,27 +550,24 @@ func (l *Iptables) familyInitStatus(family string) (bool, bool, error) {
} }
data, err := l.system.ReadFile(sysctlPath) data, err := l.system.ReadFile(sysctlPath)
if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) { if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) {
return false, false, nil return false, false, false, nil
} }
if err != 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" forwardingEnabled := strings.TrimSpace(string(data)) != "0"
natRules, err := list(iptables_helper.NatTab, "-S") natRules, err := list(iptables_helper.NatTab, "-S")
if err != nil { 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( natInit, natBind := checkInitAndBind(
[]string{"-N " + ChainPreRouting, "-N " + ChainPostRouting}, []string{"-N " + ChainPreRouting, "-N " + ChainPostRouting},
[]string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting}, []string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting},
strings.Split(natRules, "\n"), strings.Split(natRules, "\n"),
) )
if !natInit {
return false, false, nil
}
filterRules, err := list(iptables_helper.FilterTab, "-S") filterRules, err := list(iptables_helper.FilterTab, "-S")
if err != nil { 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( filterInit, _ := checkInitAndBind(
[]string{"-N " + ChainForward}, []string{"-N " + ChainForward},
@@ -563,7 +575,9 @@ func (l *Iptables) familyInitStatus(family string) (bool, bool, error) {
strings.Split(filterRules, "\n"), strings.Split(filterRules, "\n"),
) )
filterBind := forwardBindingEffective(filterRules) 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) { func (l *Iptables) FamilyStatus(family string) (bool, bool, error) {
@@ -601,7 +615,7 @@ func (l *Iptables) Replay() error {
if family == FamilyIPv6 && !l.backend.IPv6Available() { if family == FamilyIPv6 && !l.backend.IPv6Available() {
continue continue
} }
if err := l.batchEnsureChains(family); err != nil { if err := l.batchEnsureChains(family, true); err != nil {
return err return err
} }
} }
@@ -726,3 +740,59 @@ func loadProtocol(protocol string) string {
return protocol 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())
}
+97 -84
View File
@@ -2,11 +2,11 @@ package forwarding
import ( import (
"context" "context"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -14,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall" 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" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
) )
@@ -24,10 +25,13 @@ const (
nftForwardMarker = "1panel-forward:" nftForwardMarker = "1panel-forward:"
) )
type Nftables struct{ system forwardingSystem } type Nftables struct {
ctx context.Context
system forwardingSystem
}
func NewNftables() *Nftables { func NewNftables(ctx context.Context) *Nftables {
return &Nftables{system: defaultForwardingSystem{}} return &Nftables{ctx: ctx, system: defaultForwardingSystem{}}
} }
func (n *Nftables) Name() string { return "nftables" } func (n *Nftables) Name() string { return "nftables" }
@@ -35,8 +39,8 @@ func (n *Nftables) Name() string { return "nftables" }
func (n *Nftables) List() ([]Rule, error) { func (n *Nftables) List() ([]Rule, error) {
rules := make([]Rule, 0) rules := make([]Rule, 0)
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, "NFT_"+ChainPreRouting) stdout, err := nftables_helper.ReadChain(n.run, nftTableFamily(family), nftForwardTable, "NFT_"+ChainPreRouting)
if errors.Is(err, nftables_helper.ErrChainNotFound) { if errors.Is(err, nftables_helper.ErrChainNotFound) || (family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable)) {
continue continue
} }
if err != nil { if err != nil {
@@ -58,31 +62,25 @@ func (n *Nftables) ReplaceRules(rules []Rule) error {
} }
var failures []error var failures []error
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == FamilyIPv6 { initialized, _, err := n.FamilyStatus(family)
if len(byFamily[family]) > 0 { if family == FamilyIPv6 && len(byFamily[family]) == 0 && errors.Is(err, filter.ErrFamilyUnavailable) {
if err := ensureForwardingSysctls(n.system, true); err != nil { continue
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
}
}
} }
if err := ensureNftForwardTables(family); err != nil { if err != nil {
return fmt.Errorf("initialize nftables forwarding table: %w", err) 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) commands, err := rebuildNftForwardCommands(byFamily[family], family)
if err != nil { if err != nil {
return err return err
} }
if err := nftRunCommands(context.Background(), commands); err != nil { if err := nftRunCommands(n.ctx, commands); err != nil {
return err return err
} }
} }
@@ -97,17 +95,6 @@ func (n *Nftables) CreateRules(ctx context.Context, rules []Rule) error {
if err != nil { if err != nil {
return err 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) return nftRunCommands(ctx, commands)
} }
@@ -154,27 +141,35 @@ func (n *Nftables) DeleteRules(ctx context.Context, rules []Rule) error {
return nftRunCommands(ctx, commands) return nftRunCommands(ctx, commands)
} }
func (n *Nftables) Enable() error { func (n *Nftables) Enable(families ...string) error {
if err := ensureForwardingSysctls(n.system, false); err != nil { if err := n.OperateFamily(FamilyIPv4, true); err != nil {
return err return err
} }
if err := ensureNftForwardTables(FamilyIPv4); err != nil { if len(families) > 0 && !slices.Contains(families, FamilyIPv6) {
return fmt.Errorf("initialize nftables forwarding table: %w", err) 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 { func (n *Nftables) Cleanup() error {
commands := make([][]string, 0, 2) commands := make([][]string, 0, 2)
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family) 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 continue
} }
commands = append(commands, []string{"delete", "table", tableFamily, nftForwardTable}) commands = append(commands, []string{"delete", "table", tableFamily, nftForwardTable})
} }
if len(commands) > 0 { if len(commands) > 0 {
if err := nftRunCommands(context.Background(), commands); err != nil { if err := nftRunCommands(n.ctx, commands); err != nil {
return err return err
} }
} }
@@ -189,6 +184,9 @@ func (n *Nftables) InitStatus() (bool, bool, error) {
var anyInitialized, anyBound bool var anyInitialized, anyBound bool
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
initialized, bound, err := n.FamilyStatus(family) initialized, bound, err := n.FamilyStatus(family)
if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
@@ -199,31 +197,37 @@ func (n *Nftables) InitStatus() (bool, bool, error) {
} }
func (n *Nftables) FamilyStatus(family string) (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" sysctlPath := "/proc/sys/net/ipv4/ip_forward"
if family == FamilyIPv6 { if family == FamilyIPv6 {
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding" sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
} }
data, err := n.system.ReadFile(sysctlPath) data, err := n.system.ReadFile(sysctlPath)
if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) { if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) {
return false, false, nil return false, false, false, nil
} }
if err != 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 { if err != nil {
return false, false, err return false, false, false, err
} }
if !exists { if !exists {
return false, false, nil return false, false, false, nil
} }
chains := nftables_helper.ParseTableChains(output) chains := nftables_helper.ParseTableChains(output)
count := 0
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} { for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
if _, exists := chains["NFT_"+chain]; !exists { if _, exists := chains["NFT_"+chain]; exists {
return false, false, nil 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 { func (n *Nftables) Replay() error {
@@ -235,21 +239,21 @@ func (n *Nftables) Replay() error {
} }
allPresent := true allPresent := true
for _, family := range []string{FamilyIPv4, FamilyIPv6} { 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 allPresent = false
} }
} }
if allPresent { if allPresent {
return nil 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) commands := make([][]string, 0, 8)
for _, family := range families { for _, family := range families {
tableFamily := nftTableFamily(family) 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 { if err != nil {
return err return err
} }
@@ -277,7 +281,7 @@ func ensureNftForwardTables(families ...string) error {
if len(commands) == 0 { if len(commands) == 0 {
return nil return nil
} }
return nftRunCommands(context.Background(), commands) return nftRunCommands(n.ctx, commands)
} }
func rebuildNftForwardCommands(rules []Rule, family string) ([][]string, error) { func rebuildNftForwardCommands(rules []Rule, family string) ([][]string, error) {
@@ -350,13 +354,6 @@ func decodeNftForwardRule(value string) (Rule, bool) {
return Rule{}, false return Rule{}, false
} }
value = strings.TrimPrefix(value, nftForwardMarker) 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, "|") parts := strings.Split(value, "|")
if len(parts) != 7 || parts[0] != "v2" { if len(parts) != 7 || parts[0] != "v2" {
return Rule{}, false return Rule{}, false
@@ -383,22 +380,6 @@ func decodeCompactNftForwardRule(value string) (Rule, bool) {
}, true }, 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 { func parseNftForwardRules(stdout string) []Rule {
result := make([]Rule, 0) result := make([]Rule, 0)
for _, line := range strings.Split(stdout, "\n") { for _, line := range strings.Split(stdout, "\n") {
@@ -422,16 +403,17 @@ func parseNftForwardRules(stdout string) []Rule {
return result return result
} }
func nftRun(args ...string) (string, error) { func (n *Nftables) run(args ...string) (string, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...) 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 { if err != nil {
return stdout, fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err) return stdout, fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err)
} }
return stdout, nil return stdout, nil
} }
func nftRunCommand(args ...string) error { func (n *Nftables) runCommand(args ...string) error {
err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudo("nft", args...) err := cmd.NewCommandMgr(cmd.WithContext(n.ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudo("nft", args...)
if err != nil { if err != nil {
return fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err) 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()) != "" { if err == nil && strings.TrimSpace(stderr.String()) != "" {
err = fmt.Errorf("firewall command warning: %s", 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) { func nftCommandsScript(commands [][]string) (string, error) {
@@ -467,3 +449,34 @@ func nftCommandsScript(commands [][]string) (string, error) {
} }
return script.String(), nil 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)
}
+118 -20
View File
@@ -1,6 +1,8 @@
package forwarding package forwarding
import ( import (
"context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -8,23 +10,32 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "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/docker"
"github.com/docker/docker/api/types/network"
) )
func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error { func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error {
withIPv6 := family == FamilyIPv6
if withIPv6 { if withIPv6 {
interfaces, err := IPv6RAInterfaces(system.ReadFile) interfaces, err := IPv6RAInterfaces(system.ReadFile, LoadDockerIPv4BridgePorts)
if err != nil { if err != nil {
return fmt.Errorf("check IPv6 Router Advertisement: %w", err) global.LOG.Warnf("IPv6 forwarding blocked: failed to check Router Advertisement: %v", err)
return buserr.New("ErrIPv6RACheckFailed")
} }
if len(interfaces) > 0 { 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, ", ")) global.LOG.Warnf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; review the network configuration and persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", "))
return buserr.New("ErrIPv6RARisk")
} }
} }
paths := []string{"/proc/sys/net/ipv4/ip_forward"} paths := []string{"/proc/sys/net/ipv4/ip_forward"}
if withIPv6 { 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 { for _, path := range paths {
if err := system.WriteFile(path, []byte("1"), constant.FilePerm); err != nil { if err := system.WriteFile(path, []byte("1"), constant.FilePerm); err != nil {
@@ -35,14 +46,14 @@ func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error {
if err != nil && !errors.Is(err, os.ErrNotExist) { if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to read /etc/sysctl.conf: %w", err) 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 { if err := system.WriteFile("/etc/sysctl.conf", []byte(content), constant.FilePerm); err != nil {
return fmt.Errorf("failed to persist IP forwarding: %w", err) return fmt.Errorf("failed to persist IP forwarding: %w", err)
} }
return nil return nil
} }
func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) { func IPv6RAInterfaces(readFile func(string) ([]byte, error), loadDockerPorts func() (map[string]bool, error)) ([]string, error) {
addresses, err := readFile("/proc/net/if_inet6") addresses, err := readFile("/proc/net/if_inet6")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -66,21 +77,16 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, exists := globalAddresses[name]; !exists { globalAddresses[name] = globalAddresses[name] || fields[3] == "00"
globalAddresses[name] = false if fields[3] == "00" && flags&permanentAddress == 0 {
} raInterfaces[name] = true
if fields[3] == "00" {
globalAddresses[name] = true
if flags&permanentAddress == 0 {
raInterfaces[name] = true
}
} }
} }
routes, err := readFile("/proc/net/ipv6_route") routes, err := readFile("/proc/net/ipv6_route")
if err != nil { if err != nil {
return nil, err return nil, err
} }
const raRouteFlags = 0x00010000 | 0x00800000 const raRouteFlags = 0x00010000 | 0x00080000 | 0x00800000
for _, line := range strings.Split(string(routes), "\n") { for _, line := range strings.Split(string(routes), "\n") {
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) == 0 { if len(fields) == 0 {
@@ -97,12 +103,15 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
raInterfaces[fields[9]] = true raInterfaces[fields[9]] = true
} }
} }
uncertainInterfaces := make(map[string]bool)
for name, hasGlobalAddress := range globalAddresses { for name, hasGlobalAddress := range globalAddresses {
if !hasGlobalAddress { if !hasGlobalAddress && !raInterfaces[name] {
raInterfaces[name] = true raInterfaces[name] = true
uncertainInterfaces[name] = true
} }
} }
var interfaces []string var interfaces []string
checkDockerPorts := false
for name := range raInterfaces { for name := range raInterfaces {
if name == "lo" { if name == "lo" {
continue continue
@@ -117,20 +126,109 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
switch strings.TrimSpace(string(value)) { switch strings.TrimSpace(string(value)) {
case "1": case "1":
interfaces = append(interfaces, name) interfaces = append(interfaces, name)
checkDockerPorts = checkDockerPorts || uncertainInterfaces[name]
case "0", "2": case "0", "2":
default: default:
return nil, fmt.Errorf("invalid accept_ra value for %s", name) return nil, fmt.Errorf("invalid accept_ra value for %s", name)
} }
} }
if checkDockerPorts {
ports, err := loadDockerPorts()
if err != nil {
global.LOG.Warnf("IPv6 forwarding RA check: cannot exclude Docker bridge ports: %v", err)
} else {
kept := interfaces[:0]
for _, name := range interfaces {
if !uncertainInterfaces[name] || !ports[name] {
kept = append(kept, name)
}
}
interfaces = kept
}
}
sort.Strings(interfaces) sort.Strings(interfaces)
return interfaces, nil return interfaces, nil
} }
func enableForwardingSysctls(content string, withIPv6 bool) string { func LoadDockerIPv4BridgePorts() (map[string]bool, error) {
cli, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer cli.Close()
if !strings.HasPrefix(cli.DaemonHost(), "unix://") {
return nil, errors.New("cannot verify local bridge ports using a remote Docker daemon")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
info, err := cli.Info(ctx)
if err != nil {
return nil, err
}
for _, option := range info.SecurityOptions {
if option == "name=rootless" {
return nil, errors.New("cannot verify host bridge ports using a rootless Docker daemon")
}
}
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout("ip", "-details", "-json", "link", "show")
if err != nil {
return nil, err
}
return dockerIPv4BridgePorts(networks, output)
}
func dockerIPv4BridgePorts(networks []network.Summary, linkJSON string) (map[string]bool, error) {
bridges := make(map[string]bool)
for _, item := range networks {
if item.Driver != "bridge" || item.EnableIPv6 || item.ConfigOnly {
continue
}
name := item.Options["com.docker.network.bridge.name"]
if name == "" {
if item.Options["com.docker.network.bridge.default_bridge"] == "true" {
name = "docker0"
} else if len(item.ID) >= 12 {
name = "br-" + item.ID[:12]
}
}
if name != "" {
bridges[name] = true
}
}
var links []struct {
Name string `json:"ifname"`
Master string `json:"master"`
LinkInfo struct {
Kind string `json:"info_kind"`
} `json:"linkinfo"`
}
if err := json.Unmarshal([]byte(linkJSON), &links); err != nil {
return nil, err
}
confirmedBridges := make(map[string]bool)
for _, link := range links {
if link.LinkInfo.Kind == "bridge" && bridges[link.Name] {
confirmedBridges[link.Name] = true
}
}
ports := make(map[string]bool)
for _, link := range links {
if link.LinkInfo.Kind == "veth" && confirmedBridges[link.Master] {
ports[link.Name] = true
}
}
return ports, nil
}
func enableFamilyForwardingSysctls(content, family string) string {
lines := strings.Split(strings.TrimRight(content, "\n"), "\n") lines := strings.Split(strings.TrimRight(content, "\n"), "\n")
wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"} wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"}
if withIPv6 { if family == FamilyIPv6 {
wanted["net.ipv6.conf.all.forwarding"] = "net.ipv6.conf.all.forwarding = 1" wanted = map[string]string{"net.ipv6.conf.all.forwarding": "net.ipv6.conf.all.forwarding = 1"}
} }
found := make(map[string]bool, len(wanted)) found := make(map[string]bool, len(wanted))
for index, line := range lines { for index, line := range lines {
+28 -21
View File
@@ -9,18 +9,39 @@ import (
) )
func LoadInitStatus(tab string) (bool, bool, error) { func LoadInitStatus(tab string) (bool, bool, error) {
return loadInitStatus(tab, RunWithStd, true) return loadInitStatus(tab, RunWithStd)
} }
func LoadFamilyInitStatus(family, tab string) (bool, bool, error) { 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 { switch family {
case constant.FirewallFamilyIPv4: case constant.FirewallFamilyIPv4:
return loadInitStatus(tab, RunWithStd, true)
case constant.FirewallFamilyIPv6: case constant.FirewallFamilyIPv6:
return loadInitStatus(tab, RunIPv6WithStd, true) runner = RunIPv6WithStd
default: 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) { func LoadFamilyBindStatus(family string) (bool, error) {
@@ -54,7 +75,7 @@ func hasBaseChainBinding(output string) bool {
return false 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 { switch tab {
case "base": case "base":
filterRules, err := runner(FilterTab, "-S") filterRules, err := runner(FilterTab, "-S")
@@ -66,14 +87,6 @@ func loadInitStatus(tab string, runner func(string, ...string) (string, error),
"-N " + BasicBeforeChain, "-N " + BasicBeforeChain,
"-N " + BasicChain, "-N " + BasicChain,
"-N " + BasicAfterChain, "-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{ bindRules := []string{
fmt.Sprintf("-A %s -j %s", InputChain, BasicBeforeChain), 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) { func checkWithInitAndBind(initRules, bindRules []string, lines []string) (bool, bool) {
output := strings.Join(lines, "\n")
for _, rule := range initRules { for _, rule := range initRules {
found := false if !containsIptablesRule(output, rule) {
for _, line := range lines {
if strings.TrimSpace(line) == strings.TrimSpace(rule) {
found = true
break
}
}
if !found {
if global.LOG != nil { if global.LOG != nil {
global.LOG.Debugf("not found init rule: %s", rule) global.LOG.Debugf("not found init rule: %s", rule)
} }
+5 -2
View File
@@ -52,8 +52,9 @@ func BindIPv6BaseChains() error {
} }
func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelist, output string) (string, error) { func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelist, output string) (string, error) {
existing := iptablesRuleIndex(output)
for _, chain := range BasicChains() { for _, chain := range BasicChains() {
if !containsIptablesRule(output, "-N "+chain) { if !existing[canonicalIptablesRule("-N "+chain)] {
return buildBaseChainsRestoreScript(dir, true, ports...) return buildBaseChainsRestoreScript(dir, true, ports...)
} }
} }
@@ -64,7 +65,9 @@ func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelis
var script strings.Builder var script strings.Builder
script.WriteString("*filter\n") script.WriteString("*filter\n")
for _, rule := range defaults { for _, rule := range defaults {
if !containsIptablesRule(output, rule) { key := canonicalIptablesRule(rule)
if !existing[key] {
existing[key] = true
script.WriteString(rule + "\n") 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"
}
+109 -32
View File
@@ -6,6 +6,7 @@ import (
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strings" "strings"
@@ -253,7 +254,7 @@ func saveBaseChainsFamily(ipv6 bool) error {
return nil return nil
} }
func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error { func RestoreBaseChains(requiredPorts []firewall.PortWhitelist, families ...string) error {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
return err return err
@@ -268,7 +269,7 @@ func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
if err := restoreRules(commands.Restore4, input); err != nil { if err := restoreRules(commands.Restore4, input); err != nil {
return fmt.Errorf("batch restore IPv4 base chains: %w", err) 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 return nil
} }
if err := ensureBaseChainsFamily(true); err != 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) { func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) {
existing := make(map[string]bool)
var script strings.Builder var script strings.Builder
script.WriteString("*filter\n") script.WriteString("*filter\n")
for _, chain := range BasicChains() { 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") { if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") {
continue continue
} }
existing[canonicalIptablesRule(line)] = true
script.WriteString(line) script.WriteString(line)
script.WriteByte('\n') script.WriteByte('\n')
} }
@@ -327,7 +330,9 @@ func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts .
return "", err return "", err
} }
for _, rule := range defaults { 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 ") { if strings.HasPrefix(rule, "-A "+BasicBeforeChain+" ") && strings.Contains(rule, " --dport ") {
rule = strings.Replace(rule, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1) 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 { func buildRequiredPortsRestoreScript(desired []firewall.SystemPort, family string, beforeRaw, afterRaw string, includeDefaults bool) string {
existing := iptablesRuleIndex(beforeRaw + "\n" + afterRaw)
var commands []string var commands []string
for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} { 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) commands = append(commands, line)
} }
} }
for _, rule := range desired { for _, rule := range desired {
line := iptablesSystemPortRuleLine(rule) 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)) commands = append(commands, strings.Replace(line, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1))
beforeRaw += "\n" + line
} }
} }
if includeDefaults { if includeDefaults {
for _, rule := range []string{DropAllTcp, DropAllUdp} { for _, rule := range []string{DropAllTcp, DropAllUdp} {
line := "-A " + BasicAfterChain + " " + rule line := "-A " + BasicAfterChain + " " + rule
if !containsIptablesRule(afterRaw, line) { key := canonicalIptablesRule(line)
if !existing[key] {
existing[key] = true
commands = append(commands, line) commands = append(commands, line)
} }
} }
@@ -459,35 +470,101 @@ func containsIptablesRule(output, rule string) bool {
} }
func countIptablesRule(output, rule string) int { func countIptablesRule(output, rule string) int {
canonical := func(value string) string { rule = canonicalIptablesRule(rule)
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)
count := 0 count := 0
for _, line := range strings.Split(output, "\n") { for _, line := range strings.Split(output, "\n") {
if canonical(line) == rule { if canonicalIptablesRule(line) == rule {
count++ count++
} }
} }
return 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" "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() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
return err return err
} }
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} { for _, family := range families {
ipv6 := family == constant.FirewallFamilyIPv6 ipv6 := family == constant.FirewallFamilyIPv6
run, executable := RunWithStd, commands.Restore4 run, executable := RunWithStd, commands.Restore4
if ipv6 { if ipv6 {
+99 -65
View File
@@ -21,7 +21,11 @@ func Cleanup() error {
commands := make([][]string, 0, 2) commands := make([][]string, 0, 2)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family) 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 continue
} }
commands = append(commands, []string{"delete", "table", tableFamily, TableName}) commands = append(commands, []string{"delete", "table", tableFamily, TableName})
@@ -64,9 +68,12 @@ func enableBase(prepare bool, requiredPorts []firewall.PortWhitelist) error {
return nil 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) commands := make([][]string, 0, 10)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range families {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
output, tableExists, err := ReadTable(run, tableFamily, TableName) output, tableExists, err := ReadTable(run, tableFamily, TableName)
if err != nil { if err != nil {
@@ -106,22 +113,35 @@ func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string
"accept", "comment", `"`+requiredPortComment+`"`) "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) ports, err := firewall.NormalizeRequiredPorts(requiredPorts)
if err != nil { if err != nil {
return err return err
} }
rules := firewall.ExpandPortWhitelist(ports) rules := firewall.ExpandPortWhitelist(ports)
var commands [][]string var commands [][]string
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range families {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
output, _, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain) output, _, err := ReadTable(run, tableFamily, TableName)
if err != nil { if err != nil {
return err 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{ candidates := [][]string{
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`}, {"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, 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 { for _, rule := range rules {
if rule.Family == string(family) { if rule.Family == string(family) {
@@ -129,90 +149,86 @@ func initPreRules(requiredPorts []firewall.PortWhitelist) error {
} }
} }
for _, command := range candidates { for _, command := range candidates {
chain := command[4]
expression := strings.Join(command[5:], " ") expression := strings.Join(command[5:], " ")
if !containsRequiredPortRule(output, expression) { key := canonicalRequiredPortRule(expression)
if !existing[chain][key] {
existing[chain][key] = true
commands = append(commands, command) 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...) return runBatch(commands...)
} }
func containsRequiredPortRule(output, expression string) bool { func canonicalRequiredPortRule(line string) string {
canonical := func(line string) string { line, _, _ = strings.Cut(line, " comment ")
line, _, _ = strings.Cut(line, " comment ") line, _, _ = strings.Cut(line, " # handle ")
line, _, _ = strings.Cut(line, " # handle ") for _, protocol := range []string{"tcp", "udp"} {
for _, protocol := range []string{"tcp", "udp"} { line = strings.ReplaceAll(line, "meta l4proto "+protocol+" "+protocol+" ", protocol+" ")
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "") }
} line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line) fields := strings.Fields(line)
fields := strings.Fields(line) for index, field := range fields {
for index, field := range fields { if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" {
if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" { states := strings.Split(field, ",")
states := strings.Split(field, ",") for i, state := range states {
for i, state := range states { value, err := strconv.ParseUint(state, 0, 64)
value, err := strconv.ParseUint(state, 0, 64) if err != nil {
if err != nil { continue
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"
}
} }
slices.Sort(states) switch value {
fields[index] = strings.Join(states, ",") case 1:
} states[i] = "invalid"
if prefix, err := netip.ParsePrefix(field); err == nil { case 2:
prefix = prefix.Masked() states[i] = "established"
fields[index] = prefix.String() case 4:
if prefix.Bits() == prefix.Addr().BitLen() { states[i] = "related"
fields[index] = prefix.Addr().String() case 8:
states[i] = "new"
case 64:
states[i] = "untracked"
} }
} }
slices.Sort(states)
fields[index] = strings.Join(states, ",")
} }
return strings.Join(fields, " ") if prefix, err := netip.ParsePrefix(field); err == nil {
} prefix = prefix.Masked()
wanted := canonical(expression) fields[index] = prefix.String()
for _, line := range strings.Split(output, "\n") { if prefix.Bits() == prefix.Addr().BitLen() {
if canonical(line) == wanted { fields[index] = prefix.Addr().String()
return true }
} }
} }
return false return strings.Join(fields, " ")
} }
func Bind() error { func Bind(families ...filter.Family) error {
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { if len(families) == 0 {
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
}
for _, family := range families {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
if _, err := run("list", "chain", tableFamily, TableName, InputChain); err != nil { initialized, _, err := LoadFamilyInitStatus(family, "base")
return fmt.Errorf("1Panel nftables %s input chain is not initialized: %w", tableFamily, err) if err != nil {
return err
}
if !initialized {
return fmt.Errorf("1Panel nftables %s chains are not initialized", tableFamily)
} }
} }
commands := make([][]string, 0, 8) commands := make([][]string, 0, 8)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range families {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
commands = append(commands, []string{"add", "table", tableFamily, TableName})
commands = append(commands, []string{"flush", "chain", tableFamily, TableName, InputChain}) commands = append(commands, []string{"flush", "chain", tableFamily, TableName, InputChain})
for _, chain := range BasicChains() { for _, chain := range BasicChains() {
commands = append(commands, []string{"add", "rule", tableFamily, TableName, InputChain, "jump", chain}) commands = append(commands, []string{"add", "rule", tableFamily, TableName, InputChain, "jump", chain})
} }
} }
if err := runBatch(commands...); err != nil { if err := runBatch(commands...); err != nil {
cleanupErr := flushInputChains() cleanupErr := flushInputChains(families...)
return errors.Join(err, cleanupErr) return errors.Join(err, cleanupErr)
} }
return PersistRuleset(context.Background()) return PersistRuleset(context.Background())
@@ -225,10 +241,28 @@ func Unbind() error {
return PersistRuleset(context.Background()) 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) 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}) commands = append(commands, []string{"flush", "chain", TableFamily(family), TableName, InputChain})
} }
return runBatch(commands...) 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())) return atomicWrite(filepath.Join(global.Dir.FirewallDir, RulesFile), []byte(ruleset.String()))
} }
func Restore() error { func Restore(families ...filter.Family) error {
file := filepath.Join(global.Dir.FirewallDir, RulesFile) if len(families) == 0 {
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { 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 return nil
} else if err != nil { }
if err != nil {
return err return err
} }
existing := make([]string, 0, 2) var script strings.Builder
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range families {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
_, exists, err := readNftObject(run, "list", "table", tableFamily, TableName) _, exists, err := readNftObject(run, "list", "table", tableFamily, TableName)
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
@@ -58,18 +62,35 @@ func Restore() error {
return err return err
} }
if exists { 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 return nil
} }
for _, tableFamily := range existing { return RunScript(script.String())
if _, err := run("delete", "table", tableFamily, TableName); err != nil { }
return fmt.Errorf("remove partial nftables %s table before restore: %w", tableFamily, err)
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 { 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 runScriptFile(script, func(file string) error {
return cmd.NewCommandMgr( return cmd.NewCommandMgr(
cmd.WithContext(ctx), cmd.WithContext(ctx),
cmd.WithEnv("LC_ALL=C"),
cmd.WithTimeout(60*time.Second), cmd.WithTimeout(60*time.Second),
).RunWithOptionalSudo("nft", "-f", file) ).RunWithOptionalSudo("nft", "-f", file)
}) })
@@ -169,26 +170,35 @@ func hasBaseChainBinding(output string) bool {
} }
func loadFamilyInitStatus(family filter.Family) (bool, bool, error) { 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) output, exists, err := ReadTable(run, TableFamily(family), TableName)
if err != nil || !exists { if err != nil || !exists {
return false, false, err return false, false, false, err
} }
chains := ParseTableChains(output) chains := ParseTableChains(output)
for _, chain := range BasicChains() { count := 0
if _, exists := chains[chain]; !exists { for _, chain := range append(BasicChains(), InputChain) {
return false, false, nil if _, ok := chains[chain]; ok {
count++
} }
} }
if count != len(BasicChains())+1 {
return false, false, count > 0, nil
}
input, exists := chains[InputChain] input, exists := chains[InputChain]
if !exists { if !exists {
return false, false, nil return false, false, false, nil
} }
for _, chain := range BasicChains() { for _, chain := range BasicChains() {
if !strings.Contains(input, "jump "+chain) { 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) { 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 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)
}
+1 -5
View File
@@ -263,7 +263,7 @@ func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) {
} }
func SystemPortKey(port SystemPort) string { 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 != "" { if family := strings.ToLower(strings.TrimSpace(port.Family)); family != "" {
key = family + "/" + key key = family + "/" + key
} }
@@ -273,10 +273,6 @@ func SystemPortKey(port SystemPort) string {
return key 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 { func SortedSystemPortKeys(ports map[string]SystemPort) []string {
keys := make([]string, 0, len(ports)) keys := make([]string, 0, len(ports))
for key := range ports { for key := range ports {
-21
View File
@@ -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"
)
+6
View File
@@ -6,6 +6,9 @@ import (
) )
const ( const (
VMNameValidationPattern = `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`
VMCommonPattern = `^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,63}$`
VLLMMetricSamplePattern = `^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{(?:[^"\\]|"(?:\\.|[^"\\])*")*\})?\s+(\S+)(?:\s+.*)?$` VLLMMetricSamplePattern = `^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{(?:[^"\\]|"(?:\\.|[^"\\])*")*\})?\s+(\S+)(?:\s+.*)?$`
VLLMMetricLabelPattern = `([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("(?:[^"\\]|\\.)*")` VLLMMetricLabelPattern = `([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("(?:[^"\\]|\\.)*")`
NumberAlphaPattern = `(\d+)([A-Za-z]+)` NumberAlphaPattern = `(\d+)([A-Za-z]+)`
@@ -63,6 +66,9 @@ var regexMap = make(map[string]*regexp.Regexp)
func Init() { func Init() {
patterns := []string{ patterns := []string{
VMNameValidationPattern,
VMCommonPattern,
VLLMMetricSamplePattern, VLLMMetricSamplePattern,
VLLMMetricLabelPattern, VLLMMetricLabelPattern,
NumberAlphaPattern, NumberAlphaPattern,
+3 -4
View File
@@ -309,16 +309,15 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) {
if revalidate { if revalidate {
s.revalidateCursor = cursor s.revalidateCursor = cursor
} }
shouldClose := clean || (!s.Persistent && !revalidate) if !clean {
if !shouldClose {
timeout := graceTimeout timeout := graceTimeout
if revalidate { if !s.Persistent || revalidate {
timeout = revalidateGrace timeout = revalidateGrace
} }
s.grace = time.AfterFunc(timeout, s.Close) s.grace = time.AfterFunc(timeout, s.Close)
} }
s.mu.Unlock() s.mu.Unlock()
if shouldClose { if clean {
s.Close() s.Close()
} }
} }
-1
View File
@@ -80,7 +80,6 @@ const (
TaskScopeCluster = "Cluster" TaskScopeCluster = "Cluster"
TaskScopeAppInstall = "AppInstallTask" TaskScopeAppInstall = "AppInstallTask"
TaskScopeAI = "AI" TaskScopeAI = "AI"
TaskScopeVm = "VirtualMachine"
) )
func GetTaskName(resourceName, operate, scope string) string { func GetTaskName(resourceName, operate, scope string) string {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+99 -111
View File
@@ -265,8 +265,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]", "formatZH": "应用端口修改 [key]-[name] => [port]",
"formatEN": "Application port update [key]-[name] =\u003e [port]" "formatEN": "Application port update [key]-[name] => [port]"
}, },
"/apps/installed/sort/update": { "/apps/installed/sort/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -644,13 +644,6 @@
"formatZH": "创建容器网络 name", "formatZH": "创建容器网络 name",
"formatEN": "create container network [name]" "formatEN": "create container network [name]"
}, },
"/containers/network/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理未使用的容器网络",
"formatEN": "Clean unused container networks"
},
"/containers/network/del": { "/containers/network/del": {
"bodyKeys": [ "bodyKeys": [
"names" "names"
@@ -686,8 +679,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "容器重命名 [name] =\u003e [newName]", "formatZH": "容器重命名 [name] => [newName]",
"formatEN": "rename container [name] =\u003e [newName]" "formatEN": "rename container [name] => [newName]"
}, },
"/containers/repo": { "/containers/repo": {
"bodyKeys": [ "bodyKeys": [
@@ -838,8 +831,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/auth/expired/reset": { "/core/auth/expired/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -1723,8 +1716,8 @@
"output_value": "name" "output_value": "name"
} }
], ],
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/enterprise/users/del": { "/core/enterprise/users/del": {
"bodyKeys": [ "bodyKeys": [
@@ -1925,8 +1918,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", "formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]" "formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
}, },
"/core/settings/memo": { "/core/settings/memo": {
"bodyKeys": [], "bodyKeys": [],
@@ -1955,8 +1948,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统端口 =\u003e [serverPort]", "formatZH": "修改系统端口 => [serverPort]",
"formatEN": "update system port =\u003e [serverPort]" "formatEN": "update system port => [serverPort]"
}, },
"/core/settings/proxy/update": { "/core/settings/proxy/update": {
"bodyKeys": [ "bodyKeys": [
@@ -1981,8 +1974,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统 ssl =\u003e [ssl]", "formatZH": "修改系统 ssl => [ssl]",
"formatEN": "update system ssl =\u003e [ssl]" "formatEN": "update system ssl => [ssl]"
}, },
"/core/settings/terminal/update": { "/core/settings/terminal/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -1998,8 +1991,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] =\u003e [value]", "formatZH": "修改系统配置 [key] => [value]",
"formatEN": "update system setting [key] =\u003e [value]" "formatEN": "update system setting [key] => [value]"
}, },
"/core/settings/upgrade": { "/core/settings/upgrade": {
"bodyKeys": [ "bodyKeys": [
@@ -2007,8 +2000,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新系统 =\u003e [version]", "formatZH": "更新系统 => [version]",
"formatEN": "upgrade system =\u003e [version]" "formatEN": "upgrade system => [version]"
}, },
"/core/xapp/appQRCode": { "/core/xapp/appQRCode": {
"bodyKeys": [ "bodyKeys": [
@@ -2466,7 +2459,7 @@
"formatZH": "同步 SSL 证书 [primaryDomain]", "formatZH": "同步 SSL 证书 [primaryDomain]",
"formatEN": "sync SSL certificate [primaryDomain]" "formatEN": "sync SSL certificate [primaryDomain]"
}, },
"/core/xpack/vms": { "/xpack/vms": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
], ],
@@ -2475,7 +2468,7 @@
"formatZH": "创建虚拟机 [name]", "formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]" "formatEN": "create VM [name]"
}, },
"/core/xpack/vms/del": { "/xpack/vms/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2493,7 +2486,7 @@
"formatZH": "删除虚拟机 [name]", "formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]" "formatEN": "delete VM [name]"
}, },
"/core/xpack/vms/disks/resize": { "/xpack/vms/disks/resize": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"target", "target",
@@ -2504,14 +2497,14 @@
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB", "formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB" "formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
}, },
"/core/xpack/vms/environment/enable": { "/xpack/vms/environment/enable": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "启用虚拟机运行环境", "formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment" "formatEN": "enable VM runtime environment"
}, },
"/core/xpack/vms/iso": { "/xpack/vms/iso": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2521,7 +2514,7 @@
"formatZH": "创建虚拟机镜像 [name] [type]", "formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]" "formatEN": "create VM ISO [name] [type]"
}, },
"/core/xpack/vms/iso/del": { "/xpack/vms/iso/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2539,7 +2532,7 @@
"formatZH": "删除虚拟机镜像 [name]", "formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]" "formatEN": "delete VM ISO [name]"
}, },
"/core/xpack/vms/iso/update": { "/xpack/vms/iso/update": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2549,7 +2542,7 @@
"formatZH": "更新虚拟机镜像 [name] [type]", "formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]" "formatEN": "update VM ISO [name] [type]"
}, },
"/core/xpack/vms/networks": { "/xpack/vms/networks": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2559,7 +2552,7 @@
"formatZH": "创建虚拟机网络 [name] [type]", "formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]" "formatEN": "create VM network [name] [type]"
}, },
"/core/xpack/vms/networks/del": { "/xpack/vms/networks/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2577,7 +2570,7 @@
"formatZH": "删除虚拟机网络 [name]", "formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]" "formatEN": "delete VM network [name]"
}, },
"/core/xpack/vms/networks/update": { "/xpack/vms/networks/update": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2595,7 +2588,7 @@
"formatZH": "更新虚拟机网络 [name]", "formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]" "formatEN": "update VM network [name]"
}, },
"/core/xpack/vms/operate": { "/xpack/vms/operate": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"operate" "operate"
@@ -2605,14 +2598,14 @@
"formatZH": "操作虚拟机 [name] [operate]", "formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]" "formatEN": "operate VM [name] [operate]"
}, },
"/core/xpack/vms/orphans/clean": { "/xpack/vms/orphans/clean": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据", "formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data" "formatEN": "clean VM orphaned data"
}, },
"/core/xpack/vms/rename": { "/xpack/vms/rename": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"newName" "newName"
@@ -2622,7 +2615,7 @@
"formatZH": "重命名虚拟机 [name] 为 [newName]", "formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]" "formatEN": "rename VM [name] to [newName]"
}, },
"/core/xpack/vms/snapshots": { "/xpack/vms/snapshots": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -2632,7 +2625,7 @@
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]", "formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]" "formatEN": "create VM [name] snapshot [snapshotName]"
}, },
"/core/xpack/vms/snapshots/del": { "/xpack/vms/snapshots/del": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -2642,7 +2635,7 @@
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]", "formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]" "formatEN": "delete VM [name] snapshot [snapshotName]"
}, },
"/core/xpack/vms/snapshots/recover": { "/xpack/vms/snapshots/recover": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -2652,7 +2645,7 @@
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]", "formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]" "formatEN": "recover VM [name] snapshot [snapshotName]"
}, },
"/core/xpack/vms/storages": { "/xpack/vms/storages": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -2662,7 +2655,7 @@
"formatZH": "创建虚拟机存储 [name] [type]", "formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]" "formatEN": "create VM storage [name] [type]"
}, },
"/core/xpack/vms/storages/del": { "/xpack/vms/storages/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2680,7 +2673,7 @@
"formatZH": "删除虚拟机存储 [name]", "formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]" "formatEN": "delete VM storage [name]"
}, },
"/core/xpack/vms/storages/update": { "/xpack/vms/storages/update": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2698,14 +2691,14 @@
"formatZH": "更新虚拟机存储 [name]", "formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]" "formatEN": "update VM storage [name]"
}, },
"/core/xpack/vms/sync": { "/xpack/vms/sync": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "同步虚拟机", "formatZH": "同步虚拟机",
"formatEN": "sync virtual machines" "formatEN": "sync virtual machines"
}, },
"/core/xpack/vms/templates": { "/xpack/vms/templates": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"templateName" "templateName"
@@ -2715,7 +2708,7 @@
"formatZH": "创建虚拟机 [name] 模板 [templateName]", "formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]" "formatEN": "create VM [name] template [templateName]"
}, },
"/core/xpack/vms/templates/del": { "/xpack/vms/templates/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -2733,7 +2726,7 @@
"formatZH": "删除虚拟机模板 [name]", "formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]" "formatEN": "delete VM template [name]"
}, },
"/core/xpack/vms/update": { "/xpack/vms/update": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
], ],
@@ -2895,8 +2888,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "首页应用 [key] =\u003e 显示:[value]", "formatZH": "首页应用 [key] => 显示:[value]",
"formatEN": "app launcher [key] =\u003e show: [value]" "formatEN": "app launcher [key] => show: [value]"
}, },
"/dashboard/quick/change": { "/dashboard/quick/change": {
"bodyKeys": [], "bodyKeys": [],
@@ -3024,7 +3017,7 @@
} }
], ],
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]", "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": { "/databases/grants": {
"bodyKeys": [ "bodyKeys": [
@@ -3105,7 +3098,7 @@
} }
], ],
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]", "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": { "/databases/mongodb/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3190,7 +3183,7 @@
} }
], ],
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", "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": { "/databases/pg/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3295,8 +3288,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]", "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]" "formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
}, },
"/databases/variables/update": { "/databases/variables/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -3332,8 +3325,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]", "formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]" "formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
}, },
"/files/chunkdownload": { "/files/chunkdownload": {
"bodyKeys": [ "bodyKeys": [
@@ -3423,8 +3416,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改权限 [path] =\u003e [mode]", "formatZH": "修改权限 [path] => [mode]",
"formatEN": "Change mode [path] =\u003e [mode]" "formatEN": "Change mode [path] => [mode]"
}, },
"/files/move": { "/files/move": {
"bodyKeys": [ "bodyKeys": [
@@ -3433,8 +3426,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]", "formatZH": "移动文件 [oldPaths] => [newPath]",
"formatEN": "Move [oldPaths] =\u003e [newPath]" "formatEN": "Move [oldPaths] => [newPath]"
}, },
"/files/owner": { "/files/owner": {
"bodyKeys": [ "bodyKeys": [
@@ -3444,8 +3437,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]", "formatZH": "修改用户/组 [path] => [user]/[group]",
"formatEN": "Change owner [path] =\u003e [user]/[group]" "formatEN": "Change owner [path] => [user]/[group]"
}, },
"/files/preview": { "/files/preview": {
"bodyKeys": [ "bodyKeys": [
@@ -3479,8 +3472,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "重命名 [oldName] =\u003e [newName]", "formatZH": "重命名 [oldName] => [newName]",
"formatEN": "Rename [oldName] =\u003e [newName]" "formatEN": "Rename [oldName] => [newName]"
}, },
"/files/save": { "/files/save": {
"bodyKeys": [ "bodyKeys": [
@@ -3537,8 +3530,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "下载 url =\u003e [path]/[name]", "formatZH": "下载 url => [path]/[name]",
"formatEN": "Download url =\u003e [path]/[name]" "formatEN": "Download url => [path]/[name]"
}, },
"/files/wget/process/remove": { "/files/wget/process/remove": {
"bodyKeys": [ "bodyKeys": [
@@ -3688,12 +3681,16 @@
"formatZH": "删除 Docker 端口防护策略 [uuids]", "formatZH": "删除 Docker 端口防护策略 [uuids]",
"formatEN": "delete Docker port guard policies [uuids]" "formatEN": "delete Docker port guard policies [uuids]"
}, },
"/hosts/firewall/docker/sync": { "/hosts/firewall/family/operate": {
"bodyKeys": [], "bodyKeys": [
"subsystem",
"family",
"operation"
],
"formatEN": "[operation] [subsystem] [family] firewall chains",
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": []
"formatZH": "同步 Docker 端口防护规则",
"formatEN": "sync Docker port guard rules"
}, },
"/hosts/firewall/filter/operate": { "/hosts/firewall/filter/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3734,13 +3731,6 @@
"formatZH": "添加防火墙规则", "formatZH": "添加防火墙规则",
"formatEN": "create firewall rules" "formatEN": "create firewall rules"
}, },
"/hosts/firewall/rules/adopt": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "纳管防火墙规则",
"formatEN": "adopt firewall rule"
},
"/hosts/firewall/rules/delete": { "/hosts/firewall/rules/delete": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
@@ -3750,12 +3740,12 @@
}, },
"/hosts/firewall/rules/reorder": { "/hosts/firewall/rules/reorder": {
"bodyKeys": [ "bodyKeys": [
"uuid" "instanceKey"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "调整防火墙规则顺序 [uuid]", "formatZH": "调整防火墙规则顺序 [instanceKey]",
"formatEN": "reorder firewall rule [uuid]" "formatEN": "reorder firewall rule [instanceKey]"
}, },
"/hosts/firewall/rules/reset": { "/hosts/firewall/rules/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -3764,25 +3754,23 @@
"formatZH": "重置防火墙规则", "formatZH": "重置防火墙规则",
"formatEN": "reset firewall rules" "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": { "/hosts/firewall/rules/update": {
"bodyKeys": [ "bodyKeys": [
"uuid" "instanceKey"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防火墙规则 [uuid]", "formatZH": "更新防火墙规则 [instanceKey]",
"formatEN": "update firewall rule [uuid]" "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": { "/hosts/firewall/settings/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3900,8 +3888,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]", "formatZH": "修改 SSH 配置 [key] => [newValue]",
"formatEN": "update SSH setting [key] =\u003e [newValue]" "formatEN": "update SSH setting [key] => [newValue]"
}, },
"/hosts/tool/config/set": { "/hosts/tool/config/set": {
"bodyKeys": [ "bodyKeys": [
@@ -4162,7 +4150,7 @@
} }
], ],
"formatZH": "快照 [name] 描述信息修改 [description]", "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": { "/settings/snapshot/import": {
"bodyKeys": [ "bodyKeys": [
@@ -4254,8 +4242,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] =\u003e [value]", "formatZH": "修改系统配置 [key] => [value]",
"formatEN": "update system setting [key] =\u003e [value]" "formatEN": "update system setting [key] => [value]"
}, },
"/toolbox/clam": { "/toolbox/clam": {
"bodyKeys": [ "bodyKeys": [
@@ -4373,8 +4361,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机参数 [key] =\u003e [value]", "formatZH": "修改主机参数 [key] => [value]",
"formatEN": "update device conf [key] =\u003e [value]" "formatEN": "update device conf [key] => [value]"
}, },
"/toolbox/device/update/host": { "/toolbox/device/update/host": {
"bodyKeys": [ "bodyKeys": [
@@ -4383,8 +4371,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机 Host [key] =\u003e [value]", "formatZH": "修改主机 Host [key] => [value]",
"formatEN": "update device host [key] =\u003e [value]" "formatEN": "update device host [key] => [value]"
}, },
"/toolbox/device/update/swap": { "/toolbox/device/update/swap": {
"bodyKeys": [ "bodyKeys": [
@@ -4412,8 +4400,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]", "formatZH": "修改 Fail2ban 配置 [key] => [value]",
"formatEN": "update fail2ban conf [key] =\u003e [value]" "formatEN": "update fail2ban conf [key] => [value]"
}, },
"/toolbox/ftp": { "/toolbox/ftp": {
"bodyKeys": [ "bodyKeys": [
@@ -4672,8 +4660,8 @@
"output_value": "domain" "output_value": "domain"
} }
], ],
"formatZH": "修改默认 server =\u003e [domain]", "formatZH": "修改默认 server => [domain]",
"formatEN": "Change default server =\u003e [domain]" "formatEN": "Change default server => [domain]"
}, },
"/websites/del": { "/websites/del": {
"bodyKeys": [ "bodyKeys": [
@@ -5436,8 +5424,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]", "formatZH": "更新防篡改信息 [website][path] => [status]",
"formatEN": "update tamper info [website][path] =\u003e [status]" "formatEN": "update tamper info [website][path] => [status]"
}, },
"/xpack/vllm/command-template/create": { "/xpack/vllm/command-template/create": {
"bodyKeys": [ "bodyKeys": [
-42
View File
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "Failed to fetch application status. Check installation node
MasterNodePortNotAvailable: "Node {{ .name }} port {{ .port }} connectivity check failed. Check firewall/security group settings and master node status." MasterNodePortNotAvailable: "Node {{ .name }} port {{ .port }} connectivity check failed. Check firewall/security group settings and master node status."
ClusterMasterNotExist: "Cluster master node is disconnected. Delete child nodes." ClusterMasterNotExist: "Cluster master node is disconnected. Delete child nodes."
# virtual machine
ErrVMNotFound: "Virtual machine does not exist"
ErrVMAlreadyExists: "Virtual machine already exists"
ErrVMCountLimit: "The Professional edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade to Enterprise to create more."
ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}"
ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm not found"
ErrVMBridgeManaged: "Bridge name is already managed by libvirt or Docker"
ErrVMNetworkInUse: "Network is in use and cannot be deleted"
ErrVMStorageInUse: "Storage pool is in use and cannot be deleted"
ErrVMStorageNameCannotChange: "Storage pool name cannot be changed"
ErrVMStoragePathCannotChange: "Storage pool path cannot be changed"
ErrVMRunning: "Virtual machine is running"
ErrVMLibvirtConnect: "Failed to connect to libvirt: {{ .detail }}"
ErrVMPermission: "No permission to operate virtual machine: {{ .detail }}"
ErrVMNetworkNotFound: "Virtual machine network does not exist: {{ .detail }}"
ErrVMInvalidPath: "Invalid virtual machine path: {{ .detail }}"
ErrVMInvalidInput: "Invalid virtual machine parameter: {{ .detail }}"
ErrVMCommandFailed: "Virtual machine command failed: {{ .err }}"
ErrVMDiskResizeBlocked: "The disk does not meet expansion requirements or is referenced by another device. Check the disk status."
ErrVMDiskReferenced: "This virtual disk is used by another virtual machine or device and cannot be expanded yet."
ErrVMDiskReferenceCheck: "Cannot complete virtual disk reference checks. Expansion is unavailable; check the disks of other virtual machines."
ErrVMSnapshotMetadataSync: "The snapshot was restored, but disk information could not be synced. Refresh and check; do not restore the snapshot again."
VMCreateTask: "Create virtual machine [{{ .name }}]"
VMCreateFromTemplateTask: "Create virtual machine [{{ .name }}] from template"
VMUpdateTask: "Update virtual machine [{{ .name }}]"
VMDiskResizeTask: "Expand disk [{{ .disk }}] of virtual machine [{{ .name }}]"
VMRenameTask: "Rename virtual machine [{{ .name }}] to [{{ .newName }}]"
VMDeleteTask: "Delete virtual machine [{{ .name }}]"
VMTemplateCreateTask: "Save virtual machine [{{ .name }}] as template [{{ .template }}]"
VMTemplateDeleteTask: "Delete virtual machine template [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm not found, force deleting virtual machine [{{ .name }}] metadata only"
VMForceDeleteMetadataAfterLibvirtFailed: "Failed to delete virtual machine [{{ .name }}] from libvirt, force deleting metadata: {{ .err }}"
VMForceDeleteDiskFailed: "Force delete virtual machine disk file [{{ .path }}] failed: {{ .err }}"
VMForceDeleteDiskSuccess: "Force deleted virtual machine disk file [{{ .path }}]"
VMSnapshotCreateTask: "Create snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotCreateStep: "Create virtual machine snapshot [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Recover snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotRecoverStep: "Recover virtual machine snapshot [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Delete snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotDeleteStep: "Delete virtual machine snapshot [{{ .snapshot }}]"
# app # app
CustomAppStoreFileValid: "Application store package requires .tar.gz format" CustomAppStoreFileValid: "Application store package requires .tar.gz format"
AppBackup: 'Application backup' AppBackup: 'Application backup'
-42
View File
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "Adquisición anormal del estado de la aplicación, revise e
MasterNodePortNotAvailable: "Verificación de conectividad del puerto {{ .port }} del nodo {{ .name }} fallida, verifique la configuración del firewall/grupo de seguridad y el estado del nodo principal." MasterNodePortNotAvailable: "Verificación de conectividad del puerto {{ .port }} del nodo {{ .name }} fallida, verifique la configuración del firewall/grupo de seguridad y el estado del nodo principal."
ClusterMasterNotExist: "El nodo principal del clúster está desconectado, elimine los nodos secundarios." ClusterMasterNotExist: "El nodo principal del clúster está desconectado, elimine los nodos secundarios."
# máquina virtual
ErrVMNotFound: "La máquina virtual no existe"
ErrVMAlreadyExists: "La máquina virtual ya existe"
ErrVMCountLimit: "La edición Professional permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o actualiza a Enterprise y vuelve a intentarlo."
ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}"
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
ErrVMHelperNotFound: "No se encontró 1panel-kvm"
ErrVMBridgeManaged: "El nombre del bridge ya está administrado por libvirt o Docker"
ErrVMNetworkInUse: "La red está en uso y no se puede eliminar"
ErrVMStorageInUse: "El pool de almacenamiento está en uso y no se puede eliminar"
ErrVMStorageNameCannotChange: "El nombre del pool de almacenamiento no se puede cambiar"
ErrVMStoragePathCannotChange: "La ruta del pool de almacenamiento no se puede cambiar"
ErrVMRunning: "La máquina virtual está en ejecución"
ErrVMLibvirtConnect: "Error al conectar con libvirt: {{ .detail }}"
ErrVMPermission: "Sin permiso para operar la máquina virtual: {{ .detail }}"
ErrVMNetworkNotFound: "La red de la máquina virtual no existe: {{ .detail }}"
ErrVMInvalidPath: "Ruta de máquina virtual no válida: {{ .detail }}"
ErrVMInvalidInput: "Parámetro de máquina virtual no válido: {{ .detail }}"
ErrVMCommandFailed: "Error al ejecutar el comando de la máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "El disco no cumple los requisitos de ampliación o está referenciado por otro dispositivo. Compruebe su estado."
ErrVMDiskReferenced: "Otro equipo virtual o dispositivo utiliza este disco virtual; la ampliación aún no se admite."
ErrVMDiskReferenceCheck: "No se pueden comprobar las referencias al disco virtual. La ampliación no está disponible; revise los discos de otras máquinas virtuales."
ErrVMSnapshotMetadataSync: "La instantánea se restauró, pero falló la sincronización de los datos del disco. Actualice y compruebe; no restaure de nuevo la instantánea."
VMCreateTask: "Crear máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Crear máquina virtual [{{ .name }}] desde plantilla"
VMUpdateTask: "Actualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Ampliar disco [{{ .disk }}] de la máquina virtual [{{ .name }}]"
VMRenameTask: "Renombrar máquina virtual [{{ .name }}] a [{{ .newName }}]"
VMDeleteTask: "Eliminar máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Guardar máquina virtual [{{ .name }}] como plantilla [{{ .template }}]"
VMTemplateDeleteTask: "Eliminar plantilla de máquina virtual [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm no encontrado, eliminando solo los metadatos de la máquina virtual [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "Error al eliminar la máquina virtual [{{ .name }}] de libvirt, eliminando metadatos: {{ .err }}"
VMForceDeleteDiskFailed: "Error al forzar la eliminación del archivo de disco de la máquina virtual [{{ .path }}]: {{ .err }}"
VMForceDeleteDiskSuccess: "Archivo de disco de la máquina virtual [{{ .path }}] eliminado forzosamente"
VMSnapshotCreateTask: "Crear instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
VMSnapshotCreateStep: "Crear instantánea de máquina virtual [{{ .snapshot }}]"
VMSnapshotRecoverTask: "Restaurar instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
VMSnapshotRecoverStep: "Restaurar instantánea de máquina virtual [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Eliminar instantánea [{{ .snapshot }}] de la máquina virtual [{{ .vm }}]"
VMSnapshotDeleteStep: "Eliminar instantánea de máquina virtual [{{ .snapshot }}]"
# aplicación # aplicación
CustomAppStoreFileValid: "El paquete de la tienda de aplicaciones debe tener formato .tar.gz" CustomAppStoreFileValid: "El paquete de la tienda de aplicaciones debe tener formato .tar.gz"
AppBackup: 'Copia de seguridad de aplicación' AppBackup: 'Copia de seguridad de aplicación'
-42
View File
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "دریافت وضعیت برنامه ناموفق بود.
MasterNodePortNotAvailable: "بررسی اتصال پورت {{ .port }} گره {{ .name }} ناموفق بود. تنظیمات دیواره آتش/گروه امنیتی و وضعیت گره اصلی را بررسی کنید." MasterNodePortNotAvailable: "بررسی اتصال پورت {{ .port }} گره {{ .name }} ناموفق بود. تنظیمات دیواره آتش/گروه امنیتی و وضعیت گره اصلی را بررسی کنید."
ClusterMasterNotExist: "گره اصلی خوشه قطع شده است. گره‌های زیرین را حذف کنید." ClusterMasterNotExist: "گره اصلی خوشه قطع شده است. گره‌های زیرین را حذف کنید."
# ماشین مجازی
ErrVMNotFound: "ماشین مجازی وجود ندارد"
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
ErrVMCountLimit: "در نسخه Professional می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید."
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
ErrVMStoragePathInUse: "مسیر استخر ذخیره‌سازی قبلاً توسط [{{ .name }}] استفاده شده است"
ErrVMHelperNotFound: "1panel-kvm یافت نشد"
ErrVMBridgeManaged: "نام bridge از قبل توسط libvirt یا Docker مدیریت می‌شود"
ErrVMNetworkInUse: "شبکه در حال استفاده است و قابل حذف نیست"
ErrVMStorageInUse: "مخزن ذخیره‌سازی در حال استفاده است و قابل حذف نیست"
ErrVMStorageNameCannotChange: "نام مخزن ذخیره‌سازی قابل تغییر نیست"
ErrVMStoragePathCannotChange: "مسیر مخزن ذخیره‌سازی قابل تغییر نیست"
ErrVMRunning: "ماشین مجازی در حال اجرا است"
ErrVMLibvirtConnect: "اتصال به libvirt ناموفق بود: {{ .detail }}"
ErrVMPermission: "مجوز عملیات ماشین مجازی وجود ندارد: {{ .detail }}"
ErrVMNetworkNotFound: "شبکه ماشین مجازی وجود ندارد: {{ .detail }}"
ErrVMInvalidPath: "مسیر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMInvalidInput: "پارامتر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMCommandFailed: "اجرای دستور ماشین مجازی ناموفق بود: {{ .err }}"
ErrVMDiskResizeBlocked: "دیسک شرایط افزایش ظرفیت را ندارد یا دستگاه دیگری به آن ارجاع می‌دهد. وضعیت دیسک را بررسی کنید."
ErrVMDiskReferenced: "این دیسک مجازی توسط ماشین مجازی یا دستگاه دیگری استفاده می‌شود؛ افزایش ظرفیت فعلاً پشتیبانی نمی‌شود."
ErrVMDiskReferenceCheck: "بررسی ارجاع‌های دیسک مجازی کامل نشد. افزایش ظرفیت ممکن نیست؛ دیسک‌های سایر ماشین‌های مجازی را بررسی کنید."
ErrVMSnapshotMetadataSync: "اسنپ‌شات بازیابی شد، اما همگام‌سازی اطلاعات دیسک ناموفق بود. تازه‌سازی و بررسی کنید؛ نیازی به بازیابی دوباره اسنپ‌شات نیست."
VMCreateTask: "ایجاد ماشین مجازی [{{ .name }}]"
VMCreateFromTemplateTask: "ایجاد ماشین مجازی [{{ .name }}] از قالب"
VMUpdateTask: "به‌روزرسانی ماشین مجازی [{{ .name }}]"
VMDiskResizeTask: "افزایش ظرفیت دیسک [{{ .disk }}] ماشین مجازی [{{ .name }}]"
VMRenameTask: "تغییر نام ماشین مجازی [{{ .name }}] به [{{ .newName }}]"
VMDeleteTask: "حذف ماشین مجازی [{{ .name }}]"
VMTemplateCreateTask: "ذخیره ماشین مجازی [{{ .name }}] به عنوان قالب [{{ .template }}]"
VMTemplateDeleteTask: "حذف قالب ماشین مجازی [{{ .name }}]"
VMForceDeleteMetadataOnly: "1panel-kvm پیدا نشد، فقط فراداده ماشین مجازی [{{ .name }}] به اجبار حذف می‌شود"
VMForceDeleteMetadataAfterLibvirtFailed: "حذف ماشین مجازی [{{ .name }}] از libvirt ناموفق بود، فراداده به اجبار حذف می‌شود: {{ .err }}"
VMForceDeleteDiskFailed: "حذف اجباری فایل دیسک ماشین مجازی [{{ .path }}] ناموفق بود: {{ .err }}"
VMForceDeleteDiskSuccess: "فایل دیسک ماشین مجازی [{{ .path }}] به اجبار حذف شد"
VMSnapshotCreateTask: "ایجاد اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotCreateStep: "ایجاد اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
VMSnapshotRecoverTask: "بازیابی اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotRecoverStep: "بازیابی اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
VMSnapshotDeleteTask: "حذف اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotDeleteStep: "حذف اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
# برنامه # برنامه
CustomAppStoreFileValid: "بسته فروشگاه برنامه باید با فرمت .tar.gz باشد" CustomAppStoreFileValid: "بسته فروشگاه برنامه باید با فرمت .tar.gz باشد"
AppBackup: 'پشتیبان‌گیری از برنامه' AppBackup: 'پشتیبان‌گیری از برنامه'
-42
View File
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "アプリケーションのステータス取得が異常
MasterNodePortNotAvailable: "ノード {{ .name }} のポート {{ .port }} の接続性検証が失敗しました。ファイアウォール/セキュリティグループの設定とマスターノードのステータスを確認してください。" MasterNodePortNotAvailable: "ノード {{ .name }} のポート {{ .port }} の接続性検証が失敗しました。ファイアウォール/セキュリティグループの設定とマスターノードのステータスを確認してください。"
ClusterMasterNotExist: "クラスタのマスターノードが切断されています。子ノードを削除してください。" ClusterMasterNotExist: "クラスタのマスターノードが切断されています。子ノードを削除してください。"
# 仮想マシン
ErrVMNotFound: "仮想マシンが存在しません"
ErrVMAlreadyExists: "仮想マシンは既に存在します"
ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。"
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
ErrVMHelperNotFound: "1panel-kvm が見つかりません"
ErrVMBridgeManaged: "ブリッジ名はすでに libvirt または Docker によって管理されています"
ErrVMNetworkInUse: "ネットワークは使用中のため削除できません"
ErrVMStorageInUse: "ストレージプールは使用中のため削除できません"
ErrVMStorageNameCannotChange: "ストレージプール名は変更できません"
ErrVMStoragePathCannotChange: "ストレージプールのパスは変更できません"
ErrVMRunning: "仮想マシンは実行中です"
ErrVMLibvirtConnect: "libvirt への接続に失敗しました: {{ .detail }}"
ErrVMPermission: "仮想マシンを操作する権限がありません: {{ .detail }}"
ErrVMNetworkNotFound: "仮想マシンネットワークが存在しません: {{ .detail }}"
ErrVMInvalidPath: "仮想マシンのパスが無効です: {{ .detail }}"
ErrVMInvalidInput: "仮想マシンのパラメータが無効です: {{ .detail }}"
ErrVMCommandFailed: "仮想マシンコマンドの実行に失敗しました: {{ .err }}"
ErrVMDiskResizeBlocked: "ディスクが拡張条件を満たしていないか、他のデバイスから参照されています。ディスクの状態を確認してください。"
ErrVMDiskReferenced: "この仮想ディスクは他の仮想マシンまたはデバイスで使用されているため、現在は拡張できません。"
ErrVMDiskReferenceCheck: "仮想ディスクの参照を確認できないため拡張できません。他の仮想マシンのディスク状態を確認してください。"
ErrVMSnapshotMetadataSync: "スナップショットは復元されましたが、ディスク情報の同期に失敗しました。更新して確認してください。再度復元する必要はありません。"
VMCreateTask: "仮想マシン [{{ .name }}] を作成"
VMCreateFromTemplateTask: "テンプレートから仮想マシン [{{ .name }}] を作成"
VMUpdateTask: "仮想マシン [{{ .name }}] を更新"
VMDiskResizeTask: "仮想マシン [{{ .name }}] のディスク [{{ .disk }}] を拡張"
VMRenameTask: "仮想マシン [{{ .name }}] を [{{ .newName }}] にリネーム"
VMDeleteTask: "仮想マシン [{{ .name }}] を削除"
VMTemplateCreateTask: "仮想マシン [{{ .name }}] をテンプレート [{{ .template }}] として保存"
VMTemplateDeleteTask: "仮想マシンテンプレート [{{ .name }}] を削除"
VMForceDeleteMetadataOnly: "1panel-kvm が見つかりません。仮想マシン [{{ .name }}] のメタデータのみを強制削除します"
VMForceDeleteMetadataAfterLibvirtFailed: "libvirt から仮想マシン [{{ .name }}] を削除できませんでした。メタデータを強制削除します: {{ .err }}"
VMForceDeleteDiskFailed: "仮想マシンディスクファイル [{{ .path }}] の強制削除に失敗しました: {{ .err }}"
VMForceDeleteDiskSuccess: "仮想マシンディスクファイル [{{ .path }}] を強制削除しました"
VMSnapshotCreateTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を作成"
VMSnapshotCreateStep: "仮想マシンのスナップショット [{{ .snapshot }}] を作成"
VMSnapshotRecoverTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を復元"
VMSnapshotRecoverStep: "仮想マシンのスナップショット [{{ .snapshot }}] を復元"
VMSnapshotDeleteTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を削除"
VMSnapshotDeleteStep: "仮想マシンのスナップショット [{{ .snapshot }}] を削除"
# アプリ # アプリ
CustomAppStoreFileValid: "アプリストアパッケージは .tar.gz 形式である必要があります" CustomAppStoreFileValid: "アプリストアパッケージは .tar.gz 形式である必要があります"
AppBackup: 'アプリケーションバックアップ' AppBackup: 'アプリケーションバックアップ'
-42
View File
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "애플리케이션 상태 획득이 비정상입니다. 노
MasterNodePortNotAvailable: "노드 {{ .name }} 포트 {{ .port }} 연결성 검증에 실패했습니다. 방화벽/보안 그룹 설정 및 마스터 노드 상태를 확인하세요." MasterNodePortNotAvailable: "노드 {{ .name }} 포트 {{ .port }} 연결성 검증에 실패했습니다. 방화벽/보안 그룹 설정 및 마스터 노드 상태를 확인하세요."
ClusterMasterNotExist: "클러스터의 마스터 노드가 연결이 끊어졌습니다. 자식 노드를 삭제하세요." ClusterMasterNotExist: "클러스터의 마스터 노드가 연결이 끊어졌습니다. 자식 노드를 삭제하세요."
# 가상 머신
ErrVMNotFound: "가상 머신이 존재하지 않습니다"
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요."
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
ErrVMBridgeManaged: "브리지 이름이 이미 libvirt 또는 Docker에서 관리되고 있습니다"
ErrVMNetworkInUse: "네트워크가 사용 중이므로 삭제할 수 없습니다"
ErrVMStorageInUse: "스토리지 풀이 사용 중이므로 삭제할 수 없습니다"
ErrVMStorageNameCannotChange: "스토리지 풀 이름은 변경할 수 없습니다"
ErrVMStoragePathCannotChange: "스토리지 풀 경로는 변경할 수 없습니다"
ErrVMRunning: "가상 머신이 실행 중입니다"
ErrVMLibvirtConnect: "libvirt 연결 실패: {{ .detail }}"
ErrVMPermission: "가상 머신 작업 권한이 없습니다: {{ .detail }}"
ErrVMNetworkNotFound: "가상 머신 네트워크가 존재하지 않습니다: {{ .detail }}"
ErrVMInvalidPath: "가상 머신 경로가 유효하지 않습니다: {{ .detail }}"
ErrVMInvalidInput: "가상 머신 매개변수가 유효하지 않습니다: {{ .detail }}"
ErrVMCommandFailed: "가상 머신 명령 실행 실패: {{ .err }}"
ErrVMDiskResizeBlocked: "디스크가 확장 조건을 충족하지 않거나 다른 장치에서 참조하고 있습니다. 디스크 상태를 확인하세요."
ErrVMDiskReferenced: "다른 가상 머신 또는 장치가 이 가상 디스크를 사용 중이므로 현재 확장할 수 없습니다."
ErrVMDiskReferenceCheck: "가상 디스크 참조를 확인할 수 없어 확장할 수 없습니다. 다른 가상 머신의 디스크 상태를 확인하세요."
ErrVMSnapshotMetadataSync: "스냅샷은 복원되었지만 디스크 정보 동기화에 실패했습니다. 새로 고쳐 확인하세요. 스냅샷을 다시 복원할 필요는 없습니다."
VMCreateTask: "가상 머신 [{{ .name }}] 생성"
VMCreateFromTemplateTask: "템플릿에서 가상 머신 [{{ .name }}] 생성"
VMUpdateTask: "가상 머신 [{{ .name }}] 업데이트"
VMDiskResizeTask: "가상 머신 [{{ .name }}]의 디스크 [{{ .disk }}] 확장"
VMRenameTask: "가상 머신 [{{ .name }}]을(를) [{{ .newName }}](으)로 이름 변경"
VMDeleteTask: "가상 머신 [{{ .name }}] 삭제"
VMTemplateCreateTask: "가상 머신 [{{ .name }}]을(를) 템플릿 [{{ .template }}](으)로 저장"
VMTemplateDeleteTask: "가상 머신 템플릿 [{{ .name }}] 삭제"
VMForceDeleteMetadataOnly: "1panel-kvm을 찾을 수 없어 가상 머신 [{{ .name }}] 메타데이터만 강제 삭제합니다"
VMForceDeleteMetadataAfterLibvirtFailed: "libvirt에서 가상 머신 [{{ .name }}] 삭제 실패, 메타데이터 강제 삭제: {{ .err }}"
VMForceDeleteDiskFailed: "가상 머신 디스크 파일 [{{ .path }}] 강제 삭제 실패: {{ .err }}"
VMForceDeleteDiskSuccess: "가상 머신 디스크 파일 [{{ .path }}]을(를) 강제 삭제했습니다"
VMSnapshotCreateTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 생성"
VMSnapshotCreateStep: "가상 머신 스냅샷 [{{ .snapshot }}] 생성"
VMSnapshotRecoverTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 복구"
VMSnapshotRecoverStep: "가상 머신 스냅샷 [{{ .snapshot }}] 복구"
VMSnapshotDeleteTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 삭제"
VMSnapshotDeleteStep: "가상 머신 스냅샷 [{{ .snapshot }}] 삭제"
# 앱 # 앱
CustomAppStoreFileValid: "앱 스토어 패키지는 .tar.gz 형식이어야 합니다" CustomAppStoreFileValid: "앱 스토어 패키지는 .tar.gz 형식이어야 합니다"
AppBackup: '애플리케이션 백업' AppBackup: '애플리케이션 백업'
-42
View File
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "ດຶງສະຖານະແອັບບໍ່ສຳເ
MasterNodePortNotAvailable: "ການກວດສອບການເຊື່ອມຕໍ່ຂອງໂນດ {{ .name }} ຜ່ານພອດ {{ .port }} ລົ້ມເຫຼວ. ກະລຸນາກວດສອບການຕັ້ງຄ່າໄຟຣ໌ວໍ/ກຸ່ມຄວາມປອດໄພ ແລະ ສະຖານະໂນດຫຼັກ." MasterNodePortNotAvailable: "ການກວດສອບການເຊື່ອມຕໍ່ຂອງໂນດ {{ .name }} ຜ່ານພອດ {{ .port }} ລົ້ມເຫຼວ. ກະລຸນາກວດສອບການຕັ້ງຄ່າໄຟຣ໌ວໍ/ກຸ່ມຄວາມປອດໄພ ແລະ ສະຖານະໂນດຫຼັກ."
ClusterMasterNotExist: "ໂນດຫຼັກຂອງຄລັສເຕີຖືກຕັດການເຊື່ອມຕໍ່. ກະລຸນາລຶບໂນດຍ່ອຍ." ClusterMasterNotExist: "ໂນດຫຼັກຂອງຄລັສເຕີຖືກຕັດການເຊື່ອມຕໍ່. ກະລຸນາລຶບໂນດຍ່ອຍ."
# virtual machine
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ."
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
ErrVMBridgeManaged: "ຊື່ບຣິດຈ໌ຖືກຈັດການໂດຍ libvirt ຫຼື Docker ແລ້ວ"
ErrVMNetworkInUse: "ເຄືອຂ່າຍກຳລັງຖືກໃຊ້ງານ ແລະ ບໍ່ສາມາດລຶບໄດ້"
ErrVMStorageInUse: "ພູນຈັດເກັບກຳລັງຖືກໃຊ້ງານ ແລະ ບໍ່ສາມາດລຶບໄດ້"
ErrVMStorageNameCannotChange: "ບໍ່ສາມາດປ່ຽນຊື່ພູນຈັດເກັບໄດ້"
ErrVMStoragePathCannotChange: "ບໍ່ສາມາດປ່ຽນເສັ້ນທາງພູນຈັດເກັບໄດ້"
ErrVMRunning: "ເຄື່ອງສະເໝືອນກຳລັງເຮັດວຽກ"
ErrVMLibvirtConnect: "ເຊື່ອມຕໍ່ກັບ libvirt ບໍ່ສຳເລັດ: {{ .detail }}"
ErrVMPermission: "ບໍ່ມີສິດດຳເນີນການກັບເຄື່ອງສະເໝືອນ: {{ .detail }}"
ErrVMNetworkNotFound: "ບໍ່ມີເຄືອຂ່າຍເຄື່ອງສະເໝືອນ: {{ .detail }}"
ErrVMInvalidPath: "ເສັ້ນທາງເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMInvalidInput: "ພາລາມິເຕີເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMCommandFailed: "ຄຳສັ່ງເຄື່ອງສະເໝືອນລົ້ມເຫຼວ: {{ .err }}"
ErrVMDiskResizeBlocked: "ດິສກ໌ບໍ່ຕອບສະໜອງເງື່ອນໄຂການຂະຫຍາຍ ຫຼື ຖືກອ້າງອີງໂດຍອຸປະກອນອື່ນ. ກະລຸນາກວດສອບສະຖານະດິສກ໌."
ErrVMDiskReferenced: "ດິສກ໌ເສມືອນນີ້ຖືກໃຊ້ໂດຍເຄື່ອງເສມືອນ ຫຼື ອຸປະກອນອື່ນ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ."
ErrVMDiskReferenceCheck: "ບໍ່ສາມາດກວດສອບການອ້າງອີງດິສກ໌ເສມືອນໄດ້. ຍັງຂະຫຍາຍບໍ່ໄດ້; ກະລຸນາກວດສອບດິສກ໌ຂອງເຄື່ອງເສມືອນອື່ນ."
ErrVMSnapshotMetadataSync: "ກູ້ຄືນສະແນັບຊັອດແລ້ວ ແຕ່ຊິງຂໍ້ມູນດິສກ໌ບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໃໝ່ ແລະ ກວດສອບ; ບໍ່ຈຳເປັນຕ້ອງກູ້ຄືນສະແນັບຊັອດອີກ."
VMCreateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMCreateFromTemplateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກແມ່ແບບ"
VMUpdateTask: "ອັບເດດເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMDiskResizeTask: "ຂະຫຍາຍດິສກ໌ [{{ .disk }}] ຂອງເຄື່ອງເສມືອນ [{{ .name }}]"
VMRenameTask: "ປ່ຽນຊື່ເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນ [{{ .newName }}]"
VMDeleteTask: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMTemplateCreateTask: "ບັນທຶກເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນແມ່ແບບ [{{ .template }}]"
VMTemplateDeleteTask: "ລຶບແມ່ແບບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMForceDeleteMetadataOnly: "ບໍ່ພົບ 1panel-kvm, ຈະບັງຄັບລຶບສະເພາະເມຕາດາຕາຂອງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMForceDeleteMetadataAfterLibvirtFailed: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກ libvirt ບໍ່ສຳເລັດ, ຈະບັງຄັບລຶບເມຕາດາຕາ: {{ .err }}"
VMForceDeleteDiskFailed: "ບັງຄັບລຶບໄຟລ໌ດິສກ໌ເຄື່ອງສະເໝືອນ [{{ .path }}] ບໍ່ສຳເລັດ: {{ .err }}"
VMForceDeleteDiskSuccess: "ບັງຄັບລຶບໄຟລ໌ດິສກ໌ເຄື່ອງສະເໝືອນ [{{ .path }}] ສຳເລັດແລ້ວ"
VMSnapshotCreateTask: "ສ້າງສະແນັບຊັອດ [{{ .snapshot }}] ໃຫ້ເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotCreateStep: "ສ້າງສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
VMSnapshotRecoverTask: "ກູ້ຄືນສະແນັບຊັອດ [{{ .snapshot }}] ໃຫ້ເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotRecoverStep: "ກູ້ຄືນສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
VMSnapshotDeleteTask: "ລຶບສະແນັບຊັອດ [{{ .snapshot }}] ຂອງເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotDeleteStep: "ລຶບສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
# app # app
CustomAppStoreFileValid: "ແພັກເກດຮ້ານຄ້າແອັບຕ້ອງເປັນຮູບແບບ .tar.gz" CustomAppStoreFileValid: "ແພັກເກດຮ້ານຄ້າແອັບຕ້ອງເປັນຮູບແບບ .tar.gz"
AppBackup: 'ສຳຮອງແອັບ' AppBackup: 'ສຳຮອງແອັບ'

Some files were not shown because too many files have changed in this diff Show More