mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 16:00:29 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486cd5d0ad | ||
|
|
00ce5a7b36 | ||
|
|
c17f055e06 | ||
|
|
f22df4b29c | ||
|
|
b9bab90a14 | ||
|
|
ce14a104cc | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 |
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||
total, list, err := containerService.Page(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -249,10 +249,9 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
||||
// @Success 200 {object} dto.ContainerStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param containersOnly query boolean false "Only count containers"
|
||||
// @Router /containers/status [get]
|
||||
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 {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -416,14 +415,9 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
||||
// @Success 200 {array} dto.ContainerListStats
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||
// @Router /containers/list/stats [get]
|
||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||
var ids []string
|
||||
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)
|
||||
data, err := containerService.ContainerListStats()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -970,12 +964,3 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
||||
|
||||
// @Tags Database Redis
|
||||
// @Summary Install redis-cli
|
||||
// @Success 200
|
||||
// @Accept json
|
||||
// @Param request body dto.RedisCliInstall true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/redis/install/cli [post]
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Database Redis
|
||||
|
||||
+79
-138
@@ -4,11 +4,9 @@ import (
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
@@ -90,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/base [post]
|
||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||
data, err := forwardingService.LoadBaseInfo()
|
||||
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -111,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := forwardingService.SearchRules(request)
|
||||
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -262,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Adopt an external firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleAdopt true "request"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/adopt [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
|
||||
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleAdopt
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Queue firewall rule creation
|
||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||
@@ -309,69 +285,9 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Preview firewall rule synchronization
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync/preview [post]
|
||||
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
||||
var request dto.FirewallRuleSyncRequest
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Load the currently executing firewall rule synchronization task
|
||||
// @Success 200 {object} dto.FirewallRuleSyncTask
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync/task [get]
|
||||
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
||||
result, err := firewallService.CurrentRuleSyncTask()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Synchronize firewall rules to a target backend
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleSyncResult
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
||||
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleSyncRequest
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Queue firewall rule deletion
|
||||
// @Description Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.
|
||||
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleDelete true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||
@@ -394,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update a managed unified firewall v2 rule
|
||||
// @Summary Update a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||
// @Success 200
|
||||
@@ -402,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/update [post]
|
||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [instanceKey]","formatEN":"update firewall rule [instanceKey]"}
|
||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
@@ -419,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Reorder a managed unified firewall v2 rule
|
||||
// @Summary Reorder a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleReorder true "request"
|
||||
// @Success 200
|
||||
@@ -427,35 +340,19 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/reorder [post]
|
||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [instanceKey]","formatEN":"reorder firewall rule [instanceKey]"}
|
||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleReorder
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
||||
if value == nil {
|
||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||
return false
|
||||
}
|
||||
*value = strings.TrimSpace(*value)
|
||||
if *value == "" {
|
||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||
var businessErr buserr.BusinessError
|
||||
isBusinessError := errors.As(err, &businessErr)
|
||||
@@ -464,21 +361,20 @@ func handleFirewallRuleError(c *gin.Context, err error) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrRuleStale):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
||||
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||
case isBusinessError && businessErr.Msg == "ErrFirewallRuleScopeChange":
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
|
||||
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
|
||||
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||
c.Abort()
|
||||
case errors.Is(err, filter.ErrVerificationFailed):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
||||
default:
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
@@ -501,7 +397,7 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Create firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||
// @Success 200
|
||||
@@ -523,7 +419,7 @@ func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||
// @Success 200
|
||||
@@ -545,7 +441,7 @@ func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Delete firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
||||
// @Description Removes whitelist configuration; existing firewall rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
||||
// @Success 200
|
||||
@@ -622,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Sync Docker port guard rules
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/sync [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
|
||||
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
|
||||
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate Docker port guard
|
||||
// @Accept json
|
||||
@@ -737,3 +618,63 @@ func handleDockerPortGuardError(c *gin.Context, err error) {
|
||||
}
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List firewall rule backups
|
||||
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||
// @Success 200 {object} dto.FirewallRuleBackups
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/backups [get]
|
||||
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Initialize, repair or bind one firewall address family
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/family/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||
var request dto.FirewallFamilyOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateFamily(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall IPv6 support
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||
var request dto.FirewallIPv6Operation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateIPv6(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
||||
Port int64 `json:"port"`
|
||||
}
|
||||
|
||||
type RedisCliInstall struct {
|
||||
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||
}
|
||||
|
||||
// mysql
|
||||
type MysqlDBSearch struct {
|
||||
PageInfo
|
||||
|
||||
+91
-127
@@ -2,11 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
)
|
||||
|
||||
type FirewallSubsystemStatus struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Backend string `json:"backend"`
|
||||
ConflictBackend string `json:"conflictBackend,omitempty"`
|
||||
@@ -18,7 +20,6 @@ type FirewallSubsystemStatus struct {
|
||||
PingStatus string `json:"pingStatus"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
SyncError string `json:"syncError,omitempty"`
|
||||
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
@@ -49,12 +50,12 @@ type FirewallBackendOption struct {
|
||||
}
|
||||
|
||||
type FirewallBackendFamilyStatus struct {
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
ForwardPolicy string `json:"forwardPolicy,omitempty"`
|
||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||
Partial bool `json:"partial"`
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallBackendGroup struct {
|
||||
@@ -64,6 +65,7 @@ type FirewallBackendGroup struct {
|
||||
}
|
||||
|
||||
type FirewallSettings struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
@@ -92,6 +94,17 @@ type FirewallBackendOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||
}
|
||||
|
||||
type FirewallIPv6Operation struct {
|
||||
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type FirewallFamilyOperation struct {
|
||||
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||
}
|
||||
|
||||
type FilterChainOperation struct {
|
||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||
@@ -104,42 +117,55 @@ type FilterChainOperationResponse struct {
|
||||
}
|
||||
|
||||
type FirewallInitializationTask struct {
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallSystemPort = firewall.SystemPort
|
||||
|
||||
type FirewallRuleInventoryResponse struct {
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
ManagedTotal int64 `json:"managedTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackup struct {
|
||||
Name string `json:"name"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
RuleCount int `json:"ruleCount"`
|
||||
ModifiedAt int64 `json:"modifiedAt"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackups struct {
|
||||
Directory string `json:"directory"`
|
||||
Files []FirewallRuleBackup `json:"files"`
|
||||
}
|
||||
|
||||
type FirewallRuleResetResponse struct {
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
BackupPath string `json:"backupPath"`
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type FirewallRuleReset struct {
|
||||
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type FirewallRuleInventory struct {
|
||||
Refresh bool `json:"refresh,omitempty"`
|
||||
PageInfo
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
|
||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||
}
|
||||
|
||||
type FirewallNativeDetail struct {
|
||||
@@ -150,6 +176,7 @@ type FirewallNativeDetail struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardBase struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
IsExist bool `json:"isExist"`
|
||||
@@ -162,6 +189,7 @@ type DockerPortGuardBase struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardFamilyStatus struct {
|
||||
Partial bool `json:"partial"`
|
||||
State string `json:"state"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Initialized bool `json:"initialized"`
|
||||
@@ -182,11 +210,8 @@ type DockerPortGuardEndpoint struct {
|
||||
Application string `json:"application,omitempty"`
|
||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
NativeAction string `json:"nativeAction,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
Effective bool `json:"effective"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TrafficPath string `json:"trafficPath"`
|
||||
ManagementTarget string `json:"managementTarget"`
|
||||
ManagementReason string `json:"managementReason,omitempty"`
|
||||
@@ -223,6 +248,7 @@ type DockerPortGuardEndpointIdentity struct {
|
||||
|
||||
type DockerPortGuardPolicyBatch struct {
|
||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||
Import bool `json:"import"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicyBatchDelete struct {
|
||||
@@ -231,31 +257,27 @@ type DockerPortGuardPolicyBatchDelete struct {
|
||||
|
||||
type DockerPortGuardPolicy struct {
|
||||
DockerPortGuardEndpointIdentity
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
|
||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||
}
|
||||
|
||||
type DockerPortGuardOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleAdopt struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
InstanceKey string `json:"instanceKey,omitempty" validate:"omitempty,max=128"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
Marker string `json:"marker,omitempty" validate:"max=256"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateItem struct {
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
SourceID string `json:"sourceID"`
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreate struct {
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Initialize bool `json:"initialize"`
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateResponse struct {
|
||||
@@ -274,66 +296,13 @@ type FirewallRuleCreateFailure struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncRequest struct {
|
||||
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||
ResetSource bool `json:"resetSource"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncItem struct {
|
||||
SourceUUID string `json:"sourceUUID"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||
Status firewallsync.Status `json:"status"`
|
||||
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncPreview struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||
TargetProvider filter.Provider `json:"targetProvider"`
|
||||
Total int `json:"total"`
|
||||
Ready int `json:"ready"`
|
||||
Existing int `json:"existing"`
|
||||
Removed int `json:"removed"`
|
||||
Blocked int `json:"blocked"`
|
||||
Items []FirewallRuleSyncItem `json:"items"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncResult struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||
TargetProvider filter.Provider `json:"targetProvider"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Skipped int `json:"skipped"`
|
||||
Removed int `json:"removed"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncTask struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Executing bool `json:"executing"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncFailure struct {
|
||||
SourceUUID string `json:"sourceUUID"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleDelete struct {
|
||||
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
|
||||
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
|
||||
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteItem struct {
|
||||
FirewallRuleDeleteTarget
|
||||
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteTarget struct {
|
||||
@@ -350,13 +319,13 @@ type FirewallRuleDeleteResponse struct {
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteFailure struct {
|
||||
Index int `json:"index"`
|
||||
UUID string `json:"uuid"`
|
||||
Error string `json:"error"`
|
||||
Index int `json:"index"`
|
||||
InstanceKey string `json:"instanceKey"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleUpdate struct {
|
||||
UUID string `json:"uuid" validate:"required,max=64"`
|
||||
FirewallRuleDeleteTarget
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
||||
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||
@@ -364,26 +333,21 @@ type FirewallRuleUpdate struct {
|
||||
}
|
||||
|
||||
type FirewallRuleReorder struct {
|
||||
UUID string `json:"uuid" validate:"required,max=64"`
|
||||
FirewallRuleDeleteTarget
|
||||
TargetPosition *int64 `json:"targetPosition"`
|
||||
Priority *int `json:"priority"`
|
||||
}
|
||||
|
||||
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
|
||||
p.Items = append(p.Items, item)
|
||||
switch item.Status {
|
||||
case firewallsync.StatusReady:
|
||||
p.Ready++
|
||||
p.Total++
|
||||
case firewallsync.StatusExisting:
|
||||
p.Existing++
|
||||
p.Total++
|
||||
case firewallsync.StatusRemove:
|
||||
p.Removed++
|
||||
case firewallsync.StatusBlocked:
|
||||
p.Blocked++
|
||||
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
||||
p.Total++
|
||||
}
|
||||
}
|
||||
type FirewallRuleExportItem struct {
|
||||
filter.FirewallRule
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallSubsystemBackup struct {
|
||||
Families []string `json:"families,omitempty"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||
}
|
||||
|
||||
@@ -24,15 +24,11 @@ type ForwardRule struct {
|
||||
|
||||
UsedStatus string `json:"usedStatus"`
|
||||
Description string `json:"description"`
|
||||
|
||||
IsDesired bool `json:"isDesired"`
|
||||
IsRuntime bool `json:"isRuntime"`
|
||||
SyncStatus string `json:"syncStatus"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperate struct {
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||
Import bool `json:"import"`
|
||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperation struct {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package model
|
||||
|
||||
type DockerPortGuardPolicy struct {
|
||||
BaseModel
|
||||
|
||||
UUID string `gorm:"uniqueIndex" json:"uuid"`
|
||||
ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
||||
Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
||||
HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
||||
HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
||||
Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
||||
Mode string `json:"mode"`
|
||||
Sources string `gorm:"type:text" json:"-"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
NativeAction string `gorm:"default:''" json:"-"`
|
||||
NativeRules string `gorm:"type:text" json:"-"`
|
||||
Sequence int64 `gorm:"default:0" json:"-"`
|
||||
}
|
||||
|
||||
type ForwardingRule struct {
|
||||
BaseModel
|
||||
|
||||
Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
||||
Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
||||
Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
||||
TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
||||
TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
||||
Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
||||
}
|
||||
|
||||
type FirewallRule struct {
|
||||
UUID string `gorm:"primaryKey" json:"uuid"`
|
||||
Family string `json:"family"`
|
||||
|
||||
Protocol string `json:"protocol"`
|
||||
SourceAddress string `json:"sourceAddress"`
|
||||
SourcePort string `json:"sourcePort"`
|
||||
DestinationAddress string `json:"destinationAddress"`
|
||||
DestinationPort string `json:"destinationPort"`
|
||||
Interface string `json:"interface"`
|
||||
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
|
||||
Action string `json:"action"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
|
||||
|
||||
Origin string `json:"origin"`
|
||||
Owner string `json:"owner"`
|
||||
Revision uint `gorm:"default:1" json:"revision"`
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type IDockerPortGuardRepo interface {
|
||||
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
|
||||
DeleteBatch(context.Context, []string) error
|
||||
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
|
||||
}
|
||||
|
||||
type DockerPortGuardRepo struct{}
|
||||
|
||||
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
|
||||
|
||||
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
||||
var policies []model.DockerPortGuardPolicy
|
||||
err := global.DB.WithContext(ctx).
|
||||
Where("read_only = ?", false).
|
||||
Order("family, host_ip, host_port, protocol").
|
||||
Find(&policies).Error
|
||||
return policies, err
|
||||
}
|
||||
|
||||
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
|
||||
return global.DB.WithContext(ctx).
|
||||
Where("read_only = ? AND uuid IN ?", false, uuids).
|
||||
Delete(&model.DockerPortGuardPolicy{}).Error
|
||||
}
|
||||
|
||||
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for i := range policies {
|
||||
policies[i].ReadOnly = false
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
|
||||
}).Create(&policies[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
|
||||
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
|
||||
)
|
||||
|
||||
type IFirewallRuleRepo interface {
|
||||
Create(context.Context, *model.FirewallRule) error
|
||||
GetByUUID(context.Context, string) (model.FirewallRule, error)
|
||||
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
||||
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
||||
DeleteWithRevision(context.Context, string, uint) error
|
||||
DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error
|
||||
SaveResetOrder(context.Context, []model.FirewallRule) error
|
||||
}
|
||||
|
||||
type FirewallRuleRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
||||
return &FirewallRuleRepo{}
|
||||
}
|
||||
|
||||
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
|
||||
return &FirewallRuleRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
|
||||
if err := prepareFirewallRule(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.dbFor(ctx).Create(rule).Error
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
|
||||
var rule model.FirewallRule
|
||||
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
|
||||
return rule, err
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
|
||||
var rules []model.FirewallRule
|
||||
db := r.dbFor(ctx).Model(&model.FirewallRule{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return rules, db.Find(&rules).Error
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
|
||||
updates = sanitizeRuleUpdates(updates)
|
||||
updates["revision"] = gorm.Expr("revision + 1")
|
||||
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
|
||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||
Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
|
||||
result := r.dbFor(ctx).
|
||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||
Delete(&model.FirewallRule{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) DeleteBatchWithRevision(ctx context.Context, rules []model.FirewallRule) map[string]error {
|
||||
failures := make(map[string]error)
|
||||
for start := 0; start < len(rules); start += 500 {
|
||||
batch := rules[start:min(start+500, len(rules))]
|
||||
ids := make([][]interface{}, 0, len(batch))
|
||||
for _, rule := range batch {
|
||||
ids = append(ids, []interface{}{rule.UUID, rule.Revision})
|
||||
failures[rule.UUID] = ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
var deleted []model.FirewallRule
|
||||
err := r.dbFor(ctx).Clauses(clause.Returning{Columns: []clause.Column{{Name: "uuid"}}}).
|
||||
Where("(uuid, revision) IN ?", ids).Delete(&deleted).Error
|
||||
if err != nil {
|
||||
for _, rule := range batch {
|
||||
failures[rule.UUID] = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, rule := range deleted {
|
||||
delete(failures, rule.UUID)
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) SaveResetOrder(ctx context.Context, rules []model.FirewallRule) error {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.dbFor(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, rule := range rules {
|
||||
result := tx.Model(&model.FirewallRule{}).
|
||||
Where("uuid = ? AND revision = ?", rule.UUID, rule.Revision).
|
||||
Updates(map[string]interface{}{"sequence": rule.Sequence, "priority": rule.Priority, "revision": gorm.Expr("revision + 1")})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
||||
return firewallDB(ctx, r.db)
|
||||
}
|
||||
|
||||
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
|
||||
return tx.WithContext(ctx)
|
||||
}
|
||||
if fallback == nil {
|
||||
fallback = global.DB
|
||||
}
|
||||
return fallback.WithContext(ctx)
|
||||
}
|
||||
|
||||
func prepareFirewallRule(rule *model.FirewallRule) error {
|
||||
if rule == nil {
|
||||
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
|
||||
}
|
||||
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
|
||||
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
|
||||
}
|
||||
if rule.UUID == "" {
|
||||
rule.UUID = uuid.NewString()
|
||||
}
|
||||
if rule.Revision == 0 {
|
||||
rule.Revision = 1
|
||||
}
|
||||
if rule.Origin == "" {
|
||||
rule.Origin = constant.FirewallRuleOriginCreated
|
||||
}
|
||||
if rule.Owner == "" {
|
||||
rule.Owner = constant.FirewallRuleSourceUser
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{}, len(updates)+1)
|
||||
for key, value := range updates {
|
||||
result[key] = value
|
||||
}
|
||||
delete(result, "id")
|
||||
delete(result, "uuid")
|
||||
delete(result, "revision")
|
||||
delete(result, "created_at")
|
||||
return result
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
|
||||
type IForwardingRuleRepo interface {
|
||||
List(context.Context) ([]model.ForwardingRule, error)
|
||||
CreateBatch(context.Context, []model.ForwardingRule) error
|
||||
DeleteBatch(context.Context, []uint) error
|
||||
}
|
||||
|
||||
type ForwardingRuleRepo struct{}
|
||||
|
||||
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
|
||||
|
||||
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
|
||||
var rules []model.ForwardingRule
|
||||
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error
|
||||
}
|
||||
|
||||
func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type SettingRepo struct{}
|
||||
@@ -19,12 +21,16 @@ type ISettingRepo interface {
|
||||
WithByKey(key string) DBOption
|
||||
|
||||
UpdateOrCreate(key, value string) error
|
||||
UpdateValues(map[string]string) error
|
||||
|
||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||
CreateDescription(data *model.CommonDescription) error
|
||||
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||
UpdateDescription(id string, val map[string]interface{}) error
|
||||
DelDescription(id string) error
|
||||
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||
WithDescriptionIDs(ids []string) DBOption
|
||||
WithByDescriptionID(id string) DBOption
|
||||
}
|
||||
|
||||
@@ -90,6 +96,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range values {
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", key).First(&setting).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||
var lists []model.CommonDescription
|
||||
db := global.DB.Model(&model.CommonDescription{})
|
||||
@@ -111,14 +136,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||
return global.DB.Create(data).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||
}).CreateInBatches(&descriptions, 100).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||
}
|
||||
func (s *SettingRepo) DelDescription(id string) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||
var deleted int64
|
||||
for start := 0; start < len(ids); start += 500 {
|
||||
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||
if emptyOnly {
|
||||
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||
}
|
||||
result := query.Delete(&model.CommonDescription{})
|
||||
deleted += result.RowsAffected
|
||||
if result.Error != nil {
|
||||
return deleted, result.Error
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||
}
|
||||
|
||||
+4
-117
@@ -4,11 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -20,12 +17,10 @@ import (
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
type AlertService struct{}
|
||||
@@ -358,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
}
|
||||
|
||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||
var disks []dto.DiskDTO
|
||||
excludes := map[string]struct{}{
|
||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
||||
infos := loadDiskInfo(true)
|
||||
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||
for _, item := range infos {
|
||||
disks = append(disks, dto.DiskDTO(item))
|
||||
}
|
||||
stdout, err := executeDiskCommand()
|
||||
if err != nil {
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
lines := strings.Split(stdout, "\n")
|
||||
var mounts []dto.AlertDiskInfo
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
mountPoint := strings.Join(fields[6:], " ")
|
||||
if shouldExclude(fields, mountPoint, excludes) {
|
||||
continue
|
||||
}
|
||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
)
|
||||
wg.Add(len(mounts))
|
||||
for i := 0; i < len(mounts); i++ {
|
||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
var itemData dto.DiskDTO
|
||||
itemData.Path = mount.Mount
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
default:
|
||||
state, err := disk.Usage(mount.Mount)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
return
|
||||
}
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(time.After(5*time.Second), mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
sort.Slice(disks, func(i, j int) bool {
|
||||
return disks[i].Path < disks[j].Path
|
||||
})
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func executeDiskCommand() (string, error) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
||||
}
|
||||
if err != nil {
|
||||
return stdout, err
|
||||
}
|
||||
var lines []string
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(fields[2], "K") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(mountPoint, "docker") {
|
||||
return true
|
||||
}
|
||||
_, excluded := excludes[mountPoint]
|
||||
return excluded
|
||||
}
|
||||
|
||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||
var (
|
||||
opts []repo.DBOption
|
||||
|
||||
@@ -1066,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||
return err
|
||||
}
|
||||
var errMsgs []string
|
||||
for _, item := range diskList {
|
||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
||||
errMsgs = append(errMsgs, errMsg)
|
||||
global.LOG.Errorf("%s", errMsg)
|
||||
if item.Total == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(errMsgs) > 0 {
|
||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
||||
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processSingleDisk(alert dto.AlertDTO) error {
|
||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
||||
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s", err.Error())
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||
return err
|
||||
}
|
||||
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||
commonTotal := float64(alert.Count)
|
||||
if alert.Cycle == 1 {
|
||||
commonTotal *= 1024 * 1024 * 1024
|
||||
}
|
||||
if usedTotal < commonTotal {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
params := createAlertDiskParams(path, usedStr)
|
||||
sender := NewAlertSender(alert, alert.Project)
|
||||
sender.ResourceSend(path, params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||
|
||||
@@ -58,10 +58,10 @@ type ContainerService struct{}
|
||||
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
||||
|
||||
type IContainerService interface {
|
||||
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
|
||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||
List() []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)
|
||||
ListNetwork() ([]dto.Options, error)
|
||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
@@ -80,7 +80,7 @@ type IContainerService interface {
|
||||
ContainerUpdate(req dto.ContainerOperate) error
|
||||
ContainerUpgrade(req dto.ContainerUpgrade) 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)
|
||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||
ContainerRename(req dto.ContainerRename) error
|
||||
@@ -92,7 +92,6 @@ type IContainerService interface {
|
||||
|
||||
Inspect(req dto.InspectReq) (string, error)
|
||||
DeleteNetwork(req dto.BatchDelete) error
|
||||
CleanNetworks() (*dto.NetworkCleanupTask, error)
|
||||
CreateNetwork(req dto.NetworkCreate) error
|
||||
DeleteVolume(req dto.BatchDelete) error
|
||||
CreateVolume(req dto.VolumeCreate) error
|
||||
@@ -113,7 +112,7 @@ func NewIContainerService() IContainerService {
|
||||
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()
|
||||
if err != nil {
|
||||
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.Add("label", req.Filters)
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, options)
|
||||
containers, err := client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -207,30 +206,27 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
|
||||
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
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer client.Close()
|
||||
if !containersOnly {
|
||||
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(ctx, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(ctx, client)
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
c := context.Background()
|
||||
|
||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(client)
|
||||
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
|
||||
data.ContainerCount = len(containers)
|
||||
for _, item := range containers {
|
||||
switch item.State {
|
||||
@@ -296,65 +292,27 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
options := 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)
|
||||
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
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))
|
||||
for i, item := range list {
|
||||
datas[i].ContainerID = item.ID
|
||||
}
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for worker := 0; worker < min(8, len(list)); worker++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for index := range jobs {
|
||||
if ctx.Err() != nil || list[index].State != "running" {
|
||||
continue
|
||||
}
|
||||
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
datas[index] = load(sampleCtx, list[index].ID)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
wg.Add(len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
go func(index int, item container.Summary) {
|
||||
datas[index] = loadCpuAndMem(client, item.ID)
|
||||
wg.Done()
|
||||
}(i, list[i])
|
||||
}
|
||||
dispatch:
|
||||
for index := range list {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break dispatch
|
||||
case jobs <- index:
|
||||
}
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
return datas
|
||||
return datas, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
name := ""
|
||||
switch req.PruneType {
|
||||
case "container":
|
||||
@@ -450,6 +403,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) 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()
|
||||
if req.WithTagAll {
|
||||
pruneFilters.Add("dangling", "false")
|
||||
@@ -473,10 +434,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||
}
|
||||
SpaceReclaimed = int(rep.SpaceReclaimed)
|
||||
case "network":
|
||||
_, err := client.NetworksPrune(context.Background(), pruneFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cleanUnusedNetworks(t, client)
|
||||
case "volume":
|
||||
versions, err := client.ServerVersion(context.Background())
|
||||
if err != nil {
|
||||
@@ -1757,11 +1715,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
|
||||
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{
|
||||
ContainerID: containerItem,
|
||||
}
|
||||
res, err := client.ContainerStats(ctx, containerItem, false)
|
||||
res, err := client.ContainerStats(context.Background(), containerItem, false)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
@@ -1984,11 +1942,11 @@ func transPortToStr(ports []container.Port) []string {
|
||||
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.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", composeProjectLabel)
|
||||
list, err := client.ContainerList(ctx, options)
|
||||
list, err := client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -2,16 +2,24 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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/i18n"
|
||||
"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/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
)
|
||||
|
||||
var networkCleanupSlot = make(chan struct{}, 1)
|
||||
|
||||
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
||||
currentInfo.SwapMemoryUsed = swapInfo.Used
|
||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||
|
||||
currentInfo.DiskData = loadDiskInfo()
|
||||
currentInfo.DiskData = loadDiskInfo(false)
|
||||
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||
|
||||
if ioOption == "all" {
|
||||
@@ -455,26 +455,9 @@ type diskInfo struct {
|
||||
Device string
|
||||
}
|
||||
|
||||
func loadDiskInfo() []dto.DiskInfo {
|
||||
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
|
||||
var datas []dto.DiskInfo
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
stdout, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return datas
|
||||
}
|
||||
}
|
||||
stdout := loadDiskMounts()
|
||||
lines := strings.Split(stdout, "\n")
|
||||
|
||||
var mounts []diskInfo
|
||||
@@ -522,43 +505,22 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(mount.Mount, false)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
case result := <-resultCh:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
|
||||
return
|
||||
}
|
||||
itemData.Total = result.state.Total
|
||||
itemData.Free = result.state.Free
|
||||
itemData.Used = result.state.Used
|
||||
itemData.UsedPercent = result.state.UsedPercent
|
||||
itemData.InodesTotal = result.state.InodesTotal
|
||||
itemData.InodesUsed = result.state.InodesUsed
|
||||
itemData.InodesFree = result.state.InodesFree
|
||||
itemData.InodesUsedPercent = result.state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
} else {
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
}
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
}(mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -569,6 +531,69 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
return datas
|
||||
}
|
||||
|
||||
var diskMountsMu sync.Mutex
|
||||
|
||||
func loadDiskMounts() string {
|
||||
if !diskMountsMu.TryLock() {
|
||||
return ""
|
||||
}
|
||||
resultCh := make(chan string, 1)
|
||||
go func() {
|
||||
var stdout string
|
||||
defer func() {
|
||||
diskMountsMu.Unlock()
|
||||
resultCh <- stdout
|
||||
}()
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
output, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
output, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stdout = output
|
||||
}()
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case stdout := <-resultCh:
|
||||
return stdout
|
||||
case <-timer.C:
|
||||
global.LOG.Error("load disk mounts timed out; df collection is still running")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(path, forceRefresh)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
|
||||
case result := <-resultCh:
|
||||
return result.state, result.err
|
||||
}
|
||||
}
|
||||
|
||||
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
ok, client := accelerator.New()
|
||||
if !ok {
|
||||
|
||||
@@ -8,6 +8,12 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"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/global"
|
||||
@@ -17,12 +23,15 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"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"
|
||||
)
|
||||
|
||||
type RedisService struct{}
|
||||
|
||||
// The CLI container is shared by all remote Redis databases on this node.
|
||||
var redisCliInstallMutex sync.Mutex
|
||||
|
||||
type IRedisService interface {
|
||||
UpdateConf(req dto.RedisConfUpdate) error
|
||||
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
||||
@@ -33,7 +42,7 @@ type IRedisService interface {
|
||||
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
||||
|
||||
CheckHasCli() bool
|
||||
InstallCli() error
|
||||
InstallCli(req dto.RedisCliInstall) (string, error)
|
||||
}
|
||||
|
||||
func NewIRedisService() IRedisService {
|
||||
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
|
||||
}
|
||||
|
||||
func (u *RedisService) CheckHasCli() bool {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
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
|
||||
installed, _ := u.checkCliInstalled()
|
||||
return installed
|
||||
}
|
||||
|
||||
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{
|
||||
TaskID: req.TaskID,
|
||||
Name: "1Panel-redis-cli-tools",
|
||||
Image: "redis:7.4.4",
|
||||
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 {
|
||||
|
||||
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
||||
if baseInfo.SwapMemoryTotal != 0 {
|
||||
baseInfo.SwapDetails = loadSwap()
|
||||
}
|
||||
disks := loadDiskInfo()
|
||||
disks := loadDiskInfo(false)
|
||||
for _, item := range disks {
|
||||
baseInfo.MaxSize += item.Free
|
||||
}
|
||||
|
||||
@@ -487,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
if err := controller.Handle(req.Operation, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Operation == "start" || req.Operation == "restart" {
|
||||
return RestoreDockerPortGuard(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -38,9 +38,8 @@ var (
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
|
||||
|
||||
settingRepo = repo.NewISettingRepo()
|
||||
forwardingRuleRepo = repo.NewIForwardingRuleRepo()
|
||||
backupRepo = repo.NewIBackupRepo()
|
||||
settingRepo = repo.NewISettingRepo()
|
||||
backupRepo = repo.NewIBackupRepo()
|
||||
|
||||
websiteRepo = repo.NewIWebsiteRepo()
|
||||
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
||||
|
||||
@@ -1310,7 +1310,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
||||
}
|
||||
|
||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||
return loadDiskInfo()
|
||||
return loadDiskInfo(false)
|
||||
}
|
||||
|
||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||
|
||||
+483
-881
File diff suppressed because it is too large
Load Diff
@@ -2,17 +2,16 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
@@ -39,9 +38,8 @@ const (
|
||||
)
|
||||
|
||||
type DockerPortGuardService struct {
|
||||
policies repo.IDockerPortGuardRepo
|
||||
runtime dockerfirewall.Runtime
|
||||
runtimeForBackend func(string) dockerfirewall.Runtime
|
||||
runtimeForBackend func(context.Context, string) dockerfirewall.Runtime
|
||||
client func() (*client.Client, error)
|
||||
version func(string) string
|
||||
}
|
||||
@@ -50,24 +48,35 @@ var dockerPortGuardServiceMu sync.Mutex
|
||||
|
||||
type IDockerPortGuardService interface {
|
||||
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
|
||||
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
|
||||
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
|
||||
Operate(context.Context, dto.DockerPortGuardOperation) error
|
||||
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
|
||||
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
|
||||
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
|
||||
Reconcile(context.Context) error
|
||||
Restore(context.Context) error
|
||||
}
|
||||
|
||||
func NewIDockerPortGuardService() IDockerPortGuardService {
|
||||
return newDockerPortGuardService()
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
|
||||
policies, err := s.policies.ListManaged(ctx)
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
policies := dockerGuardInventoryEndpoints(inventory)
|
||||
unavailable := func() dto.DockerPortGuardList {
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
base := s.runtimeStatus(s.guardRuntime(backend), backend)
|
||||
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||
base.Message = i18n.Get("ErrDockerFailed")
|
||||
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}
|
||||
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: policies}
|
||||
}
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
@@ -79,13 +88,13 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
|
||||
return unavailable(), nil
|
||||
}
|
||||
detectedBackend := dockerFirewallBackend(info)
|
||||
backend := selectedDockerFirewallBackend(detectedBackend)
|
||||
base := s.runtimeStatus(s.guardRuntime(backend), backend)
|
||||
backend = selectedDockerFirewallBackend(detectedBackend)
|
||||
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
annotateDockerEndpointManagement(endpoints, detectedBackend)
|
||||
annotateDockerEndpointManagement(ctx, endpoints, detectedBackend)
|
||||
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
|
||||
sort.Slice(endpoints, func(i, j int) bool {
|
||||
return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
|
||||
@@ -96,6 +105,24 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
|
||||
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
|
||||
backend := string(provider)
|
||||
if backend == "" {
|
||||
backend = selectedDockerFirewallBackend("")
|
||||
}
|
||||
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
|
||||
return dto.FirewallSubsystemBackup{}, filter.ErrInvalidRule
|
||||
}
|
||||
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemBackup{}, err
|
||||
}
|
||||
if inventory.Policies == nil {
|
||||
inventory.Policies = []dockerfirewall.Policy{}
|
||||
}
|
||||
return dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}, nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
@@ -117,7 +144,7 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.
|
||||
if info, infoErr := cli.Info(ctx); infoErr == nil {
|
||||
backend = dockerFirewallBackend(info)
|
||||
}
|
||||
annotateDockerEndpointManagement(endpoints, backend)
|
||||
annotateDockerEndpointManagement(ctx, endpoints, backend)
|
||||
return groupDockerGuardContainers(endpoints), nil
|
||||
}
|
||||
|
||||
@@ -126,34 +153,17 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
switch request.Operation {
|
||||
case "initialize":
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies, err := s.runtimePolicies(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runtime.Initialize(policies, inventory); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return s.initialize(ctx, request, nil)
|
||||
case "bind":
|
||||
runtime, _, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runtime.Bind(); err != nil {
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := errors.Join(runtime.Bind(families...), ctx.Err()); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
@@ -162,9 +172,9 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
||||
if s.runtime != nil {
|
||||
err = s.runtime.Unbind()
|
||||
} else {
|
||||
err = errors.Join(dockerfirewall.NewIptables().Unbind(), dockerfirewall.NewNftables().Unbind())
|
||||
err = errors.Join(dockerfirewall.NewIptables(ctx).Unbind(), dockerfirewall.NewNftables(ctx).Unbind())
|
||||
}
|
||||
if err != nil {
|
||||
if err = errors.Join(err, ctx.Err()); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
@@ -174,55 +184,22 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Operation != "initialize" {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("only Docker port guard initialization can be queued")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskDocker, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create Docker port guard initialization task: %w", err)
|
||||
if request.Operation != "initialize" {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
var runtime dockerfirewall.Runtime
|
||||
var backend string
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func(t *task.Task) error {
|
||||
var err error
|
||||
runtime, backend, err = s.runtimeForDocker(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
if request.BackupFile != "" {
|
||||
if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
t.Logf("backend=%s", backend)
|
||||
return nil
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeDockerGuardStep", "Docker"), func(t *task.Task) error {
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
policies, err := s.runtimePolicies(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Logf("backend=%s", backend)
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runtime.Initialize(policies, inventory)
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallPersistDockerGuardStep"), func(t *task.Task) error {
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("save Docker port guard initialization task: %w", err)
|
||||
}
|
||||
go func() { _ = taskItem.Execute() }()
|
||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||
return s.initialize(t.TaskCtx, request, t)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
|
||||
@@ -230,20 +207,34 @@ func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolic
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
labels := make([]string, len(uuids))
|
||||
for i, id := range uuids {
|
||||
labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id)
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, labels, func(ctx context.Context) error {
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error {
|
||||
ctx := t.TaskCtx
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.reconcileLocked(ctx)
|
||||
wanted := make(map[string]bool, len(uuids))
|
||||
for _, id := range uuids {
|
||||
wanted[id] = true
|
||||
}
|
||||
remaining := make([]dockerfirewall.Policy, 0, len(inventory.Policies))
|
||||
for _, policy := range inventory.Policies {
|
||||
if wanted[policy.UUID] {
|
||||
delete(wanted, policy.UUID)
|
||||
} else {
|
||||
remaining = append(remaining, policy)
|
||||
}
|
||||
}
|
||||
if len(wanted) > 0 {
|
||||
return filter.ErrRuleStale
|
||||
}
|
||||
return applyDockerPolicies(ctx, runtime, backend, inventory, remaining)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -252,7 +243,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||
}
|
||||
labels := make([]string, len(request.Policies))
|
||||
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies))
|
||||
policies := make([]dockerfirewall.Policy, 0, len(request.Policies))
|
||||
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
|
||||
count := 0
|
||||
for i, policy := range request.Policies {
|
||||
@@ -264,7 +255,7 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
|
||||
}
|
||||
if normalized.Mode == dockerfirewall.ModeAll {
|
||||
if normalized.Mode == dockerfirewall.ModeAll || normalized.Mode == dockerfirewall.ModeAcceptAll {
|
||||
count++
|
||||
} else {
|
||||
count += len(normalized.Sources)
|
||||
@@ -275,42 +266,142 @@ func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolic
|
||||
if count > filter.MaxAtomicExpansion {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||
}
|
||||
encoded, err := json.Marshal(normalized.Sources)
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
|
||||
}
|
||||
policies = append(policies, model.DockerPortGuardPolicy{
|
||||
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
|
||||
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
|
||||
Sources: string(encoded), Description: strings.TrimSpace(policy.Description),
|
||||
})
|
||||
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
|
||||
Family: normalized.Family, HostIP: normalized.HostIP, HostPort: normalized.HostPort, Protocol: normalized.Protocol,
|
||||
})
|
||||
normalized.UUID = uuid.NewString()
|
||||
policies = append(policies, normalized)
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, labels, func(ctx context.Context) error {
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, "", labels, func(t *task.Task) error {
|
||||
ctx := t.TaskCtx
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current := append([]dockerfirewall.Policy(nil), inventory.Policies...)
|
||||
if request.Import {
|
||||
backup := dto.FirewallSubsystemBackup{Provider: filter.Provider(backend), Docker: &dockerfirewall.PolicyInventory{Policies: policies}}
|
||||
merged, err := mergeDockerBackup(inventory, backup, backend, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = merged.Policies
|
||||
}
|
||||
byEndpoint := make(map[string]int, len(current))
|
||||
for i, policy := range current {
|
||||
byEndpoint[dockerPolicyEndpointKey(policy)] = i
|
||||
}
|
||||
for i := range policies {
|
||||
key := dockerPolicyEndpointKey(policies[i])
|
||||
index, exists := byEndpoint[key]
|
||||
if request.Import {
|
||||
if !exists || current[index].UUID != policies[i].UUID {
|
||||
labels[i] = ""
|
||||
continue
|
||||
}
|
||||
} else if exists {
|
||||
policies[i].UUID = current[index].UUID
|
||||
current[index] = policies[i]
|
||||
} else {
|
||||
byEndpoint[key] = len(current)
|
||||
current = append(current, policies[i])
|
||||
}
|
||||
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
|
||||
Family: policies[i].Family, HostIP: policies[i].HostIP, HostPort: policies[i].HostPort, Protocol: policies[i].Protocol,
|
||||
})
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
|
||||
if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.reconcileLocked(ctx)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func NewIDockerPortGuardService() IDockerPortGuardService {
|
||||
return newDockerPortGuardService()
|
||||
func (s *DockerPortGuardService) Restore(ctx context.Context) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
enabled, err := dockerPortGuardPersistedEnabled()
|
||||
if err != nil || !enabled {
|
||||
return err
|
||||
}
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup, err := readFirewallSubsystemBackup("docker-"+backend+".rules", "docker")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
||||
before := len(backup.Docker.Policies)
|
||||
backup.Docker.Policies = slices.DeleteFunc(backup.Docker.Policies, func(policy dockerfirewall.Policy) bool { return policy.Family == constant.FirewallFamilyIPv6 })
|
||||
logFirewallIPv6Skipped(nil, "Docker startup", before-len(backup.Docker.Policies))
|
||||
}
|
||||
missing := make(map[string]bool)
|
||||
needsBind := false
|
||||
for _, family := range families {
|
||||
status := runtime.Status(family)
|
||||
if status.Reason == dockerfirewall.ReasonInspectFailed {
|
||||
return fmt.Errorf("inspect Docker guard %s failed", family)
|
||||
}
|
||||
if status.Reason == dockerfirewall.ReasonCommandMissing {
|
||||
continue
|
||||
}
|
||||
missing[family] = !status.Initialized
|
||||
needsBind = needsBind || (status.Initialized && !status.Effective)
|
||||
}
|
||||
if !missing[dockerfirewall.FamilyIPv4] && !missing[dockerfirewall.FamilyIPv6] {
|
||||
if needsBind {
|
||||
return runtime.Bind(families...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, policy := range inventory.Policies {
|
||||
missing[policy.Family] = false
|
||||
}
|
||||
if inventory.RuleOrders == nil {
|
||||
inventory.RuleOrders = make(map[string][]int64)
|
||||
}
|
||||
for _, policy := range backup.Docker.Policies {
|
||||
if !missing[policy.Family] {
|
||||
continue
|
||||
}
|
||||
inventory.Policies = append(inventory.Policies, policy)
|
||||
key := policy.Family + "\x00" + policy.UUID
|
||||
inventory.RuleOrders[key] = backup.Docker.RuleOrders[key]
|
||||
}
|
||||
return runtime.Initialize(inventory.Policies, inventory, families...)
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string) dto.DockerPortGuardBase {
|
||||
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string, ipv6Enabled bool) dto.DockerPortGuardBase {
|
||||
ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
|
||||
ipv6 := runtime.Status(dockerfirewall.FamilyIPv6)
|
||||
var ipv6 dockerfirewall.FamilyStatus
|
||||
if ipv6Enabled {
|
||||
ipv6 = runtime.Status(dockerfirewall.FamilyIPv6)
|
||||
}
|
||||
version := "-"
|
||||
if s.version != nil {
|
||||
version = s.version(backend)
|
||||
@@ -320,83 +411,68 @@ func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, b
|
||||
name = "nftables-docker"
|
||||
}
|
||||
return dto.DockerPortGuardBase{
|
||||
IPv6Enabled: ipv6Enabled,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Backend: backend,
|
||||
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || ipv6.Reason != dockerfirewall.ReasonCommandMissing,
|
||||
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || (ipv6Enabled && ipv6.Reason != dockerfirewall.ReasonCommandMissing),
|
||||
Initialized: ipv4.Initialized || ipv6.Initialized,
|
||||
Bound: ipv4.Bound || ipv6.Bound,
|
||||
IPv4: dto.DockerPortGuardFamilyStatus{State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
||||
IPv6: dto.DockerPortGuardFamilyStatus{State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
||||
IPv4: dto.DockerPortGuardFamilyStatus{Partial: ipv4.Partial, State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
||||
IPv6: dto.DockerPortGuardFamilyStatus{Partial: ipv6.Partial, State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
||||
}
|
||||
}
|
||||
|
||||
func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.DockerPortGuardEndpoint {
|
||||
endpoints := make([]dto.DockerPortGuardEndpoint, 0, len(policies))
|
||||
for _, policy := range policies {
|
||||
sources := []string{}
|
||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
||||
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources,
|
||||
Description: policy.Description, TrafficPath: dockerTrafficPathUnknown,
|
||||
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
|
||||
})
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []model.DockerPortGuardPolicy, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
||||
byEndpoint := make(map[string]model.DockerPortGuardPolicy, len(policies))
|
||||
for _, policy := range policies {
|
||||
byEndpoint[fmt.Sprintf("%s|%s|%d|%s", policy.Family, policy.HostIP, policy.HostPort, policy.Protocol)] = policy
|
||||
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
||||
matched := make(map[int]bool, len(policies))
|
||||
byEndpoint := make(map[string]int, len(policies))
|
||||
for index, policy := range policies {
|
||||
key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
|
||||
if _, exists := byEndpoint[key]; !exists {
|
||||
byEndpoint[key] = index
|
||||
}
|
||||
}
|
||||
for i := range endpoints {
|
||||
key := fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol)
|
||||
policy, ok := byEndpoint[key]
|
||||
if !ok {
|
||||
key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00")
|
||||
index, exists := byEndpoint[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
sources := []string{}
|
||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
||||
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, sources
|
||||
endpoints[i].Description = policy.Description
|
||||
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard &&
|
||||
((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
|
||||
delete(byEndpoint, key)
|
||||
policy := policies[index]
|
||||
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources
|
||||
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
|
||||
matched[index] = true
|
||||
}
|
||||
orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint))
|
||||
for _, policy := range byEndpoint {
|
||||
sources := []string{}
|
||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
||||
orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: sources, Description: policy.Description,
|
||||
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
|
||||
ManagementReason: dockerReasonNoMatchingPath,
|
||||
})
|
||||
orphans := make([]dto.DockerPortGuardEndpoint, 0)
|
||||
for i, policy := range policies {
|
||||
if !matched[i] {
|
||||
orphans = append(orphans, policy)
|
||||
}
|
||||
}
|
||||
return endpoints, orphanPolicies
|
||||
return endpoints, orphans
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
|
||||
if s.client == nil || len(requested) == 0 {
|
||||
return nil
|
||||
return ctx.Err()
|
||||
}
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
return nil
|
||||
return ctx.Err()
|
||||
}
|
||||
defer cli.Close()
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
return ctx.Err()
|
||||
}
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||
if err != nil {
|
||||
return nil
|
||||
return ctx.Err()
|
||||
}
|
||||
annotateDockerEndpointManagement(ctx, endpoints, dockerFirewallBackend(info))
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
annotateDockerEndpointManagement(endpoints, dockerFirewallBackend(info))
|
||||
targets := make(map[string]string, len(endpoints))
|
||||
for _, endpoint := range endpoints {
|
||||
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -18,7 +18,10 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -28,106 +31,115 @@ type IFirewallSettingService interface {
|
||||
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
|
||||
Load(context.Context) (dto.FirewallSettings, error)
|
||||
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
|
||||
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
|
||||
}
|
||||
|
||||
type FirewallSettingService struct{}
|
||||
|
||||
var firewallWhitelistMu sync.Mutex
|
||||
|
||||
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
|
||||
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
current = append(current, request.Rule)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = append(current, request.Rule)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
|
||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current[index] = request.Rule
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current[index] = request.Rule
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||
if request.Rule == nil {
|
||||
return fmt.Errorf("select one firewall port whitelist rule to delete")
|
||||
}
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = slices.Delete(current, index, index+1)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Rule == nil {
|
||||
return filter.ErrInvalidRule
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = slices.Delete(current, index, index+1)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.FirewallSettings{}, err
|
||||
}
|
||||
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
|
||||
|
||||
installed := make(map[string]bool)
|
||||
for _, name := range lifecycle.InstalledProviders() {
|
||||
@@ -153,7 +165,7 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
||||
overview, err := loadSystemFirewallOverview(name, "base")
|
||||
overview, err := loadSystemFirewallOverview(name, "base", families)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
@@ -180,18 +192,18 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||
if option.Installed && name == result.Forwarding.Selected {
|
||||
manager, err := newForwardingAdapterFor(name)
|
||||
manager, err := newForwardingAdapterFor(ctx, name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else {
|
||||
status, statusErr := loadForwardingFirewallOverview(manager)
|
||||
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
||||
if statusErr != nil {
|
||||
option.Message = statusErr.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
}
|
||||
if name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||
if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
||||
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
||||
}
|
||||
@@ -228,20 +240,25 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
option.Active = false
|
||||
}
|
||||
if option.Active {
|
||||
guard := newDockerFirewallRuntime(name)
|
||||
ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6)
|
||||
option.Initialized = ipv4.Initialized || ipv6.Initialized
|
||||
option.Bound = ipv4.Bound || ipv6.Bound
|
||||
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
|
||||
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
|
||||
option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing
|
||||
option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing
|
||||
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
|
||||
guard := newDockerFirewallRuntime(ctx, name)
|
||||
for _, family := range families {
|
||||
status := guard.Status(family)
|
||||
option.Initialized = option.Initialized || status.Initialized
|
||||
option.Bound = option.Bound || status.Bound
|
||||
info := dto.FirewallBackendFamilyStatus{
|
||||
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
|
||||
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
|
||||
}
|
||||
if family == constant.FirewallFamilyIPv4 {
|
||||
option.IPv4 = info
|
||||
} else {
|
||||
option.IPv6 = info
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Docker.Options = append(result.Docker.Options, option)
|
||||
}
|
||||
var err error
|
||||
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
|
||||
result.PortWhitelist, err = loadPortWhitelistSetting()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -256,6 +273,10 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if request.Operation == "cleanup" {
|
||||
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
|
||||
return err
|
||||
}
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -273,13 +294,12 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
service := newFirewallService()
|
||||
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
|
||||
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||
return errors.Join(rulesErr, whitelistErr)
|
||||
return whitelistErr
|
||||
}
|
||||
return nil
|
||||
case "forwarding":
|
||||
return s.operateForwarding(request)
|
||||
return s.operateForwarding(ctx, request)
|
||||
case "docker":
|
||||
return s.operateDocker(ctx, request)
|
||||
default:
|
||||
@@ -287,6 +307,211 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
|
||||
}
|
||||
subsystem := ""
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
subsystem = firewallTaskHost
|
||||
case "forwarding":
|
||||
subsystem = firewallTaskForwarding
|
||||
case "docker":
|
||||
subsystem = firewallTaskDocker
|
||||
default:
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(families, request.Family) {
|
||||
return fmt.Errorf("IPv6 firewall support is disabled")
|
||||
}
|
||||
initialize := request.Operation != "bind"
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
required, err := firewall.RequiredPortWhitelist(ports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
firewallRuleMutationMu.Lock()
|
||||
if request.Backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.OperateFamily(request.Family, initialize, required)
|
||||
} else {
|
||||
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
|
||||
}
|
||||
firewallRuleMutationMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if initialize {
|
||||
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
|
||||
case "forwarding":
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
manager, err := newForwardingAdapter(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if manager.Name() != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := manager.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistForwardingRules(manager, rules); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||
default:
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if backend != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistDockerRules(backend, inventory); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if request.Status == constant.StatusEnable {
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(ipv4PortWhitelist(ports))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installed := lifecycle.InstalledProviders()
|
||||
for _, selection := range []struct{ subsystem, key string }{
|
||||
{"system", constant.FirewallSystemBackendKey},
|
||||
{"forwarding", constant.FirewallForwardingBackendKey},
|
||||
{"docker", constant.FirewallDockerBackendKey},
|
||||
} {
|
||||
backend, err := settingRepo.GetValueByKey(selection.key)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
backend = strings.ToLower(strings.TrimSpace(backend))
|
||||
if backend == "" {
|
||||
if selection.subsystem == "system" {
|
||||
if len(installed) == 0 {
|
||||
continue
|
||||
}
|
||||
client, err := NewSelectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backend = client.Name()
|
||||
} else {
|
||||
backend = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
|
||||
continue
|
||||
}
|
||||
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
|
||||
switch selection.subsystem {
|
||||
case "system":
|
||||
if backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.UnbindIPv6BaseChains()
|
||||
} else {
|
||||
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
|
||||
if err == nil {
|
||||
err = nftables_helper.PersistRuleset(t.TaskCtx)
|
||||
}
|
||||
}
|
||||
case "forwarding":
|
||||
var manager forwarding.Adapter
|
||||
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
|
||||
if err == nil {
|
||||
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
case "docker":
|
||||
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := t.TaskCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
|
||||
})
|
||||
}
|
||||
|
||||
func NewIFirewallSettingService() IFirewallSettingService {
|
||||
return &FirewallSettingService{}
|
||||
}
|
||||
@@ -297,9 +522,6 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "cleanup" {
|
||||
return cleanupSystemBackend(request.Backend)
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
if previous == "" {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
@@ -363,32 +585,21 @@ func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||
return client.Status()
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
|
||||
manager, err := newForwardingAdapterFor(request.Backend)
|
||||
if err != nil {
|
||||
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "cleanup" {
|
||||
if err := manager.Cleanup(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusDisable); err != nil {
|
||||
return err
|
||||
}
|
||||
recordForwardingSyncError(nil)
|
||||
return nil
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
detected, err := newForwardingAdapter()
|
||||
detected, err := newForwardingAdapter(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = detected.Name()
|
||||
}
|
||||
initialized, err := forwardingBackendInitialized(current)
|
||||
initialized, err := forwardingBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -400,14 +611,13 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
|
||||
return err
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
return newForwardingService().Enable()
|
||||
return newForwardingService().Enable(ctx)
|
||||
}
|
||||
recordForwardingSyncError(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
manager, err := newForwardingAdapterFor(backend)
|
||||
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
manager, err := newForwardingAdapterFor(ctx, backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
@@ -427,13 +637,7 @@ func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
guard := newDockerFirewallRuntime(request.Backend)
|
||||
if request.Operation == "cleanup" {
|
||||
if err := guard.Cleanup(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
}
|
||||
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
@@ -443,7 +647,7 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
|
||||
current = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
initialized, err := dockerGuardBackendInitialized(current)
|
||||
initialized, err := dockerGuardBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -469,8 +673,8 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
|
||||
return nil
|
||||
}
|
||||
|
||||
func dockerGuardBackendInitialized(backend string) (bool, error) {
|
||||
guard := newDockerFirewallRuntime(backend)
|
||||
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
guard := newDockerFirewallRuntime(ctx, backend)
|
||||
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
||||
initialized, err := guard.Initialized(family)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
||||
)
|
||||
|
||||
var (
|
||||
firewallRuleSyncTaskMu sync.Mutex
|
||||
firewallRuleSyncTaskID string
|
||||
)
|
||||
|
||||
type firewallDatabaseSyncAdapter interface {
|
||||
previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error)
|
||||
syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error)
|
||||
}
|
||||
|
||||
func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider, err := s.selectedProvider(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.syncPortWhitelist(ctx, provider, ports); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.removeTransferredSystemPortRules(ctx, provider, ports)
|
||||
}
|
||||
|
||||
func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
||||
switch strings.TrimSpace(request.Subsystem) {
|
||||
case "forwarding":
|
||||
service := s.forwardingSync
|
||||
if service == nil {
|
||||
service = newForwardingService()
|
||||
}
|
||||
return service.previewRuleSync(ctx, request)
|
||||
case "docker":
|
||||
service := s.dockerSync
|
||||
if service == nil {
|
||||
service = newDockerPortGuardService()
|
||||
}
|
||||
return service.previewRuleSync(ctx, request)
|
||||
}
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
_, rules, _, err := s.loadFirewallSyncRules(ctx, request)
|
||||
preview := dto.FirewallRuleSyncPreview{Subsystem: "system", TargetProvider: request.TargetProvider, Items: make([]dto.FirewallRuleSyncItem, 0, len(rules))}
|
||||
for _, rule := range rules {
|
||||
preview.Add(rule.FirewallRuleSyncItem)
|
||||
}
|
||||
return preview, err
|
||||
}
|
||||
|
||||
func (s *FirewallService) SyncRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
||||
switch strings.TrimSpace(request.Subsystem) {
|
||||
case "forwarding":
|
||||
service := s.forwardingSync
|
||||
if service == nil {
|
||||
service = newForwardingService()
|
||||
}
|
||||
return service.syncRules(ctx, request)
|
||||
case "docker":
|
||||
service := s.dockerSync
|
||||
if service == nil {
|
||||
service = newDockerPortGuardService()
|
||||
}
|
||||
return service.syncRules(ctx, request)
|
||||
default:
|
||||
return s.syncSystemRules(ctx, clientIP, request)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FirewallService) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) {
|
||||
firewallRuleSyncTaskMu.Lock()
|
||||
defer firewallRuleSyncTaskMu.Unlock()
|
||||
return currentFirewallRuleSyncTaskLocked()
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
return s.reconcileLocked(ctx)
|
||||
}
|
||||
|
||||
func (s *ForwardingService) Restore(ctx context.Context) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
enabled, err := s.forwardingEnabled()
|
||||
if err != nil || !enabled {
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
manager, err := s.clientFactory()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
stored, err := s.rules.List(ctx)
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
if err := s.initializeForwarding(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.ReplaceRules(forwardingRulesFromModels(stored))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ForwardingService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
||||
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncPreview{}, err
|
||||
}
|
||||
target, candidates, targetRules, _, err := s.loadRuleSyncCandidates(ctx, targetProvider)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncPreview{}, err
|
||||
}
|
||||
return forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules), nil
|
||||
}
|
||||
|
||||
func (s *ForwardingService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
|
||||
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncResult{}, err
|
||||
}
|
||||
target, candidates, targetRules, targetInitialized, err := s.loadRuleSyncCandidates(ctx, targetProvider)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncResult{}, err
|
||||
}
|
||||
preview := forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules)
|
||||
desired := make([]forwarding.Rule, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
if candidate.err == nil {
|
||||
desired = append(desired, candidate.rule)
|
||||
}
|
||||
}
|
||||
if preview.Blocked > 0 {
|
||||
return firewallSyncResult(preview, nil, false), nil
|
||||
}
|
||||
if len(desired) == 0 && !targetInitialized {
|
||||
return firewallSyncResult(preview, nil, true), nil
|
||||
}
|
||||
reconcileErr := func() error {
|
||||
if len(desired) > 0 {
|
||||
if err := s.persistForwardingEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.initializeForwarding(target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := target.ReplaceRules(desired); err != nil {
|
||||
return err
|
||||
}
|
||||
return verifyForwardingRuleSync(target, desired)
|
||||
}()
|
||||
result := firewallSyncResult(preview, reconcileErr, true)
|
||||
recordForwardingSyncError(reconcileErr)
|
||||
if reconcileErr != nil {
|
||||
if preview.Ready == 0 {
|
||||
return result, reconcileErr
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
||||
target, policies, runtime, err := s.loadRuleSyncCandidates(ctx, request)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncPreview{}, err
|
||||
}
|
||||
targetInventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncPreview{}, err
|
||||
}
|
||||
return dockerSyncPreview(filter.Provider(target), policies, targetInventory), nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
|
||||
target, policies, targetRuntime, err := s.loadRuleSyncCandidates(ctx, request)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncResult{}, err
|
||||
}
|
||||
runtimePolicies := make([]dockerfirewall.Policy, 0, len(policies))
|
||||
for _, policy := range policies {
|
||||
sources := []string{}
|
||||
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
||||
runtimePolicies = append(runtimePolicies, dockerfirewall.Policy{
|
||||
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
||||
Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources,
|
||||
})
|
||||
}
|
||||
targetInventory, err := targetRuntime.ListPolicies()
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncResult{}, err
|
||||
}
|
||||
preview := dockerSyncPreview(filter.Provider(target), policies, targetInventory)
|
||||
for _, item := range preview.Items {
|
||||
if item.Status == firewallsync.StatusBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
||||
return firewallSyncResult(preview, nil, false), nil
|
||||
}
|
||||
}
|
||||
if preview.Ready == 0 && preview.Removed == 0 {
|
||||
return firewallSyncResult(preview, nil, false), nil
|
||||
}
|
||||
reconcileErr := func() error {
|
||||
if err := reconcileDockerFirewall(target, runtimePolicies, targetRuntime, targetInventory); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verifyDockerFirewall(targetRuntime, runtimePolicies, targetInventory.ReadOnly); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(policies) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, target); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
}()
|
||||
result := firewallSyncResult(preview, reconcileErr, true)
|
||||
if reconcileErr != nil {
|
||||
return result, reconcileErr
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *FirewallService) syncSystemRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
||||
subsystem := strings.TrimSpace(request.Subsystem)
|
||||
if subsystem == "" {
|
||||
subsystem = "system"
|
||||
}
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return dto.FirewallRuleSyncResult{}, err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
firewallRuleSyncTaskMu.Lock()
|
||||
defer firewallRuleSyncTaskMu.Unlock()
|
||||
|
||||
running, err := currentFirewallRuleSyncTaskLocked()
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncResult{}, err
|
||||
}
|
||||
if running.Executing {
|
||||
return dto.FirewallRuleSyncResult{
|
||||
Subsystem: subsystem,
|
||||
TargetProvider: request.TargetProvider,
|
||||
TaskID: running.TaskID,
|
||||
Queued: true,
|
||||
}, nil
|
||||
}
|
||||
if subsystem != "system" {
|
||||
return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule)
|
||||
}
|
||||
taskItem, err := task.NewTask(firewallTaskName(task.TaskSync, firewallTaskHost, string(request.TargetProvider)), task.TaskSync, task.TaskScopeFirewall, "", 0)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall sync task: %w", err)
|
||||
}
|
||||
taskItem.AddSubTaskWithOps(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error {
|
||||
result, err := s.syncRules(t.TaskCtx, clientIP, request, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.Failed > 0 {
|
||||
return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": result.Failed}))
|
||||
}
|
||||
return nil
|
||||
}, nil, 0, 0)
|
||||
|
||||
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
||||
taskItem.LogFailedWithErr(taskItem.Name, err)
|
||||
closeUnstartedFirewallTask(taskItem)
|
||||
return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall sync task: %w", err)
|
||||
}
|
||||
firewallRuleSyncTaskID = taskItem.TaskID
|
||||
go func() {
|
||||
defer func() {
|
||||
firewallRuleSyncTaskMu.Lock()
|
||||
if firewallRuleSyncTaskID == taskItem.TaskID {
|
||||
firewallRuleSyncTaskID = ""
|
||||
}
|
||||
firewallRuleSyncTaskMu.Unlock()
|
||||
}()
|
||||
if err := taskItem.Execute(); err != nil && global.LOG != nil {
|
||||
global.LOG.Errorf("firewall sync task %s failed: %v", taskItem.TaskID, err)
|
||||
}
|
||||
}()
|
||||
return dto.FirewallRuleSyncResult{
|
||||
Subsystem: "system",
|
||||
TargetProvider: request.TargetProvider,
|
||||
TaskID: taskItem.TaskID,
|
||||
Queued: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func verifyForwardingRuleSync(target forwarding.Adapter, desired []forwarding.Rule) error {
|
||||
actual, err := target.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
||||
}
|
||||
actual, err = normalizeForwardingRuntimeRules(actual)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
||||
}
|
||||
if !firewallRuleStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
|
||||
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reconcileDockerFirewall(backend string, policies []dockerfirewall.Policy, runtime dockerfirewall.Runtime, inventory dockerfirewall.PolicyInventory) error {
|
||||
families := make(map[string]struct{}, len(policies))
|
||||
needsInitialize, needsBind := false, false
|
||||
for _, policy := range policies {
|
||||
families[policy.Family] = struct{}{}
|
||||
}
|
||||
if len(families) == 0 {
|
||||
initialized := false
|
||||
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
||||
status := runtime.Status(family)
|
||||
if status.Reason == dockerfirewall.ReasonInspectFailed {
|
||||
return fmt.Errorf("inspect Docker firewall target %s for %s failed", backend, family)
|
||||
}
|
||||
initialized = initialized || status.Initialized
|
||||
}
|
||||
if initialized {
|
||||
return runtime.ReplacePolicies(nil, inventory)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for family := range families {
|
||||
status := runtime.Status(family)
|
||||
needsInitialize = needsInitialize || !status.Initialized
|
||||
needsBind = needsBind || !status.Bound || !status.Effective
|
||||
}
|
||||
var err error
|
||||
if needsInitialize {
|
||||
err = runtime.Initialize(policies, inventory)
|
||||
} else {
|
||||
if needsBind {
|
||||
err = runtime.Bind()
|
||||
}
|
||||
if err == nil {
|
||||
err = runtime.ReplacePolicies(policies, inventory)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for family := range families {
|
||||
if !runtime.Status(family).Effective {
|
||||
return fmt.Errorf("Docker firewall target %s is not effective for %s", backend, family)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReconcileDockerPortGuardBestEffort(ctx context.Context) {
|
||||
if err := ReconcileDockerPortGuard(ctx); err != nil {
|
||||
global.LOG.Warnf("reconcile Docker port guard failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
+1540
-2860
File diff suppressed because it is too large
Load Diff
+149
-425
@@ -5,71 +5,55 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
)
|
||||
|
||||
const (
|
||||
forwardingSyncConverged = "converged"
|
||||
forwardingSyncMissing = "missing"
|
||||
forwardingSyncRuntimeOnly = "runtime_only"
|
||||
)
|
||||
|
||||
type IForwardingService interface {
|
||||
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
||||
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
|
||||
LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error)
|
||||
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
|
||||
SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
|
||||
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
|
||||
Enable() error
|
||||
Enable(ctx context.Context) error
|
||||
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
|
||||
Restore(context.Context) error
|
||||
}
|
||||
|
||||
type ForwardingService struct {
|
||||
clientFactory func() (forwarding.Adapter, error)
|
||||
rules repo.IForwardingRuleRepo
|
||||
enabled func() (bool, error)
|
||||
persistBackend func(string) error
|
||||
markEnabled func() error
|
||||
clientFactory func(context.Context) (forwarding.Adapter, error)
|
||||
}
|
||||
|
||||
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
|
||||
|
||||
var forwardingMutationMu sync.Mutex
|
||||
|
||||
var (
|
||||
forwardingSyncStateMu sync.RWMutex
|
||||
forwardingLastSyncErr error
|
||||
)
|
||||
|
||||
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
|
||||
func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) {
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemStatus{}, err
|
||||
}
|
||||
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
selected = strings.TrimSpace(selected)
|
||||
if selected == "" {
|
||||
selected = constant.FirewallProviderIptables
|
||||
}
|
||||
baseInfo := dto.FirewallSubsystemStatus{
|
||||
Version: "-", Name: selected, Backend: selected, SyncError: lastForwardingSyncError(),
|
||||
Version: "-", Name: selected, Backend: selected, IPv6Enabled: len(families) > 1,
|
||||
}
|
||||
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||
baseInfo.Name += "-forward"
|
||||
}
|
||||
manager, err := s.clientFactory()
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, errForwardingBackendUnavailable) {
|
||||
baseInfo.Reason = constant.FirewallBackendNotInstalled
|
||||
@@ -82,7 +66,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
||||
return baseInfo, err
|
||||
}
|
||||
version, versionErr := client.Version()
|
||||
status, statusErr := loadForwardingFirewallOverview(manager)
|
||||
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||
if err := errors.Join(versionErr, statusErr); err != nil {
|
||||
return baseInfo, err
|
||||
}
|
||||
@@ -95,83 +79,51 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
||||
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
||||
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
|
||||
}
|
||||
|
||||
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
|
||||
if request.Strategy != "" {
|
||||
return 0, nil, nil
|
||||
func (s *ForwardingService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
|
||||
var manager forwarding.Adapter
|
||||
var err error
|
||||
if provider == "" {
|
||||
manager, err = s.clientFactory(ctx)
|
||||
} else {
|
||||
manager, err = newForwardingAdapterFor(ctx, string(provider))
|
||||
}
|
||||
stored, err := s.rules.List(context.Background())
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemBackup{}, err
|
||||
}
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemBackup{}, err
|
||||
}
|
||||
return forwardingBackup(manager, rules)
|
||||
}
|
||||
|
||||
func (s *ForwardingService) SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
manager, err := s.clientFactory()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
runtime, err := manager.List()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
inventory, err := mergeForwardingInventory(stored, runtime)
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
items := make([]dto.ForwardRule, 0, len(rules))
|
||||
keyword := strings.ToLower(strings.TrimSpace(request.Info))
|
||||
filtered := inventory[:0]
|
||||
for _, item := range inventory {
|
||||
if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) {
|
||||
filtered = append(filtered, item)
|
||||
for _, rule := range rules {
|
||||
if keyword != "" && !strings.Contains(strings.ToLower(strings.Join([]string{rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface}, " ")), keyword) {
|
||||
continue
|
||||
}
|
||||
items = append(items, dto.ForwardRule{Num: strconv.Itoa(len(items) + 1), Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
||||
}
|
||||
inventory = filtered
|
||||
total := len(inventory)
|
||||
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
|
||||
total := len(items)
|
||||
if request.All {
|
||||
start, end = 0, total
|
||||
return int64(total), items, nil
|
||||
}
|
||||
if start > total {
|
||||
return int64(total), make([]dto.ForwardRule, 0), nil
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
pageRules := inventory[start:end]
|
||||
var items []dto.ForwardRule
|
||||
if pageRules != nil {
|
||||
items = make([]dto.ForwardRule, 0, len(pageRules))
|
||||
}
|
||||
for index, item := range pageRules {
|
||||
items = append(items, dto.ForwardRule{
|
||||
ID: item.ID,
|
||||
Num: strconv.Itoa(start + index + 1),
|
||||
Family: item.Rule.Family,
|
||||
Protocol: item.Rule.Protocol,
|
||||
Port: item.Rule.Port,
|
||||
TargetIP: item.Rule.TargetIP,
|
||||
TargetPort: item.Rule.TargetPort,
|
||||
Interface: item.Rule.Interface,
|
||||
IsDesired: item.IsDesired,
|
||||
IsRuntime: item.IsRuntime,
|
||||
SyncStatus: item.SyncStatus(),
|
||||
})
|
||||
}
|
||||
return int64(total), items, nil
|
||||
start := min(max(request.Page-1, 0)*max(request.PageSize, 1), total)
|
||||
end := min(start+max(request.PageSize, 1), total)
|
||||
return int64(total), items[start:end], nil
|
||||
}
|
||||
|
||||
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
|
||||
@@ -198,7 +150,7 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
||||
return s.operateRules(t.TaskCtx, request, t)
|
||||
return s.operateRules(t.TaskCtx, request, t, false)
|
||||
}, nil, 0, 0)
|
||||
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
||||
taskItem.LogFailedWithErr(taskItem.Name, err)
|
||||
@@ -209,358 +161,130 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
|
||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||
}
|
||||
|
||||
func (s *ForwardingService) Enable() error {
|
||||
func (s *ForwardingService) Enable(ctx context.Context) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
manager, err := s.clientFactory()
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
if err := s.persistForwardingEnabled(); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
if err := s.initializeForwarding(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
rules, err := s.rules.List(context.Background())
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initializeForwarding(manager, families); err != nil {
|
||||
return err
|
||||
}
|
||||
return persistForwardingRules(manager, rules)
|
||||
}
|
||||
|
||||
func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
|
||||
}
|
||||
var manager forwarding.Adapter
|
||||
var backend string
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
var backup dto.FirewallSubsystemBackup
|
||||
if request.BackupFile != "" {
|
||||
var err error
|
||||
manager, err = s.clientFactory()
|
||||
backup, err = readFirewallSubsystemBackup(request.BackupFile, "forwarding")
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
backend = manager.Name()
|
||||
t.Logf("backend=%s", backend)
|
||||
if err := s.persistForwardingEnabled(); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
if err := s.initializeForwarding(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func(t *task.Task) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
rules, err := s.rules.List(t.TaskCtx)
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}, nil)
|
||||
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("save forwarding initialization task: %w", err)
|
||||
}
|
||||
go func() { _ = taskItem.Execute() }()
|
||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||
operations := dto.ForwardRuleOperate{Rules: make([]dto.ForwardRuleOperation, 0, len(backup.Forwarding))}
|
||||
for _, rule := range backup.Forwarding {
|
||||
operations.Rules = append(operations.Rules, dto.ForwardRuleOperation{Operation: "add", Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskForwarding, task.TaskExec, request.TaskID, nil, func(t *task.Task) error {
|
||||
return s.operateRules(t.TaskCtx, operations, t, true, backup.Families...)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ForwardingService) Restore(ctx context.Context) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
|
||||
if err != nil || status != constant.StatusEnable {
|
||||
return err
|
||||
}
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup, err := readFirewallSubsystemBackup("forwarding-"+manager.Name()+".rules", "forwarding")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
||||
before := len(backup.Forwarding)
|
||||
backup.Forwarding = slices.DeleteFunc(backup.Forwarding, func(rule forwarding.Rule) bool { return rule.Family == constant.FirewallFamilyIPv6 })
|
||||
logFirewallIPv6Skipped(nil, "forwarding startup", before-len(backup.Forwarding))
|
||||
}
|
||||
missing := make(map[string]bool)
|
||||
needsBind := false
|
||||
for _, family := range families {
|
||||
initialized, bound, err := manager.FamilyStatus(family)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
missing[family] = !initialized
|
||||
needsBind = needsBind || (initialized && !bound)
|
||||
}
|
||||
restoreIPv6 := slices.Contains(backup.Families, forwarding.FamilyIPv6)
|
||||
for _, rule := range backup.Forwarding {
|
||||
restoreIPv6 = restoreIPv6 || rule.Family == forwarding.FamilyIPv6
|
||||
}
|
||||
if !missing[forwarding.FamilyIPv4] && (!missing[forwarding.FamilyIPv6] || !restoreIPv6) && !needsBind {
|
||||
return nil
|
||||
}
|
||||
current, err := manager.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seen := make(map[string]int, len(current))
|
||||
for _, rule := range current {
|
||||
seen[rule.Identity()]++
|
||||
}
|
||||
for _, family := range families {
|
||||
if family == forwarding.FamilyIPv6 && missing[family] && !restoreIPv6 {
|
||||
continue
|
||||
}
|
||||
if err := manager.OperateFamily(family, missing[family]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, family := range families {
|
||||
if !missing[family] {
|
||||
continue
|
||||
}
|
||||
rules := make([]forwarding.Rule, 0)
|
||||
for _, rule := range backup.Forwarding {
|
||||
if rule.Family != family {
|
||||
continue
|
||||
}
|
||||
if seen[rule.Identity()] > 0 {
|
||||
seen[rule.Identity()]--
|
||||
continue
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
if err := manager.CreateRules(ctx, rules); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewIForwardingService() IForwardingService {
|
||||
return newForwardingService()
|
||||
}
|
||||
|
||||
func loadForwardPolicy(command string) (string, error) {
|
||||
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...)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -44,6 +45,7 @@ type ISettingService interface {
|
||||
GetLocalConnForSSH() (dto.SSHConnData, error)
|
||||
|
||||
SaveDescription(req dto.CommonDescription) error
|
||||
CleanupDescriptions(context.Context) (int64, error)
|
||||
}
|
||||
|
||||
func NewISettingService() ISettingService {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
func (u *SettingService) CleanupDescriptions(ctx context.Context) (int64, error) {
|
||||
var deleted int64
|
||||
var failures []error
|
||||
for _, kind := range []string{"container", "firewall", "firewall-docker"} {
|
||||
count, err := func() (int64, error) {
|
||||
switch kind {
|
||||
case "container":
|
||||
return cleanupUnusedDescriptions(ctx, kind, loadContainerDescriptionIDs)
|
||||
case "firewall":
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return cleanupUnusedDescriptions(ctx, kind, loadHostFirewallDescriptionIDs)
|
||||
default:
|
||||
return cleanupUnusedDescriptions(ctx, kind, nil)
|
||||
}
|
||||
}()
|
||||
deleted += count
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Errorf("%s: %w", kind, err))
|
||||
}
|
||||
}
|
||||
return deleted, errors.Join(failures...)
|
||||
}
|
||||
|
||||
func cleanupUnusedDescriptions(ctx context.Context, kind string, loadIDs func(context.Context) (map[string]bool, error)) (int64, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
descriptions, err := settingRepo.GetDescriptionList(repo.WithByType(kind))
|
||||
if err != nil || len(descriptions) == 0 {
|
||||
return 0, err
|
||||
}
|
||||
var active map[string]bool
|
||||
if loadIDs != nil {
|
||||
active, err = loadIDs(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
ids, empty := make([]string, 0), make([]string, 0)
|
||||
for _, description := range descriptions {
|
||||
if !active[description.ID] {
|
||||
ids = append(ids, description.ID)
|
||||
} else if description.Description == "" && !description.IsPinned {
|
||||
empty = append(empty, description.ID)
|
||||
}
|
||||
}
|
||||
deleted, err := settingRepo.DeleteDescriptions(ctx, kind, ids, false)
|
||||
if err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
count, err := settingRepo.DeleteDescriptions(ctx, kind, empty, true)
|
||||
return deleted + count, err
|
||||
}
|
||||
|
||||
func loadContainerDescriptionIDs(ctx context.Context) (map[string]bool, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make(map[string]bool, len(containers))
|
||||
for _, item := range containers {
|
||||
ids[item.ID] = true
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func loadHostFirewallDescriptionIDs(ctx context.Context) (map[string]bool, error) {
|
||||
providers := lifecycle.InstalledProviders()
|
||||
if len(providers) == 0 {
|
||||
return nil, filter.ErrProviderUnavailable
|
||||
}
|
||||
service := newFirewallService()
|
||||
ids := make(map[string]bool)
|
||||
for _, name := range providers {
|
||||
provider := filter.Provider(name)
|
||||
inventory, err := service.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, notice := range inventory.Notices {
|
||||
if notice.Code == filter.ScopeNoticeFamilyUnavailable || notice.Code == filter.ScopeNoticeManagedScopeInactive {
|
||||
return nil, fmt.Errorf("%w: %s %s", filter.ErrInventoryUnavailable, name, notice.Code)
|
||||
}
|
||||
}
|
||||
for _, item := range inventory.Items {
|
||||
id, err := filter.DescriptionID(*item.Observed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids[id] = true
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
@@ -87,6 +87,7 @@ const (
|
||||
TaskScopeCronjob = "Cronjob"
|
||||
TaskScopeClam = "Clam"
|
||||
TaskScopeSystem = "System"
|
||||
TaskScopeVm = "VirtualMachine"
|
||||
TaskScopeFirewall = "Firewall"
|
||||
TaskScopeAppStore = "AppStore"
|
||||
TaskScopeSnapshot = "Snapshot"
|
||||
@@ -99,6 +100,7 @@ const (
|
||||
TaskScopeTamper = "Tamper"
|
||||
TaskScopeFileConvert = "Convert"
|
||||
TaskScopeTask = "Task"
|
||||
TaskScopeVm = "VirtualMachine"
|
||||
)
|
||||
|
||||
func GetTaskName(resourceName, operate, scope string) string {
|
||||
@@ -257,7 +259,15 @@ func (s *SubTask) Execute() error {
|
||||
case <-ctx.Done():
|
||||
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
|
||||
err = errors.New("timeout!")
|
||||
if s.RootTask.Task.Type == TaskScopeVm {
|
||||
<-done
|
||||
}
|
||||
case err = <-done:
|
||||
if s.RootTask.Task.Type == TaskScopeVm && ctx.Err() != nil {
|
||||
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
|
||||
err = errors.New("timeout!")
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
|
||||
if err.Error() == i18n.GetMsgByKey("ErrShutDown") {
|
||||
|
||||
@@ -265,8 +265,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
|
||||
"formatEN": "Application port update [key]-[name] =\u003e [port]"
|
||||
"formatZH": "应用端口修改 [key]-[name] => [port]",
|
||||
"formatEN": "Application port update [key]-[name] => [port]"
|
||||
},
|
||||
"/apps/installed/sort/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -644,13 +644,6 @@
|
||||
"formatZH": "创建容器网络 name",
|
||||
"formatEN": "create container network [name]"
|
||||
},
|
||||
"/containers/network/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理未使用的容器网络",
|
||||
"formatEN": "Clean unused container networks"
|
||||
},
|
||||
"/containers/network/del": {
|
||||
"bodyKeys": [
|
||||
"names"
|
||||
@@ -686,8 +679,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "容器重命名 [name] =\u003e [newName]",
|
||||
"formatEN": "rename container [name] =\u003e [newName]"
|
||||
"formatZH": "容器重命名 [name] => [newName]",
|
||||
"formatEN": "rename container [name] => [newName]"
|
||||
},
|
||||
"/containers/repo": {
|
||||
"bodyKeys": [
|
||||
@@ -838,8 +831,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
},
|
||||
"/core/auth/expired/reset": {
|
||||
"bodyKeys": [],
|
||||
@@ -1723,8 +1716,8 @@
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
},
|
||||
"/core/enterprise/users/del": {
|
||||
"bodyKeys": [
|
||||
@@ -1925,8 +1918,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||
},
|
||||
"/core/settings/memo": {
|
||||
"bodyKeys": [],
|
||||
@@ -1955,8 +1948,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统端口 =\u003e [serverPort]",
|
||||
"formatEN": "update system port =\u003e [serverPort]"
|
||||
"formatZH": "修改系统端口 => [serverPort]",
|
||||
"formatEN": "update system port => [serverPort]"
|
||||
},
|
||||
"/core/settings/proxy/update": {
|
||||
"bodyKeys": [
|
||||
@@ -1981,8 +1974,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统 ssl =\u003e [ssl]",
|
||||
"formatEN": "update system ssl =\u003e [ssl]"
|
||||
"formatZH": "修改系统 ssl => [ssl]",
|
||||
"formatEN": "update system ssl => [ssl]"
|
||||
},
|
||||
"/core/settings/terminal/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -1998,8 +1991,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||
"formatEN": "update system setting [key] =\u003e [value]"
|
||||
"formatZH": "修改系统配置 [key] => [value]",
|
||||
"formatEN": "update system setting [key] => [value]"
|
||||
},
|
||||
"/core/settings/upgrade": {
|
||||
"bodyKeys": [
|
||||
@@ -2007,8 +2000,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新系统 =\u003e [version]",
|
||||
"formatEN": "upgrade system =\u003e [version]"
|
||||
"formatZH": "更新系统 => [version]",
|
||||
"formatEN": "upgrade system => [version]"
|
||||
},
|
||||
"/core/xapp/appQRCode": {
|
||||
"bodyKeys": [
|
||||
@@ -2466,7 +2459,7 @@
|
||||
"formatZH": "同步 SSL 证书 [primaryDomain]",
|
||||
"formatEN": "sync SSL certificate [primaryDomain]"
|
||||
},
|
||||
"/core/xpack/vms": {
|
||||
"/xpack/vms": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
@@ -2475,7 +2468,7 @@
|
||||
"formatZH": "创建虚拟机 [name]",
|
||||
"formatEN": "create VM [name]"
|
||||
},
|
||||
"/core/xpack/vms/del": {
|
||||
"/xpack/vms/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2493,7 +2486,7 @@
|
||||
"formatZH": "删除虚拟机 [name]",
|
||||
"formatEN": "delete VM [name]"
|
||||
},
|
||||
"/core/xpack/vms/disks/resize": {
|
||||
"/xpack/vms/disks/resize": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"target",
|
||||
@@ -2504,14 +2497,14 @@
|
||||
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
|
||||
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
|
||||
},
|
||||
"/core/xpack/vms/environment/enable": {
|
||||
"/xpack/vms/environment/enable": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "启用虚拟机运行环境",
|
||||
"formatEN": "enable VM runtime environment"
|
||||
},
|
||||
"/core/xpack/vms/iso": {
|
||||
"/xpack/vms/iso": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2521,7 +2514,7 @@
|
||||
"formatZH": "创建虚拟机镜像 [name] [type]",
|
||||
"formatEN": "create VM ISO [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/iso/del": {
|
||||
"/xpack/vms/iso/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2539,7 +2532,7 @@
|
||||
"formatZH": "删除虚拟机镜像 [name]",
|
||||
"formatEN": "delete VM ISO [name]"
|
||||
},
|
||||
"/core/xpack/vms/iso/update": {
|
||||
"/xpack/vms/iso/update": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2549,7 +2542,7 @@
|
||||
"formatZH": "更新虚拟机镜像 [name] [type]",
|
||||
"formatEN": "update VM ISO [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/networks": {
|
||||
"/xpack/vms/networks": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2559,7 +2552,7 @@
|
||||
"formatZH": "创建虚拟机网络 [name] [type]",
|
||||
"formatEN": "create VM network [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/networks/del": {
|
||||
"/xpack/vms/networks/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2577,7 +2570,7 @@
|
||||
"formatZH": "删除虚拟机网络 [name]",
|
||||
"formatEN": "delete VM network [name]"
|
||||
},
|
||||
"/core/xpack/vms/networks/update": {
|
||||
"/xpack/vms/networks/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2595,7 +2588,7 @@
|
||||
"formatZH": "更新虚拟机网络 [name]",
|
||||
"formatEN": "update VM network [name]"
|
||||
},
|
||||
"/core/xpack/vms/operate": {
|
||||
"/xpack/vms/operate": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"operate"
|
||||
@@ -2605,14 +2598,14 @@
|
||||
"formatZH": "操作虚拟机 [name] [operate]",
|
||||
"formatEN": "operate VM [name] [operate]"
|
||||
},
|
||||
"/core/xpack/vms/orphans/clean": {
|
||||
"/xpack/vms/orphans/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理虚拟机孤立数据",
|
||||
"formatEN": "clean VM orphaned data"
|
||||
},
|
||||
"/core/xpack/vms/rename": {
|
||||
"/xpack/vms/rename": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"newName"
|
||||
@@ -2622,7 +2615,7 @@
|
||||
"formatZH": "重命名虚拟机 [name] 为 [newName]",
|
||||
"formatEN": "rename VM [name] to [newName]"
|
||||
},
|
||||
"/core/xpack/vms/snapshots": {
|
||||
"/xpack/vms/snapshots": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -2632,7 +2625,7 @@
|
||||
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "create VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/xpack/vms/snapshots/del": {
|
||||
"/xpack/vms/snapshots/del": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -2642,7 +2635,7 @@
|
||||
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "delete VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/xpack/vms/snapshots/recover": {
|
||||
"/xpack/vms/snapshots/recover": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -2652,7 +2645,7 @@
|
||||
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "recover VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/xpack/vms/storages": {
|
||||
"/xpack/vms/storages": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2662,7 +2655,7 @@
|
||||
"formatZH": "创建虚拟机存储 [name] [type]",
|
||||
"formatEN": "create VM storage [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/storages/del": {
|
||||
"/xpack/vms/storages/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2680,7 +2673,7 @@
|
||||
"formatZH": "删除虚拟机存储 [name]",
|
||||
"formatEN": "delete VM storage [name]"
|
||||
},
|
||||
"/core/xpack/vms/storages/update": {
|
||||
"/xpack/vms/storages/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2698,14 +2691,14 @@
|
||||
"formatZH": "更新虚拟机存储 [name]",
|
||||
"formatEN": "update VM storage [name]"
|
||||
},
|
||||
"/core/xpack/vms/sync": {
|
||||
"/xpack/vms/sync": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步虚拟机",
|
||||
"formatEN": "sync virtual machines"
|
||||
},
|
||||
"/core/xpack/vms/templates": {
|
||||
"/xpack/vms/templates": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"templateName"
|
||||
@@ -2715,7 +2708,7 @@
|
||||
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
|
||||
"formatEN": "create VM [name] template [templateName]"
|
||||
},
|
||||
"/core/xpack/vms/templates/del": {
|
||||
"/xpack/vms/templates/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2733,7 +2726,7 @@
|
||||
"formatZH": "删除虚拟机模板 [name]",
|
||||
"formatEN": "delete VM template [name]"
|
||||
},
|
||||
"/core/xpack/vms/update": {
|
||||
"/xpack/vms/update": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
@@ -2895,8 +2888,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "首页应用 [key] =\u003e 显示:[value]",
|
||||
"formatEN": "app launcher [key] =\u003e show: [value]"
|
||||
"formatZH": "首页应用 [key] => 显示:[value]",
|
||||
"formatEN": "app launcher [key] => show: [value]"
|
||||
},
|
||||
"/dashboard/quick/change": {
|
||||
"bodyKeys": [],
|
||||
@@ -3024,7 +3017,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the mysql database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/grants": {
|
||||
"bodyKeys": [
|
||||
@@ -3105,7 +3098,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the mongodb database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/mongodb/password": {
|
||||
"bodyKeys": [
|
||||
@@ -3190,7 +3183,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the postgresql database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/pg/password": {
|
||||
"bodyKeys": [
|
||||
@@ -3295,8 +3288,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
|
||||
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
|
||||
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
|
||||
},
|
||||
"/databases/variables/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -3332,8 +3325,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
|
||||
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
|
||||
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
|
||||
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
|
||||
},
|
||||
"/files/chunkdownload": {
|
||||
"bodyKeys": [
|
||||
@@ -3423,8 +3416,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改权限 [path] =\u003e [mode]",
|
||||
"formatEN": "Change mode [path] =\u003e [mode]"
|
||||
"formatZH": "修改权限 [path] => [mode]",
|
||||
"formatEN": "Change mode [path] => [mode]"
|
||||
},
|
||||
"/files/move": {
|
||||
"bodyKeys": [
|
||||
@@ -3433,8 +3426,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
|
||||
"formatEN": "Move [oldPaths] =\u003e [newPath]"
|
||||
"formatZH": "移动文件 [oldPaths] => [newPath]",
|
||||
"formatEN": "Move [oldPaths] => [newPath]"
|
||||
},
|
||||
"/files/owner": {
|
||||
"bodyKeys": [
|
||||
@@ -3444,8 +3437,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
|
||||
"formatEN": "Change owner [path] =\u003e [user]/[group]"
|
||||
"formatZH": "修改用户/组 [path] => [user]/[group]",
|
||||
"formatEN": "Change owner [path] => [user]/[group]"
|
||||
},
|
||||
"/files/preview": {
|
||||
"bodyKeys": [
|
||||
@@ -3479,8 +3472,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "重命名 [oldName] =\u003e [newName]",
|
||||
"formatEN": "Rename [oldName] =\u003e [newName]"
|
||||
"formatZH": "重命名 [oldName] => [newName]",
|
||||
"formatEN": "Rename [oldName] => [newName]"
|
||||
},
|
||||
"/files/save": {
|
||||
"bodyKeys": [
|
||||
@@ -3537,8 +3530,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载 url =\u003e [path]/[name]",
|
||||
"formatEN": "Download url =\u003e [path]/[name]"
|
||||
"formatZH": "下载 url => [path]/[name]",
|
||||
"formatEN": "Download url => [path]/[name]"
|
||||
},
|
||||
"/files/wget/process/remove": {
|
||||
"bodyKeys": [
|
||||
@@ -3688,12 +3681,16 @@
|
||||
"formatZH": "删除 Docker 端口防护策略 [uuids]",
|
||||
"formatEN": "delete Docker port guard policies [uuids]"
|
||||
},
|
||||
"/hosts/firewall/docker/sync": {
|
||||
"bodyKeys": [],
|
||||
"/hosts/firewall/family/operate": {
|
||||
"bodyKeys": [
|
||||
"subsystem",
|
||||
"family",
|
||||
"operation"
|
||||
],
|
||||
"formatEN": "[operation] [subsystem] [family] firewall chains",
|
||||
"formatZH": "[operation] [subsystem] [family] 防火墙链",
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步 Docker 端口防护规则",
|
||||
"formatEN": "sync Docker port guard rules"
|
||||
"beforeFunctions": []
|
||||
},
|
||||
"/hosts/firewall/filter/operate": {
|
||||
"bodyKeys": [
|
||||
@@ -3734,13 +3731,6 @@
|
||||
"formatZH": "添加防火墙规则",
|
||||
"formatEN": "create firewall rules"
|
||||
},
|
||||
"/hosts/firewall/rules/adopt": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "纳管防火墙规则",
|
||||
"formatEN": "adopt firewall rule"
|
||||
},
|
||||
"/hosts/firewall/rules/delete": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
@@ -3750,12 +3740,12 @@
|
||||
},
|
||||
"/hosts/firewall/rules/reorder": {
|
||||
"bodyKeys": [
|
||||
"uuid"
|
||||
"instanceKey"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "调整防火墙规则顺序 [uuid]",
|
||||
"formatEN": "reorder firewall rule [uuid]"
|
||||
"formatZH": "调整防火墙规则顺序 [instanceKey]",
|
||||
"formatEN": "reorder firewall rule [instanceKey]"
|
||||
},
|
||||
"/hosts/firewall/rules/reset": {
|
||||
"bodyKeys": [],
|
||||
@@ -3764,25 +3754,23 @@
|
||||
"formatZH": "重置防火墙规则",
|
||||
"formatEN": "reset firewall rules"
|
||||
},
|
||||
"/hosts/firewall/rules/sync": {
|
||||
"bodyKeys": [
|
||||
"subsystem",
|
||||
"sourceProvider",
|
||||
"targetProvider"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步 [subsystem] 防火墙规则到 [targetProvider]",
|
||||
"formatEN": "sync [subsystem] firewall rules to [targetProvider]"
|
||||
},
|
||||
"/hosts/firewall/rules/update": {
|
||||
"bodyKeys": [
|
||||
"uuid"
|
||||
"instanceKey"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新防火墙规则 [uuid]",
|
||||
"formatEN": "update firewall rule [uuid]"
|
||||
"formatZH": "更新防火墙规则 [instanceKey]",
|
||||
"formatEN": "update firewall rule [instanceKey]"
|
||||
},
|
||||
"/hosts/firewall/settings/ipv6": {
|
||||
"bodyKeys": [
|
||||
"status"
|
||||
],
|
||||
"formatEN": "Set firewall IPv6 support to [status]",
|
||||
"formatZH": "设置防火墙 IPv6 支持为 [status]",
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": []
|
||||
},
|
||||
"/hosts/firewall/settings/operate": {
|
||||
"bodyKeys": [
|
||||
@@ -3900,8 +3888,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
|
||||
"formatEN": "update SSH setting [key] =\u003e [newValue]"
|
||||
"formatZH": "修改 SSH 配置 [key] => [newValue]",
|
||||
"formatEN": "update SSH setting [key] => [newValue]"
|
||||
},
|
||||
"/hosts/tool/config/set": {
|
||||
"bodyKeys": [
|
||||
@@ -4162,7 +4150,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "快照 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the snapshot [name] is modified => [description]"
|
||||
},
|
||||
"/settings/snapshot/import": {
|
||||
"bodyKeys": [
|
||||
@@ -4254,8 +4242,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||
"formatEN": "update system setting [key] =\u003e [value]"
|
||||
"formatZH": "修改系统配置 [key] => [value]",
|
||||
"formatEN": "update system setting [key] => [value]"
|
||||
},
|
||||
"/toolbox/clam": {
|
||||
"bodyKeys": [
|
||||
@@ -4373,8 +4361,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改主机参数 [key] =\u003e [value]",
|
||||
"formatEN": "update device conf [key] =\u003e [value]"
|
||||
"formatZH": "修改主机参数 [key] => [value]",
|
||||
"formatEN": "update device conf [key] => [value]"
|
||||
},
|
||||
"/toolbox/device/update/host": {
|
||||
"bodyKeys": [
|
||||
@@ -4383,8 +4371,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改主机 Host [key] =\u003e [value]",
|
||||
"formatEN": "update device host [key] =\u003e [value]"
|
||||
"formatZH": "修改主机 Host [key] => [value]",
|
||||
"formatEN": "update device host [key] => [value]"
|
||||
},
|
||||
"/toolbox/device/update/swap": {
|
||||
"bodyKeys": [
|
||||
@@ -4412,8 +4400,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
|
||||
"formatEN": "update fail2ban conf [key] =\u003e [value]"
|
||||
"formatZH": "修改 Fail2ban 配置 [key] => [value]",
|
||||
"formatEN": "update fail2ban conf [key] => [value]"
|
||||
},
|
||||
"/toolbox/ftp": {
|
||||
"bodyKeys": [
|
||||
@@ -4672,8 +4660,8 @@
|
||||
"output_value": "domain"
|
||||
}
|
||||
],
|
||||
"formatZH": "修改默认 server =\u003e [domain]",
|
||||
"formatEN": "Change default server =\u003e [domain]"
|
||||
"formatZH": "修改默认 server => [domain]",
|
||||
"formatEN": "Change default server => [domain]"
|
||||
},
|
||||
"/websites/del": {
|
||||
"bodyKeys": [
|
||||
@@ -5436,8 +5424,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
|
||||
"formatEN": "update tamper info [website][path] =\u003e [status]"
|
||||
"formatZH": "更新防篡改信息 [website][path] => [status]",
|
||||
"formatEN": "update tamper info [website][path] => [status]"
|
||||
},
|
||||
"/xpack/vllm/command-template/create": {
|
||||
"bodyKeys": [
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
FirewallIPv6SupportKey = "FirewallIPv6Support"
|
||||
FirewallSystemBackendKey = "FirewallProvider"
|
||||
FirewallForwardingBackendKey = "ForwardingBackend"
|
||||
FirewallDockerBackendKey = "DockerFirewallBackend"
|
||||
@@ -33,12 +34,7 @@ const (
|
||||
const (
|
||||
FirewallSystemAcceptedPortSourcePrefix = "accepted-port:"
|
||||
|
||||
FirewallRuleOriginCreated = "created"
|
||||
FirewallRuleOriginAdopted = "adopted"
|
||||
|
||||
FirewallRuleSourceUser = "user"
|
||||
FirewallRuleSourceImported = "imported"
|
||||
FirewallRuleSourcePanel = "panel"
|
||||
FirewallRuleSourceSecurity = "security"
|
||||
FirewallRuleSourceApp = "application"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
StatusInactive = "Inactive"
|
||||
StatusUnknown = "Unknown"
|
||||
StatusDegraded = "Degraded"
|
||||
StatusInaccessible = "Inaccessible"
|
||||
StatusRunning = "Running"
|
||||
StatusCanceled = "Canceled"
|
||||
StatusDone = "Done"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
@@ -82,6 +83,17 @@ func Run() {
|
||||
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
|
||||
global.LOG.Errorf("can not add backup token refresh corn job: %s", err.Error())
|
||||
}
|
||||
if _, err := global.Cron.AddFunc("@every 240h", func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
deleted, err := service.NewISettingService().CleanupDescriptions(ctx)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("clean unused common descriptions: %v", err)
|
||||
}
|
||||
global.LOG.Infof("common description cleanup completed, deleted %d records", deleted)
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("add common description cleanup job: %v", err)
|
||||
}
|
||||
|
||||
var cronJobs []model.Cronjob
|
||||
if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil {
|
||||
|
||||
+50
-26
@@ -300,6 +300,9 @@ BuildCache: 'Build Cache'
|
||||
Volume: 'Volume'
|
||||
Network: "Network"
|
||||
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'
|
||||
ContainerBackupStop: 'Stop container before backup'
|
||||
ContainerBackupInspect: 'Backup container inspect and network'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again'
|
||||
ErrDockerNftablesChainUnavailable: 'The Docker nftables IPv4 firewall chain was not found. Verify that the current Docker version supports the nftables firewall backend, restart Docker, and try again'
|
||||
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD defaults to DROP. Adjust the policy and retry'
|
||||
ErrUFWRuleAdopt: 'Failed to adopt the UFW rule: {{ .detail }}. If the installed UFW version is earlier than 0.35, upgrade UFW and try again'
|
||||
ErrDockerForwardPolicyDrop: 'This server restricts network forwarding. Container port protection cannot be enabled at this time.'
|
||||
Firewall: 'Firewall'
|
||||
FirewallTaskHost: 'Host firewall'
|
||||
FirewallTaskForwarding: 'Port forwarding'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Create firewall rules'
|
||||
FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed'
|
||||
FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed'
|
||||
FirewallCreateRuleSkipped: 'Not executed'
|
||||
FirewallCreateBatchStep: 'Submit {{ .count }} rules as a batch to {{ .backend }}'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; converted to {{ .count }} rules'
|
||||
FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back'
|
||||
FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved'
|
||||
FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved'
|
||||
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed, {{ .unchanged }} already matching (no changes needed)'
|
||||
FirewallSyncRuleUnchanged: 'Already matching; no changes needed'
|
||||
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
|
||||
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
|
||||
FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted'
|
||||
FirewallCreateRuleExecutionFailed: 'Rule creation failed; executed commands were not rolled back'
|
||||
FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains'
|
||||
FirewallRestoreRulesStep: 'Restore database rules to {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist'
|
||||
FirewallEnableForwardingStep: 'Enable and bind port forwarding chains'
|
||||
FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules'
|
||||
FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
|
||||
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
|
||||
FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
|
||||
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
|
||||
FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list"
|
||||
FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules"
|
||||
FileTaskCopy: 'Copy files to {{ .dst }}'
|
||||
FileTaskMove: 'Move files to {{ .dst }}'
|
||||
FileTaskCompress: 'Compress files to {{ .dst }}'
|
||||
@@ -727,21 +716,56 @@ FileTaskDecompress: 'Extract files to {{ .dst }}'
|
||||
FileTaskSource: 'Source path: {{ .path }}'
|
||||
FileTaskFormat: 'Archive format: {{ .format }}'
|
||||
FileTaskRename: 'Target filename: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend."
|
||||
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "A UFW rule with the same match conditions and a different action already exists. Edit the existing rule instead."
|
||||
ErrForwardInterfaceNotFound: "Network interface {{ .name }} specified by the forwarding rule does not exist. Check the interface name and retry."
|
||||
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"
|
||||
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
|
||||
ErrSSHKeyMismatch: "The SSH public and private keys do not match"
|
||||
|
||||
NetworkCleanupDeleted: "Deleted network [{{ .name }}] ({{ .id }})"
|
||||
NetworkCleanupProtected: "Skipped network [{{ .name }}] ({{ .id }}): reserved network"
|
||||
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
|
||||
NetworkCleanupUnsupported: "Skipped network [{{ .name }}] ({{ .id }}): special network"
|
||||
NetworkCleanupGone: "Skipped network [{{ .name }}] ({{ .id }}): already removed"
|
||||
NetworkCleanupInspectFailed: "Failed to inspect network [{{ .name }}] ({{ .id }}); not deleted"
|
||||
NetworkCleanupRemoveFailed: "Failed to remove network [{{ .name }}] ({{ .id }})"
|
||||
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
|
||||
# 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 }}]"
|
||||
|
||||
+51
-17
@@ -300,6 +300,9 @@ BuildCache: 'Caché de compilación'
|
||||
Volume: 'Volumen de almacenamiento'
|
||||
Network: 'Red'
|
||||
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'
|
||||
ContainerBackupStop: 'Detener contenedor antes del respaldo'
|
||||
ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo'
|
||||
ErrDockerNftablesChainUnavailable: 'No se encontró la cadena de firewall IPv4 de nftables de Docker. Compruebe que la versión actual de Docker admita el backend de firewall nftables, reinicie Docker y vuelva a intentarlo'
|
||||
ErrDockerForwardPolicyDrop: 'La política predeterminada de FORWARD para {{ .family }} es DROP. Ajústela y vuelva a intentarlo'
|
||||
ErrUFWRuleAdopt: 'No se pudo administrar la regla UFW: {{ .detail }}. Si la versión actual de UFW es anterior a 0.35, actualice UFW y vuelva a intentarlo'
|
||||
ErrDockerForwardPolicyDrop: 'Este servidor restringe el reenvío de red. Por ahora no se puede habilitar la protección de puertos de contenedores.'
|
||||
Firewall: 'Firewall'
|
||||
FirewallTaskHost: 'Cortafuegos del host'
|
||||
FirewallTaskForwarding: 'Reenvío de puertos'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Crear reglas de cortafuegos'
|
||||
FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
|
||||
FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas'
|
||||
FirewallCreateRuleSkipped: 'Sin ejecutar'
|
||||
FirewallCreateBatchStep: 'Enviar {{ .count }} reglas en un lote a {{ .backend }}'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido en {{ .count }} reglas'
|
||||
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados'
|
||||
FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión'
|
||||
FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión'
|
||||
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar, {{ .unchanged }} ya coinciden (sin cambios necesarios)'
|
||||
FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios'
|
||||
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
|
||||
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
|
||||
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
|
||||
FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos'
|
||||
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. Los comandos ejecutados no se revirtieron'
|
||||
FirewallInitializeChainsStep: 'Inicializar y vincular las cadenas base de {{ .name }}'
|
||||
FirewallRestoreRulesStep: 'Restaurar las reglas de la base de datos en {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'Sincronizar la lista de puertos permitidos del firewall'
|
||||
FirewallEnableForwardingStep: 'Habilitar y vincular las cadenas de reenvío de puertos'
|
||||
FirewallRestoreForwardingRulesStep: 'Restaurar las reglas de reenvío de puertos de la base de datos'
|
||||
FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker y las políticas'
|
||||
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
|
||||
ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva."
|
||||
FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista"
|
||||
FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema"
|
||||
FileTaskCopy: 'Copiar archivos a {{ .dst }}'
|
||||
FileTaskMove: 'Mover archivos a {{ .dst }}'
|
||||
FileTaskCompress: 'Comprimir archivos en {{ .dst }}'
|
||||
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Extraer archivos a {{ .dst }}'
|
||||
FileTaskSource: 'Ruta de origen: {{ .path }}'
|
||||
FileTaskFormat: 'Formato del archivo: {{ .format }}'
|
||||
FileTaskRename: 'Nombre del archivo de destino: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "Ya existe una regla UFW con las mismas condiciones y una acción diferente. Edite la regla existente."
|
||||
ErrForwardInterfaceNotFound: "La interfaz de red {{ .name }} indicada en la regla de reenvío no existe. Compruebe el nombre e inténtelo de nuevo."
|
||||
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"
|
||||
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"
|
||||
|
||||
# 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
@@ -300,6 +300,9 @@ BuildCache: 'کش ساخت'
|
||||
Volume: 'حجم'
|
||||
Network: "شبکه"
|
||||
PruneStart: 'پاکسازی در حال انجام است'
|
||||
NetworkCleanupConnected: "از شبکه [{{ .name }}] ({{ .id }}) صرفنظر شد: کانتینر متصل دارد یا در حال استفاده است؛ حذف نشد"
|
||||
NetworkCleanupSummary: "پاکسازی شبکهها پایان یافت: حذفشده {{ .deleted }}، ردشده {{ .skipped }}، ناموفق {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "پاکسازی برخی شبکهها ناموفق بود؛ گزارش وظیفه را بررسی کنید"
|
||||
ContainerBackupPrepare: 'آمادهسازی فضای کاری پشتیبان کانتینر'
|
||||
ContainerBackupStop: 'توقف کانتینر قبل از پشتیبانگیری'
|
||||
ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان'
|
||||
@@ -684,8 +687,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راهاندازی مجدد کنید و دوباره تلاش کنید'
|
||||
ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بکاند فایروال nftables پشتیبانی کند، سپس داکر را راهاندازی مجدد کرده و دوباره تلاش کنید'
|
||||
ErrDockerForwardPolicyDrop: 'سیاست پیشفرض FORWARD برای {{ .family }} برابر DROP است. آن را تغییر دهید و دوباره تلاش کنید'
|
||||
ErrUFWRuleAdopt: 'مدیریت قانون UFW ناموفق بود: {{ .detail }}. اگر نسخه فعلی UFW قدیمیتر از 0.35 است، ابتدا UFW را ارتقا دهید و دوباره تلاش کنید'
|
||||
ErrDockerForwardPolicyDrop: 'این سرور هدایت ترافیک شبکه را محدود کرده است. در حال حاضر امکان فعالسازی محافظت از پورت کانتینرها وجود ندارد.'
|
||||
Firewall: 'فایروال'
|
||||
FirewallTaskHost: 'دیواره آتش میزبان'
|
||||
FirewallTaskForwarding: 'هدایت پورت'
|
||||
@@ -699,28 +701,15 @@ FirewallCreateRulesStep: 'ایجاد قوانین دیوار آتش'
|
||||
FirewallCreateRulesResult: 'نتیجه ایجاد: {{ .succeeded }} موفق، {{ .failed }} ناموفق، {{ .skipped }} اجرا نشده'
|
||||
FirewallRuleOperationResult: 'نتیجه عملیات: {{ .succeeded }} موفق، {{ .failed }} ناموفق'
|
||||
FirewallCreateRuleSkipped: 'اجرا نشده'
|
||||
FirewallCreateBatchStep: 'ارسال {{ .count }} قانون به صورت دستهای به {{ .backend }}'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}؛ به {{ .count }} قانون تبدیل شد'
|
||||
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
|
||||
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
|
||||
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
|
||||
FirewallSyncOperationsResult: 'عملیات همگامسازی: {{ .removed }} حذفشده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرانشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
|
||||
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
|
||||
FirewallSyncStep: 'همگامسازی قوانین با {{ .name }}'
|
||||
FirewallSyncFailed: 'همگامسازی {{ .failed }} قانون فایروال ناموفق بود'
|
||||
FirewallResetSourceStep: 'بازنشانی و غیرفعالکردن فایروال مبدأ {{ .name }}'
|
||||
FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد'
|
||||
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. دستورات اجراشده بازگردانی نشدند'
|
||||
FirewallInitializeChainsStep: 'راهاندازی و اتصال زنجیرههای پایه {{ .name }}'
|
||||
FirewallRestoreRulesStep: 'بازیابی قوانین پایگاه داده در {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'همگامسازی فهرست مجاز پورتهای فایروال'
|
||||
FirewallEnableForwardingStep: 'فعالسازی و اتصال زنجیرههای هدایت پورت'
|
||||
FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پورت پایگاه داده'
|
||||
FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاستها'
|
||||
FirewallInitializeDockerGuardStep: 'راهاندازی و اتصال زنجیرههای محافظت پورت {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
|
||||
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمیکند. لطفاً یک قانون جدید ایجاد کنید."
|
||||
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ میشود. برای بستن پورت، قانون را بهصورت دستی از فهرست قوانین حذف کنید"
|
||||
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورتهای ضروری سیستم محافظت میشود"
|
||||
FileTaskCopy: 'کپی فایلها به {{ .dst }}'
|
||||
FileTaskMove: 'انتقال فایلها به {{ .dst }}'
|
||||
FileTaskCompress: 'فشردهسازی فایلها در {{ .dst }}'
|
||||
@@ -728,11 +717,56 @@ FileTaskDecompress: 'استخراج فایلها در {{ .dst }}'
|
||||
FileTaskSource: 'مسیر مبدأ: {{ .path }}'
|
||||
FileTaskFormat: 'قالب بایگانی: {{ .format }}'
|
||||
FileTaskRename: 'نام فایل مقصد: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "بکاند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آنها را پاک کنید."
|
||||
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بکاند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
|
||||
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگامسازی دوباره تلاش کنید: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید."
|
||||
ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخصشده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید."
|
||||
ErrIPv6RARisk: "فعالسازی هدایت IPv6 به دلیل خطر قطع شبکه مسدود شد. مستندات و گزارشهای سیستم را بررسی کنید."
|
||||
ErrIPv6RACheckFailed: "فعالسازی هدایت IPv6 به دلیل عدم امکان بررسی پیکربندی شبکه مسدود شد. جزئیات در گزارشهای سیستم موجود است."
|
||||
|
||||
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
|
||||
ErrSSHPrivateKey: "کلید خصوصی 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
@@ -300,6 +300,9 @@ BuildCache: 'ビルドキャッシュ'
|
||||
Volume: 'ストレージボリューム'
|
||||
Network: 'ネットワーク'
|
||||
PruneStart: 'クリーンアップ中、しばらくお待ちください...'
|
||||
NetworkCleanupConnected: "ネットワーク [{{ .name }}] ({{ .id }}) をスキップしました:コンテナが接続されているか使用中のため、削除しませんでした"
|
||||
NetworkCleanupSummary: "ネットワークのクリーンアップが完了しました:削除 {{ .deleted }} 件、スキップ {{ .skipped }} 件、失敗 {{ .failed }} 件"
|
||||
NetworkCleanupPartialFailure: "一部のネットワークをクリーンアップできませんでした。タスクログを確認してください"
|
||||
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
|
||||
ContainerBackupStop: 'バックアップ前にコンテナを停止'
|
||||
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
|
||||
ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください'
|
||||
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD の既定ポリシーが DROP です。変更してから再試行してください'
|
||||
ErrUFWRuleAdopt: 'UFW ルールの管理に失敗しました:{{ .detail }}。現在の UFW バージョンが 0.35 未満の場合は、UFW をアップグレードしてから再試行してください'
|
||||
ErrDockerForwardPolicyDrop: 'このサーバーではネットワーク転送が制限されているため、現在コンテナのポート保護を有効にできません。'
|
||||
Firewall: 'ファイアウォール'
|
||||
FirewallTaskHost: 'ホストファイアウォール'
|
||||
FirewallTaskForwarding: 'ポート転送'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'ファイアウォールルールを作成'
|
||||
FirewallCreateRulesResult: '作成結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
|
||||
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件'
|
||||
FirewallCreateRuleSkipped: '未実行'
|
||||
FirewallCreateBatchStep: '{{ .backend }} に {{ .count }} 件のルールを一括送信'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }};{{ .count }} 件のルールに変換'
|
||||
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
|
||||
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
|
||||
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
|
||||
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
|
||||
FirewallSyncRuleUnchanged: '一致済み、変更不要'
|
||||
FirewallSyncStep: '{{ .name }} にルールを同期'
|
||||
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
|
||||
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
|
||||
FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました'
|
||||
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。実行済みのコマンドはロールバックしません'
|
||||
FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド'
|
||||
FirewallRestoreRulesStep: 'データベースルールを {{ .name }} に復元'
|
||||
FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期'
|
||||
FirewallEnableForwardingStep: 'ポート転送チェーンを有効化してバインド'
|
||||
FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルールを復元'
|
||||
FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認'
|
||||
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
|
||||
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
|
||||
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
|
||||
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
|
||||
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"
|
||||
FileTaskCopy: 'ファイルを {{ .dst }} にコピー'
|
||||
FileTaskMove: 'ファイルを {{ .dst }} に移動'
|
||||
FileTaskCompress: 'ファイルを {{ .dst }} に圧縮'
|
||||
@@ -727,11 +716,56 @@ FileTaskDecompress: 'ファイルを {{ .dst }} に展開'
|
||||
FileTaskSource: '元のパス:{{ .path }}'
|
||||
FileTaskFormat: '圧縮形式:{{ .format }}'
|
||||
FileTaskRename: '保存先ファイル名:{{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
|
||||
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
|
||||
ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。"
|
||||
ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。"
|
||||
ErrIPv6RARisk: "ネットワーク切断のおそれがあるため、IPv6 転送の有効化をブロックしました。ドキュメントとシステムログをご確認ください。"
|
||||
ErrIPv6RACheckFailed: "ネットワーク設定を確認できないため、IPv6 転送の有効化をブロックしました。詳細はシステムログをご確認ください。"
|
||||
|
||||
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
|
||||
ErrSSHPrivateKey: "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
@@ -300,6 +300,9 @@ BuildCache: '빌드 캐시'
|
||||
Volume: '스토리지 볼륨'
|
||||
Network: '네트워크'
|
||||
PruneStart: '정리 중입니다. 잠시만 기다려주세요...'
|
||||
NetworkCleanupConnected: "네트워크 [{{ .name }}] ({{ .id }}) 건너뜀: 연결된 컨테이너가 있거나 사용 중이므로 삭제하지 않음"
|
||||
NetworkCleanupSummary: "네트워크 정리 완료: 삭제 {{ .deleted }}개, 건너뜀 {{ .skipped }}개, 실패 {{ .failed }}개"
|
||||
NetworkCleanupPartialFailure: "일부 네트워크를 정리하지 못했습니다. 작업 로그를 확인하세요"
|
||||
ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비'
|
||||
ContainerBackupStop: '백업 전에 컨테이너 중지'
|
||||
ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
|
||||
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요'
|
||||
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD 기본 정책이 DROP입니다. 정책을 조정한 후 다시 시도하세요'
|
||||
ErrUFWRuleAdopt: 'UFW 규칙 관리에 실패했습니다: {{ .detail }}. 현재 UFW 버전이 0.35 미만이면 UFW를 업그레이드한 후 다시 시도하세요'
|
||||
ErrDockerForwardPolicyDrop: '현재 서버에서 네트워크 전달이 제한되어 컨테이너 포트 보호를 활성화할 수 없습니다.'
|
||||
Firewall: '방화벽'
|
||||
FirewallTaskHost: '호스트 방화벽'
|
||||
FirewallTaskForwarding: '포트 포워딩'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: '방화벽 규칙 생성'
|
||||
FirewallCreateRulesResult: '생성 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
|
||||
FirewallRuleOperationResult: '작업 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개'
|
||||
FirewallCreateRuleSkipped: '미실행'
|
||||
FirewallCreateBatchStep: '{{ .backend }}에 규칙 {{ .count }}개 일괄 제출'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; 규칙 {{ .count }}개로 변환'
|
||||
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
|
||||
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
|
||||
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
|
||||
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
|
||||
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
|
||||
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
|
||||
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
|
||||
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
|
||||
FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다'
|
||||
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 실행된 명령은 롤백하지 않습니다'
|
||||
FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩'
|
||||
FirewallRestoreRulesStep: '데이터베이스 규칙을 {{ .name }}에 복원'
|
||||
FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화'
|
||||
FirewallEnableForwardingStep: '포트 전달 체인 활성화 및 바인딩'
|
||||
FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복원'
|
||||
FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인'
|
||||
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
|
||||
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
|
||||
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
|
||||
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
|
||||
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"
|
||||
FileTaskCopy: '{{ .dst }}에 파일 복사'
|
||||
FileTaskMove: '{{ .dst }}로 파일 이동'
|
||||
FileTaskCompress: '{{ .dst }}에 파일 압축'
|
||||
@@ -727,11 +716,56 @@ FileTaskDecompress: '{{ .dst }}에 압축 해제'
|
||||
FileTaskSource: '원본 경로: {{ .path }}'
|
||||
FileTaskFormat: '압축 형식: {{ .format }}'
|
||||
FileTaskRename: '대상 파일 이름: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
|
||||
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요."
|
||||
ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요."
|
||||
ErrIPv6RARisk: "네트워크 연결이 끊길 위험이 있어 IPv6 전달 활성화를 차단했습니다. 문서와 시스템 로그를 확인하세요."
|
||||
ErrIPv6RACheckFailed: "네트워크 구성을 확인할 수 없어 IPv6 전달 활성화를 차단했습니다. 자세한 내용은 시스템 로그를 확인하세요."
|
||||
|
||||
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
|
||||
ErrSSHPrivateKey: "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
@@ -290,6 +290,9 @@ BuildCache: 'ແຄດການສ້າງ'
|
||||
Volume: 'ວໍລຸມ (Volume)'
|
||||
Network: "ເຄືອຂ່າຍ"
|
||||
PruneStart: 'ກຳລັງດຳເນີນການທຳຄວາມສະອາດ'
|
||||
NetworkCleanupConnected: "ຂ້າມເຄືອຂ່າຍ [{{ .name }}] ({{ .id }}): ມີຄອນເທນເນີເຊື່ອມຕໍ່ຢູ່ ຫຼື ເຄືອຂ່າຍກຳລັງຖືກໃຊ້ງານ; ບໍ່ໄດ້ລຶບ"
|
||||
NetworkCleanupSummary: "ການລ້າງເຄືອຂ່າຍສຳເລັດ: ລຶບແລ້ວ {{ .deleted }}, ຂ້າມ {{ .skipped }}, ລົ້ມເຫຼວ {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "ບໍ່ສາມາດລ້າງບາງເຄືອຂ່າຍໄດ້; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
|
||||
ContainerBackupPrepare: 'ກຽມພື້ນທີ່ເຮັດວຽກສຳລັບສຳຮອງຂໍ້ມູນຄອນເທນເນີ'
|
||||
ContainerBackupStop: 'ຢຸດຄອນເທນເນີກ່ອນການສຳຮອງຂໍ້ມູນ'
|
||||
ContainerBackupInspect: 'ກວດສອບຄອນເທນເນີ ແລະ ເຄືອຂ່າຍສຳລັບການສຳຮອງຂໍ້ມູນ'
|
||||
@@ -674,8 +677,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
|
||||
ErrDockerNftablesChainUnavailable: 'ບໍ່ພົບ chain ໄຟວໍ nftables IPv4 ຂອງ Docker. ກະລຸນາກວດສອບວ່າ Docker ລຸ້ນປັດຈຸບັນຮອງຮັບ backend ໄຟວໍ nftables, ເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
|
||||
ErrDockerForwardPolicyDrop: 'ນະໂຍບາຍເລີ່ມຕົ້ນ FORWARD ຂອງ {{ .family }} ແມ່ນ DROP. ກະລຸນາປັບແລ້ວລອງອີກຄັ້ງ'
|
||||
ErrUFWRuleAdopt: 'ຈັດການກົດ UFW ບໍ່ສຳເລັດ: {{ .detail }}. ຖ້າ UFW ລຸ້ນປັດຈຸບັນຕ່ຳກວ່າ 0.35, ກະລຸນາອັບເກຣດ UFW ແລ້ວລອງໃໝ່'
|
||||
ErrDockerForwardPolicyDrop: 'ເຊີບເວີນີ້ຈຳກັດການສົ່ງຕໍ່ເຄືອຂ່າຍ. ຍັງບໍ່ສາມາດເປີດໃຊ້ການປ້ອງກັນພອດຄອນເທນເນີໄດ້ໃນຕອນນີ້.'
|
||||
Firewall: 'ໄຟວອລ'
|
||||
FirewallTaskHost: 'ໄຟວໍໂຮສ'
|
||||
FirewallTaskForwarding: 'ການສົ່ງຕໍ່ພອດ'
|
||||
@@ -689,28 +691,15 @@ FirewallCreateRulesStep: 'ສ້າງກົດໄຟວໍ'
|
||||
FirewallCreateRulesResult: 'ຜົນການສ້າງ: ສຳເລັດ {{ .succeeded }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
|
||||
FirewallRuleOperationResult: 'ຜົນການດຳເນີນການ: ສຳເລັດ {{ .succeeded }} ລາຍການ, ລົ້ມເຫຼວ {{ .failed }} ລາຍການ'
|
||||
FirewallCreateRuleSkipped: 'ບໍ່ໄດ້ດຳເນີນການ'
|
||||
FirewallCreateBatchStep: 'ສົ່ງ {{ .count }} ກົດເປັນຊຸດໄປຫາ {{ .backend }}'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ແປງເປັນ {{ .count }} ກົດ'
|
||||
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
|
||||
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
|
||||
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
|
||||
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
|
||||
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
|
||||
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
|
||||
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
|
||||
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
|
||||
FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ'
|
||||
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ຄຳສັ່ງທີ່ດຳເນີນການແລ້ວບໍ່ໄດ້ຖືກຍ້ອນກັບ'
|
||||
FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}'
|
||||
FirewallRestoreRulesStep: 'ກູ້ຄືນກົດຖານຂໍ້ມູນໄປຫາ {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ'
|
||||
FirewallEnableForwardingStep: 'ເປີດໃຊ້ ແລະ ຜູກ chain ສົ່ງຕໍ່ພອດ'
|
||||
FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕໍ່ພອດຈາກຖານຂໍ້ມູນ'
|
||||
FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ'
|
||||
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
|
||||
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
|
||||
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
|
||||
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"
|
||||
FileTaskCopy: 'ສຳເນົາໄຟລ໌ໄປທີ່ {{ .dst }}'
|
||||
FileTaskMove: 'ຍ້າຍໄຟລ໌ໄປທີ່ {{ .dst }}'
|
||||
FileTaskCompress: 'ບີບອັດໄຟລ໌ໄປທີ່ {{ .dst }}'
|
||||
@@ -718,11 +707,56 @@ FileTaskDecompress: 'ແຕກໄຟລ໌ໄປທີ່ {{ .dst }}'
|
||||
FileTaskSource: 'ເສັ້ນທາງຕົ້ນທາງ: {{ .path }}'
|
||||
FileTaskFormat: 'ຮູບແບບໄຟລ໌ບີບອັດ: {{ .format }}'
|
||||
FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່."
|
||||
ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່."
|
||||
ErrIPv6RARisk: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເພື່ອປ້ອງກັນຄວາມສ່ຽງທີ່ເຄືອຂ່າຍຈະຂາດການເຊື່ອມຕໍ່. ເບິ່ງເອກະສານ ແລະ ບັນທຶກລະບົບ."
|
||||
ErrIPv6RACheckFailed: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເນື່ອງຈາກບໍ່ສາມາດກວດສອບການຕັ້ງຄ່າເຄືອຂ່າຍໄດ້. ເບິ່ງລາຍລະອຽດໃນບັນທຶກລະບົບ."
|
||||
|
||||
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
|
||||
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ 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
@@ -300,6 +300,9 @@ BuildCache: 'Cache binaan'
|
||||
Volume: 'Jilid storan'
|
||||
Network: 'Rangkaian'
|
||||
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'
|
||||
ContainerBackupStop: 'Hentikan kontena sebelum sandaran'
|
||||
ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi'
|
||||
ErrDockerNftablesChainUnavailable: 'Rantaian firewall IPv4 nftables Docker tidak ditemui. Pastikan versi Docker semasa menyokong bahagian belakang firewall nftables, mulakan semula Docker dan cuba lagi'
|
||||
ErrDockerForwardPolicyDrop: 'Dasar lalai FORWARD {{ .family }} ialah DROP. Laraskan dasar dan cuba lagi'
|
||||
ErrUFWRuleAdopt: 'Gagal mengurus peraturan UFW: {{ .detail }}. Jika versi UFW semasa lebih lama daripada 0.35, tingkatkan UFW dan cuba lagi'
|
||||
ErrDockerForwardPolicyDrop: 'Pelayan ini mengehadkan pemajuan rangkaian. Perlindungan port kontena tidak dapat diaktifkan buat masa ini.'
|
||||
Firewall: 'Firewall'
|
||||
FirewallTaskHost: 'Tembok api hos'
|
||||
FirewallTaskForwarding: 'Pemajuan port'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Cipta peraturan tembok api'
|
||||
FirewallCreateRulesResult: 'Hasil penciptaan: {{ .succeeded }} berjaya, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
|
||||
FirewallRuleOperationResult: 'Hasil operasi: {{ .succeeded }} berjaya, {{ .failed }} gagal'
|
||||
FirewallCreateRuleSkipped: 'Tidak dilaksanakan'
|
||||
FirewallCreateBatchStep: 'Hantar {{ .count }} peraturan secara kelompok ke {{ .backend }}'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ditukar kepada {{ .count }} peraturan'
|
||||
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan'
|
||||
FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
|
||||
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan'
|
||||
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)'
|
||||
FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan'
|
||||
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
|
||||
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
|
||||
FirewallResetSourceResult: 'Firewall sumber ditetapkan semula; {{ .removed }} peraturan pangkalan data dipadamkan'
|
||||
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Arahan yang dilaksanakan tidak dibatalkan'
|
||||
FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}'
|
||||
FirewallRestoreRulesStep: 'Pulihkan peraturan pangkalan data ke {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall'
|
||||
FirewallEnableForwardingStep: 'Aktifkan dan ikat rantai pemajuan port'
|
||||
FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan data'
|
||||
FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
|
||||
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
|
||||
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
|
||||
FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan"
|
||||
FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"
|
||||
FileTaskCopy: 'Salin fail ke {{ .dst }}'
|
||||
FileTaskMove: 'Pindahkan fail ke {{ .dst }}'
|
||||
FileTaskCompress: 'Mampatkan fail ke {{ .dst }}'
|
||||
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Ekstrak fail ke {{ .dst }}'
|
||||
FileTaskSource: 'Laluan sumber: {{ .path }}'
|
||||
FileTaskFormat: 'Format arkib: {{ .format }}'
|
||||
FileTaskRename: 'Nama fail sasaran: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.ip_forward=1 sebelum menggunakan bahagian belakang tembok api Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "Peraturan UFW dengan syarat padanan yang sama tetapi tindakan berbeza sudah wujud. Sila edit peraturan sedia ada."
|
||||
ErrForwardInterfaceNotFound: "Antara muka rangkaian {{ .name }} yang dinyatakan dalam peraturan pemajuan tidak wujud. Semak namanya dan cuba lagi."
|
||||
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"
|
||||
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"
|
||||
|
||||
# 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
@@ -300,6 +300,9 @@ BuildCache: 'Cache de construção'
|
||||
Volume: 'Volume de armazenamento'
|
||||
Network: 'Rede'
|
||||
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'
|
||||
ContainerBackupStop: 'Parar contêiner antes do backup'
|
||||
ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente'
|
||||
ErrDockerNftablesChainUnavailable: 'A cadeia de firewall IPv4 do nftables do Docker não foi encontrada. Confirme se a versão atual do Docker oferece suporte ao backend de firewall nftables, reinicie o Docker e tente novamente'
|
||||
ErrDockerForwardPolicyDrop: 'A política padrão de FORWARD para {{ .family }} é DROP. Ajuste-a e tente novamente'
|
||||
ErrUFWRuleAdopt: 'Falha ao gerenciar a regra UFW: {{ .detail }}. Se a versão atual do UFW for anterior à 0.35, atualize o UFW e tente novamente'
|
||||
ErrDockerForwardPolicyDrop: 'Este servidor restringe o encaminhamento de rede. Não é possível ativar a proteção de portas de contêineres no momento.'
|
||||
Firewall: 'Firewall'
|
||||
FirewallTaskHost: 'Firewall do host'
|
||||
FirewallTaskForwarding: 'Encaminhamento de portas'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Criar regras de firewall'
|
||||
FirewallCreateRulesResult: 'Resultado da criação: {{ .succeeded }} com sucesso, {{ .failed }} com falha, {{ .skipped }} não executadas'
|
||||
FirewallRuleOperationResult: 'Resultado da operação: {{ .succeeded }} com sucesso, {{ .failed }} com falha'
|
||||
FirewallCreateRuleSkipped: 'Não executada'
|
||||
FirewallCreateBatchStep: 'Enviar {{ .count }} regras em lote para {{ .backend }}'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido em {{ .count }} regras'
|
||||
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos'
|
||||
FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento'
|
||||
FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento'
|
||||
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas, {{ .unchanged }} já correspondem (sem alterações necessárias)'
|
||||
FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária'
|
||||
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
|
||||
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
|
||||
FirewallResetSourceResult: 'Firewall de origem redefinido; {{ .removed }} regras do banco de dados foram excluídas'
|
||||
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Os comandos executados não foram revertidos'
|
||||
FirewallInitializeChainsStep: 'Inicializar e vincular as cadeias base do {{ .name }}'
|
||||
FirewallRestoreRulesStep: 'Restaurar regras do banco de dados para {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'Sincronizar a lista de portas permitidas do firewall'
|
||||
FirewallEnableForwardingStep: 'Habilitar e vincular as cadeias de encaminhamento de portas'
|
||||
FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de portas do banco de dados'
|
||||
FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as políticas'
|
||||
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker'
|
||||
ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra."
|
||||
FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista"
|
||||
FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema"
|
||||
FileTaskCopy: 'Copiar arquivos para {{ .dst }}'
|
||||
FileTaskMove: 'Mover arquivos para {{ .dst }}'
|
||||
FileTaskCompress: 'Compactar arquivos em {{ .dst }}'
|
||||
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Extrair arquivos para {{ .dst }}'
|
||||
FileTaskSource: 'Caminho de origem: {{ .path }}'
|
||||
FileTaskFormat: 'Formato do arquivo: {{ .format }}'
|
||||
FileTaskRename: 'Nome do arquivo de destino: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "O backend atual {{ .current }} ainda contém regras do 1Panel. Remova-as antes de mudar para {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina net.ipv4.ip_forward=1 antes de usar o backend de firewall do Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "Já existe uma regra UFW com as mesmas condições e uma ação diferente. Edite a regra existente."
|
||||
ErrForwardInterfaceNotFound: "A interface de rede {{ .name }} especificada na regra de encaminhamento não existe. Verifique o nome e tente novamente."
|
||||
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"
|
||||
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"
|
||||
|
||||
# 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
@@ -300,6 +300,9 @@ BuildCache: 'Кэш сборки'
|
||||
Volume: 'Том хранилища'
|
||||
Network: 'Сеть'
|
||||
PruneStart: 'Очистка выполняется, пожалуйста, подождите...'
|
||||
NetworkCleanupConnected: "Сеть [{{ .name }}] ({{ .id }}) пропущена: подключены контейнеры или сеть используется; не удалена"
|
||||
NetworkCleanupSummary: "Очистка сетей завершена: удалено {{ .deleted }}, пропущено {{ .skipped }}, ошибок {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "Не удалось очистить некоторые сети; проверьте журнал задачи"
|
||||
ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера'
|
||||
ContainerBackupStop: 'Остановить контейнер перед резервным копированием'
|
||||
ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
|
||||
ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку'
|
||||
ErrDockerForwardPolicyDrop: 'Политика FORWARD по умолчанию для {{ .family }} — DROP. Измените её и повторите попытку'
|
||||
ErrUFWRuleAdopt: 'Не удалось принять правило UFW под управление: {{ .detail }}. Если текущая версия UFW ниже 0.35, обновите UFW и повторите попытку'
|
||||
ErrDockerForwardPolicyDrop: 'На этом сервере ограничена пересылка сетевого трафика. Сейчас невозможно включить защиту портов контейнеров.'
|
||||
Firewall: 'Межсетевой экран'
|
||||
FirewallTaskHost: 'Межсетевой экран хоста'
|
||||
FirewallTaskForwarding: 'Перенаправление портов'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: 'Создание правил межсетевого
|
||||
FirewallCreateRulesResult: 'Результат создания: успешно — {{ .succeeded }}, ошибок — {{ .failed }}, не выполнено — {{ .skipped }}'
|
||||
FirewallRuleOperationResult: 'Результат операции: успешно — {{ .succeeded }}, ошибок — {{ .failed }}'
|
||||
FirewallCreateRuleSkipped: 'Не выполнено'
|
||||
FirewallCreateBatchStep: 'Отправка {{ .count }} правил одним пакетом в {{ .backend }}'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; преобразовано в {{ .count }} правил'
|
||||
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
|
||||
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
|
||||
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
|
||||
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
|
||||
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
|
||||
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
|
||||
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
|
||||
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
|
||||
FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}'
|
||||
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Выполненные команды не отменены'
|
||||
FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}'
|
||||
FirewallRestoreRulesStep: 'Восстановить правила базы данных в {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана'
|
||||
FirewallEnableForwardingStep: 'Включить и привязать цепочки перенаправления портов'
|
||||
FirewallRestoreForwardingRulesStep: 'Восстановить правила перенаправления портов из базы данных'
|
||||
FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики'
|
||||
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
|
||||
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
|
||||
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
|
||||
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"
|
||||
FileTaskCopy: 'Копирование файлов в {{ .dst }}'
|
||||
FileTaskMove: 'Перемещение файлов в {{ .dst }}'
|
||||
FileTaskCompress: 'Сжатие файлов в {{ .dst }}'
|
||||
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Распаковка файлов в {{ .dst }}'
|
||||
FileTaskSource: 'Исходный путь: {{ .path }}'
|
||||
FileTaskFormat: 'Формат архива: {{ .format }}'
|
||||
FileTaskRename: 'Имя целевого файла: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
|
||||
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило."
|
||||
ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку."
|
||||
ErrIPv6RARisk: "Включение переадресации IPv6 заблокировано из-за риска потери связи. См. документацию и системные журналы."
|
||||
ErrIPv6RACheckFailed: "Включение переадресации IPv6 заблокировано: не удалось проверить настройки сети. Подробности в системных журналах."
|
||||
|
||||
ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
|
||||
ErrSSHPrivateKey: "Недопустимый закрытый ключ 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
@@ -300,6 +300,9 @@ BuildCache: 'Derleme önbelleği'
|
||||
Volume: 'Depolama hacmi'
|
||||
Network: 'Ağ'
|
||||
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'
|
||||
ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur'
|
||||
ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Docker’ı yeniden başlatıp tekrar deneyin'
|
||||
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 güvenlik duvarı zinciri bulunamadı. Geçerli Docker sürümünün nftables güvenlik duvarı arka ucunu desteklediğini doğrulayın, Docker’ı yeniden başlatıp tekrar deneyin'
|
||||
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD varsayılan ilkesi DROP. İlkeyi düzenleyip tekrar deneyin'
|
||||
ErrUFWRuleAdopt: 'UFW kuralı yönetilemedi: {{ .detail }}. Geçerli UFW sürümü 0.35’ten eskiyse UFW’yi yükseltip tekrar deneyin'
|
||||
ErrDockerForwardPolicyDrop: 'Bu sunucu ağ yönlendirmesini kısıtlıyor. Konteyner port koruması şu anda etkinleştirilemiyor.'
|
||||
Firewall: 'Güvenlik duvarı'
|
||||
FirewallTaskHost: 'Ana makine güvenlik duvarı'
|
||||
FirewallTaskForwarding: 'Port yönlendirme'
|
||||
@@ -698,28 +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'
|
||||
FirewallRuleOperationResult: 'İşlem sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız'
|
||||
FirewallCreateRuleSkipped: 'Yürütülmedi'
|
||||
FirewallCreateBatchStep: '{{ .backend }} için {{ .count }} kuralı toplu gönder'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; {{ .count }} kurala dönüştürüldü'
|
||||
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı'
|
||||
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
|
||||
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi'
|
||||
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)'
|
||||
FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor'
|
||||
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
|
||||
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
|
||||
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
|
||||
FirewallResetSourceResult: 'Kaynak güvenlik duvarı sıfırlandı; {{ .removed }} veritabanı kuralı silindi'
|
||||
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Yürütülen komutlar geri alınmadı'
|
||||
FirewallInitializeChainsStep: '{{ .name }} temel zincirlerini başlat ve bağla'
|
||||
FirewallRestoreRulesStep: 'Veritabanı kurallarını {{ .name }} üzerine geri yükle'
|
||||
FirewallSyncWhitelistStep: 'Güvenlik duvarı bağlantı noktası izin listesini eşitle'
|
||||
FirewallEnableForwardingStep: 'Bağlantı noktası yönlendirme zincirlerini etkinleştir ve bağla'
|
||||
FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirme kurallarını geri yükle'
|
||||
FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkelerini denetle'
|
||||
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
|
||||
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet'
|
||||
ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun."
|
||||
FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin"
|
||||
FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor"
|
||||
FileTaskCopy: 'Dosyaları {{ .dst }} konumuna kopyala'
|
||||
FileTaskMove: 'Dosyaları {{ .dst }} konumuna taşı'
|
||||
FileTaskCompress: 'Dosyaları {{ .dst }} konumuna sıkıştır'
|
||||
@@ -727,11 +716,56 @@ FileTaskDecompress: 'Dosyaları {{ .dst }} konumuna çıkar'
|
||||
FileTaskSource: 'Kaynak yol: {{ .path }}'
|
||||
FileTaskFormat: 'Arşiv biçimi: {{ .format }}'
|
||||
FileTaskRename: 'Hedef dosya adı: {{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin."
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güvenlik duvarı arka ucunu kullanmadan önce net.ipv4.ip_forward=1 ayarını yapın."
|
||||
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "UFW içinde aynı eşleşme koşullarına sahip, farklı bir eylem uygulayan bir kural zaten var. Mevcut kuralı düzenleyin."
|
||||
ErrForwardInterfaceNotFound: "Yönlendirme kuralında belirtilen {{ .name }} ağ arayüzü mevcut değil. Arayüz adını kontrol edip yeniden deneyin."
|
||||
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"
|
||||
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
|
||||
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"
|
||||
|
||||
@@ -300,6 +300,9 @@ BuildCache: '構建快取'
|
||||
Volume: '磁碟區'
|
||||
Network: '網路'
|
||||
PruneStart: '清理中,請稍候...'
|
||||
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
|
||||
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
|
||||
ContainerBackupPrepare: '準備容器備份工作目錄'
|
||||
ContainerBackupStop: '備份前停止容器'
|
||||
ContainerBackupInspect: '備份容器 Inspect 與網路資訊'
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: '火山方舟 Coding Plan'
|
||||
# 防火牆
|
||||
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
|
||||
ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試'
|
||||
ErrDockerForwardPolicyDrop: '偵測到 {{ .family }} FORWARD 預設策略為 DROP,請調整後重試'
|
||||
ErrUFWRuleAdopt: 'UFW 規則納管失敗:{{ .detail }}。如果目前 UFW 版本低於 0.35,請先升級 UFW 後再試'
|
||||
ErrDockerForwardPolicyDrop: '目前伺服器限制了網路轉送,暫時無法啟用容器連接埠防護。'
|
||||
Firewall: '防火牆'
|
||||
FirewallTaskHost: '主機防火牆'
|
||||
FirewallTaskForwarding: '連接埠轉發'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: '建立防火牆規則'
|
||||
FirewallCreateRulesResult: '建立結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
|
||||
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條'
|
||||
FirewallCreateRuleSkipped: '未執行'
|
||||
FirewallCreateBatchStep: '向 {{ .backend }} 批次提交 {{ .count }} 條規則'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};轉換為 {{ .count }} 條規則'
|
||||
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
|
||||
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
|
||||
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
|
||||
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
|
||||
FirewallSyncRuleUnchanged: '已一致,無需變更'
|
||||
FirewallSyncStep: '同步規則到 {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
|
||||
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
|
||||
FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則'
|
||||
FirewallCreateRuleExecutionFailed: '規則建立失敗,已執行的命令不回復'
|
||||
FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈'
|
||||
FirewallRestoreRulesStep: '恢復資料庫規則至 {{ .name }}'
|
||||
FirewallSyncWhitelistStep: '同步防火牆連接埠白名單'
|
||||
FirewallEnableForwardingStep: '啟用並綁定連接埠轉發鏈'
|
||||
FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則'
|
||||
FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
|
||||
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
|
||||
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
|
||||
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
|
||||
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
|
||||
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"
|
||||
FileTaskCopy: '複製檔案至 {{ .dst }}'
|
||||
FileTaskMove: '移動檔案至 {{ .dst }}'
|
||||
FileTaskCompress: '壓縮檔案至 {{ .dst }}'
|
||||
@@ -727,21 +716,56 @@ FileTaskDecompress: '解壓檔案至 {{ .dst }}'
|
||||
FileTaskSource: '來源路徑:{{ .path }}'
|
||||
FileTaskFormat: '壓縮格式:{{ .format }}'
|
||||
FileTaskRename: '目標檔名:{{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
|
||||
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
|
||||
ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。"
|
||||
ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。"
|
||||
ErrIPv6RARisk: "IPv6 轉送可能導致網路中斷,已阻止啟用。請參閱文件,詳情見系統日誌。"
|
||||
ErrIPv6RACheckFailed: "無法檢查 IPv6 網路設定,已阻止啟用轉送。詳情見系統日誌。"
|
||||
|
||||
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
|
||||
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
|
||||
ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
|
||||
|
||||
NetworkCleanupDeleted: "網路 [{{ .name }}] ({{ .id }}) 已刪除"
|
||||
NetworkCleanupProtected: "跳過網路 [{{ .name }}] ({{ .id }}):預設保留網路,未刪除"
|
||||
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
|
||||
NetworkCleanupUnsupported: "跳過網路 [{{ .name }}] ({{ .id }}):特殊網路,未刪除"
|
||||
NetworkCleanupGone: "跳過網路 [{{ .name }}] ({{ .id }}):網路已不存在"
|
||||
NetworkCleanupInspectFailed: "網路 [{{ .name }}] ({{ .id }}) 檢查失敗,未刪除"
|
||||
NetworkCleanupRemoveFailed: "網路 [{{ .name }}] ({{ .id }}) 刪除失敗"
|
||||
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
|
||||
# 虛擬機
|
||||
ErrVMNotFound: "虛擬機不存在"
|
||||
ErrVMAlreadyExists: "虛擬機已存在"
|
||||
ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。"
|
||||
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 }}]"
|
||||
|
||||
+50
-26
@@ -300,6 +300,9 @@ BuildCache: "构建缓存"
|
||||
Volume: "存储卷"
|
||||
Network: "网络"
|
||||
PruneStart: "清理中,请稍候..."
|
||||
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
|
||||
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
|
||||
ContainerBackupPrepare: "准备容器备份工作目录"
|
||||
ContainerBackupStop: "备份前停止容器"
|
||||
ContainerBackupInspect: "备份容器 Inspect 与网络信息"
|
||||
@@ -683,8 +686,7 @@ AIProviderArkCodingPlan: "火山方舟 Coding Plan"
|
||||
# 防火墙
|
||||
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
|
||||
ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试"
|
||||
ErrDockerForwardPolicyDrop: '检测到 {{ .family }} FORWARD 默认策略为 DROP,请调整后重试'
|
||||
ErrUFWRuleAdopt: "UFW 规则纳管失败:{{ .detail }}。如果当前 UFW 版本低于 0.35,请先升级 UFW 后重试"
|
||||
ErrDockerForwardPolicyDrop: '当前服务器限制了网络转发,暂时无法启用容器端口防护。'
|
||||
Firewall: "防火墙"
|
||||
FirewallTaskHost: '主机防火墙'
|
||||
FirewallTaskForwarding: '端口转发'
|
||||
@@ -698,28 +700,15 @@ FirewallCreateRulesStep: '创建防火墙规则'
|
||||
FirewallCreateRulesResult: '创建结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
|
||||
FirewallRuleOperationResult: '操作结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条'
|
||||
FirewallCreateRuleSkipped: '未执行'
|
||||
FirewallCreateBatchStep: '向 {{ .backend }} 批量提交 {{ .count }} 条规则'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};转换为 {{ .count }} 条规则'
|
||||
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚'
|
||||
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
|
||||
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
|
||||
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
|
||||
FirewallSyncRuleUnchanged: '已一致,无需变更'
|
||||
FirewallSyncStep: "同步规则到 {{ .name }}"
|
||||
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
|
||||
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
|
||||
FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则"
|
||||
FirewallCreateRuleExecutionFailed: '规则创建失败,已执行的命令不回滚'
|
||||
FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链"
|
||||
FirewallRestoreRulesStep: "恢复数据库规则到 {{ .name }}"
|
||||
FirewallSyncWhitelistStep: "同步防火墙端口白名单"
|
||||
FirewallEnableForwardingStep: "启用并绑定端口转发链"
|
||||
FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则"
|
||||
FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
|
||||
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
|
||||
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
|
||||
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
|
||||
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
|
||||
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"
|
||||
FileTaskCopy: '复制文件到 {{ .dst }}'
|
||||
FileTaskMove: '移动文件到 {{ .dst }}'
|
||||
FileTaskCompress: '压缩文件到 {{ .dst }}'
|
||||
@@ -727,21 +716,56 @@ FileTaskDecompress: '解压文件到 {{ .dst }}'
|
||||
FileTaskSource: '源路径:{{ .path }}'
|
||||
FileTaskFormat: '压缩格式:{{ .format }}'
|
||||
FileTaskRename: '目标文件名:{{ .name }}'
|
||||
|
||||
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
|
||||
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
|
||||
ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。"
|
||||
ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。"
|
||||
ErrIPv6RARisk: "IPv6 转发存在断网风险,已阻止启用。请参阅文档,详情见系统日志。"
|
||||
ErrIPv6RACheckFailed: "无法检查 IPv6 网络配置,已阻止启用转发。详情见系统日志。"
|
||||
|
||||
ErrSSHPublicKey: "请输入有效的 SSH 公钥"
|
||||
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
|
||||
ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
|
||||
|
||||
NetworkCleanupDeleted: "网络 [{{ .name }}] ({{ .id }}) 已删除"
|
||||
NetworkCleanupProtected: "跳过网络 [{{ .name }}] ({{ .id }}):默认保留网络,未删除"
|
||||
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
|
||||
NetworkCleanupUnsupported: "跳过网络 [{{ .name }}] ({{ .id }}):特殊网络,未删除"
|
||||
NetworkCleanupGone: "跳过网络 [{{ .name }}] ({{ .id }}):网络已不存在"
|
||||
NetworkCleanupInspectFailed: "网络 [{{ .name }}] ({{ .id }}) 检查失败,未删除"
|
||||
NetworkCleanupRemoveFailed: "网络 [{{ .name }}] ({{ .id }}) 删除失败"
|
||||
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
|
||||
# 虚拟机
|
||||
ErrVMNotFound: "虚拟机不存在"
|
||||
ErrVMAlreadyExists: "虚拟机已存在"
|
||||
ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。"
|
||||
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 }}]"
|
||||
|
||||
@@ -6,20 +6,26 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations"
|
||||
migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
ctx := context.Background()
|
||||
defer initDockerPortGuard(ctx)
|
||||
if err := initForwardingRules(ctx); err != nil {
|
||||
global.LOG.Warnf("restore forwarding rules from file failed, err: %v", err)
|
||||
}
|
||||
client, err := service.NewSelectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("select system firewall provider failed, err: %v", err)
|
||||
@@ -31,38 +37,33 @@ func Init() {
|
||||
if err := migrations.TransferFirewalldSSHService(ctx, client, service.NewIFirewallService().SyncPortWhitelist); err != nil {
|
||||
global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err)
|
||||
}
|
||||
if initialize {
|
||||
initDockerPortGuard(ctx)
|
||||
}
|
||||
}()
|
||||
if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil {
|
||||
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
|
||||
families := []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}
|
||||
nftFamilies := []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
ipv6Status, err := repo.NewISettingRepo().GetValueByKey(constant.FirewallIPv6SupportKey)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
global.LOG.Errorf("load firewall IPv6 setting failed: %v", err)
|
||||
return
|
||||
}
|
||||
if err := migrationutils.TransferLegacyHostFirewallRuleOwnership(ctx, clientName, service.AdoptLegacyHostFirewallRuleOwnership); err != nil {
|
||||
global.LOG.Warnf("transfer legacy host firewall rule ownership failed, err: %v", err)
|
||||
}
|
||||
if err := migrationutils.TransferFirewallForwarding(ctx); err != nil {
|
||||
global.LOG.Errorf("transfer legacy forwarding rules failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
if err := initForwardingRules(ctx); err != nil {
|
||||
global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err)
|
||||
if ipv6Status == constant.StatusDisable {
|
||||
global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 host chains and saved rules during startup")
|
||||
families = families[:1]
|
||||
nftFamilies = nftFamilies[:1]
|
||||
}
|
||||
initialize = needInit()
|
||||
if !initialize {
|
||||
repairIptablesBaseChains(clientName)
|
||||
repairIptablesBaseChains(clientName, families...)
|
||||
return
|
||||
}
|
||||
InitPingStatus()
|
||||
global.LOG.Info("initializing firewall settings...")
|
||||
if clientName == "nftables" {
|
||||
if err := nftables_helper.Restore(); err != nil {
|
||||
if err := nftables_helper.Restore(nftFamilies...); err != nil {
|
||||
global.LOG.Errorf("restore nftables rules failed, err: %v", err)
|
||||
}
|
||||
status, _ := repo.NewISettingRepo().GetValueByKey("IptablesStatus")
|
||||
if status == constant.StatusEnable {
|
||||
if err := nftables_helper.Bind(); err != nil {
|
||||
if err := nftables_helper.Bind(nftFamilies...); err != nil {
|
||||
global.LOG.Errorf("bind nftables base chains failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -78,7 +79,7 @@ func Init() {
|
||||
global.LOG.Errorf("load required firewall ports failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
if err := iptables_helper.RestoreBaseChains(requiredPorts); err != nil {
|
||||
if err := iptables_helper.RestoreBaseChains(requiredPorts, families...); err != nil {
|
||||
global.LOG.Errorf("restore iptables base chains failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -94,7 +95,7 @@ func Init() {
|
||||
|
||||
}
|
||||
|
||||
func repairIptablesBaseChains(clientName string) {
|
||||
func repairIptablesBaseChains(clientName string, families ...string) {
|
||||
if clientName != constant.FirewallProviderIptables {
|
||||
return
|
||||
}
|
||||
@@ -108,7 +109,7 @@ func repairIptablesBaseChains(clientName string) {
|
||||
global.LOG.Warnf("load required firewall ports for base chain repair failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
if err := iptables_helper.RepairBaseChains(ports); err != nil {
|
||||
if err := iptables_helper.RepairBaseChains(ports, families...); err != nil {
|
||||
global.LOG.Warnf("repair iptables base chains failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -120,7 +121,7 @@ func initDockerPortGuard(ctx context.Context) {
|
||||
)
|
||||
var restoreErr error
|
||||
for attempt := 1; attempt <= attempts; attempt++ {
|
||||
restoreErr = service.ReconcileDockerPortGuard(ctx)
|
||||
restoreErr = service.RestoreDockerPortGuard(ctx)
|
||||
if restoreErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ func agentDBMigrations() []*gormigrate.Migration {
|
||||
migrations.AddFirewallRuleTable,
|
||||
migrations.InitDockerPortGuardStatus,
|
||||
migrations.NormalizeFirewallBackendSelections,
|
||||
migrations.SimplifyFirewallRulePolicy,
|
||||
migrations.AddDockerPortGuardReadOnly,
|
||||
migrations.MigrateFirewallDescriptions,
|
||||
migrations.MigrateFirewallPortWhitelistSources,
|
||||
migrations.AddAcceleratorMetrics,
|
||||
migrations.AddVLLMMonitor,
|
||||
|
||||
@@ -59,7 +59,6 @@ var AddTable = &gormigrate.Migration{
|
||||
&model.Favorite{},
|
||||
&model.FileShare{},
|
||||
&model.Host{},
|
||||
&model.FirewallRule{},
|
||||
&model.Ftp{},
|
||||
&model.ImageRepo{},
|
||||
&model.ScriptLibrary{},
|
||||
@@ -1784,7 +1783,7 @@ var AddComposePinned = &gormigrate.Migration{
|
||||
var AddFirewallRuleTable = &gormigrate.Migration{
|
||||
ID: "20260819-add-firewall-v2-tables",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.FirewallRule{}, &model.DockerPortGuardPolicy{}, &model.ForwardingRule{})
|
||||
return tx.AutoMigrate(&model.CommonDescription{})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1837,75 +1836,10 @@ var NormalizeFirewallBackendSelections = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
var SimplifyFirewallRulePolicy = &gormigrate.Migration{
|
||||
ID: "20260826-simplify-firewall-rule-policy",
|
||||
var MigrateFirewallDescriptions = &gormigrate.Migration{
|
||||
ID: "20260921-migrate-firewall-descriptions",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if !tx.Migrator().HasTable(&model.FirewallRule{}) {
|
||||
return nil
|
||||
}
|
||||
return tx.Transaction(func(tx *gorm.DB) error {
|
||||
if !tx.Migrator().HasColumn("firewall_rules", "compatibility_error") {
|
||||
if err := tx.Exec("ALTER TABLE firewall_rules ADD COLUMN compatibility_error text NOT NULL DEFAULT ''").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !tx.Migrator().HasColumn("firewall_rules", "sequence") {
|
||||
if err := tx.Exec("ALTER TABLE firewall_rules ADD COLUMN sequence integer").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if tx.Migrator().HasColumn("firewall_rules", "native_kind") {
|
||||
if err := tx.Exec(`UPDATE firewall_rules
|
||||
SET compatibility_error = 'legacy native firewall rule requires manual recreation: ' || native_kind
|
||||
WHERE native_kind IN ('zone_service', 'ufw_application', 'opaque')`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if tx.Migrator().HasColumn("firewall_rules", "provider") {
|
||||
if err := tx.Exec(`UPDATE firewall_rules
|
||||
SET priority = NULL, sequence = NULL`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_firewall_rules_sequence ON firewall_rules(sequence)").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, index := range []string{
|
||||
"idx_firewall_rules_scope_key",
|
||||
"uk_firewall_rules_scope_rule",
|
||||
"uk_firewall_rules_scope_match",
|
||||
} {
|
||||
if err := tx.Exec("DROP INDEX IF EXISTS " + index).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, column := range []string{
|
||||
"provider", "scope_key", "location", "native_kind", "order_index", "order_bucket", "rule_key", "match_key",
|
||||
} {
|
||||
if tx.Migrator().HasColumn("firewall_rules", column) {
|
||||
_ = tx.Exec("ALTER TABLE firewall_rules DROP COLUMN " + column).Error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var AddDockerPortGuardReadOnly = &gormigrate.Migration{
|
||||
ID: "20260902-add-docker-port-guard-read-only",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if !tx.Migrator().HasTable(&model.DockerPortGuardPolicy{}) {
|
||||
return nil
|
||||
}
|
||||
if err := tx.AutoMigrate(&model.DockerPortGuardPolicy{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if tx.Migrator().HasIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint") {
|
||||
if err := tx.Migrator().DropIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint")
|
||||
return tx.Transaction(migrationutils.MigrateHostFirewallDescriptions)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const firewallTransferMigrationID = "firewall-transfer"
|
||||
|
||||
type legacyFirewalldForward struct {
|
||||
rule forwarding.Rule
|
||||
spec string
|
||||
}
|
||||
|
||||
type legacyFirewalldForwardFailure struct {
|
||||
spec string
|
||||
err error
|
||||
}
|
||||
|
||||
type firewallTransferSource struct {
|
||||
rules []forwarding.Rule
|
||||
firewalld []legacyFirewalldForward
|
||||
provider string
|
||||
cleanupOld func([]legacyFirewalldForward) error
|
||||
}
|
||||
|
||||
type firewallTransfer struct {
|
||||
db *gorm.DB
|
||||
load func() (firewallTransferSource, error)
|
||||
}
|
||||
|
||||
// TransferFirewallForwarding migrates the legacy system-backed forwarding
|
||||
// inventory into the forwarding_rules table once. The migrations table is
|
||||
// also used as the completion ledger so a failed transfer can be retried on
|
||||
// the next agent start without introducing another persisted setting.
|
||||
func TransferFirewallForwarding(ctx context.Context) error {
|
||||
transfer := &firewallTransfer{
|
||||
db: global.DB,
|
||||
load: loadLegacyFirewallForwarding,
|
||||
}
|
||||
return transfer.run(ctx)
|
||||
}
|
||||
|
||||
func (t *firewallTransfer) run(ctx context.Context) error {
|
||||
if t.db == nil {
|
||||
return errors.New("firewall transfer database is required")
|
||||
}
|
||||
completed, err := firewallTransferCompleted(t.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if completed {
|
||||
return nil
|
||||
}
|
||||
if t.load == nil {
|
||||
return errors.New("legacy firewall forwarding loader is required")
|
||||
}
|
||||
source, err := t.load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := importLegacyForwardingRules(ctx, t.db, source.rules, source.provider); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(source.firewalld) > 0 {
|
||||
if source.cleanupOld == nil {
|
||||
return errors.New("legacy firewalld cleanup is required")
|
||||
}
|
||||
if err := source.cleanupOld(source.firewalld); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return markFirewallTransferCompleted(t.db)
|
||||
}
|
||||
|
||||
func firewallTransferCompleted(db *gorm.DB) (bool, error) {
|
||||
return migrationRecordExists(db, firewallTransferMigrationID)
|
||||
}
|
||||
|
||||
func markFirewallTransferCompleted(db *gorm.DB) error {
|
||||
return markMigrationRecord(db, firewallTransferMigrationID)
|
||||
}
|
||||
|
||||
func migrationRecordExists(db *gorm.DB, migrationID string) (bool, error) {
|
||||
var count int64
|
||||
if err := db.Table("migrations").Where("id = ?", migrationID).Count(&count).Error; err != nil {
|
||||
return false, fmt.Errorf("check migration record %q: %w", migrationID, err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func markMigrationRecord(db *gorm.DB, migrationID string) error {
|
||||
if err := db.Table("migrations").Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(map[string]interface{}{"id": migrationID}).Error; err != nil {
|
||||
return fmt.Errorf("record migration %q: %w", migrationID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importLegacyForwardingRules(ctx context.Context, db *gorm.DB, rules []forwarding.Rule, provider string) error {
|
||||
models := make([]model.ForwardingRule, 0, len(rules))
|
||||
seen := make(map[string]struct{}, len(rules))
|
||||
for _, rule := range rules {
|
||||
normalized, err := forwarding.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("normalize legacy forwarding rule: %w", err)
|
||||
}
|
||||
key := normalized.Identity()
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
models = append(models, model.ForwardingRule{
|
||||
Family: normalized.Family, Protocol: normalized.Protocol, Port: normalized.Port,
|
||||
TargetIP: normalized.TargetIP, TargetPort: normalized.TargetPort, Interface: normalized.Interface,
|
||||
})
|
||||
}
|
||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if len(models) > 0 {
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&models).Error; err != nil {
|
||||
return fmt.Errorf("import legacy forwarding rules: %w", err)
|
||||
}
|
||||
if err := updateOrCreateSetting(tx, "IptablesForwardStatus", constant.StatusEnable); err != nil {
|
||||
return err
|
||||
}
|
||||
if provider != "" {
|
||||
if err := updateOrCreateSetting(tx, "ForwardingBackend", provider); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func updateOrCreateSetting(tx *gorm.DB, key, value string) error {
|
||||
result := tx.Model(&model.Setting{}).Where("key = ?", key).Update("value", value)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&model.Setting{Key: key, Value: value}).Error
|
||||
}
|
||||
|
||||
func loadLegacyFirewallForwarding() (firewallTransferSource, error) {
|
||||
source := firewallTransferSource{cleanupOld: cleanupLegacyFirewalldForwarding}
|
||||
if cmd.Which("firewall-cmd") {
|
||||
firewalldRules, err := listLegacyFirewalldForwarding()
|
||||
if err != nil {
|
||||
return firewallTransferSource{}, err
|
||||
}
|
||||
supportedRules, cleanupRules, failures := selectSupportedLegacyFirewalldForwarding(firewalldRules)
|
||||
for _, failure := range failures {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
|
||||
}
|
||||
}
|
||||
source.rules = append(source.rules, supportedRules...)
|
||||
source.firewalld = append(source.firewalld, cleanupRules...)
|
||||
if len(source.rules) > 0 {
|
||||
source.provider = "iptables"
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
if _, err := lifecycle.ResolveIptablesCommands(); err != nil {
|
||||
return source, nil
|
||||
}
|
||||
rules, err := listLegacyIptablesForwarding()
|
||||
if err != nil {
|
||||
return firewallTransferSource{}, err
|
||||
}
|
||||
source.rules = append(source.rules, rules...)
|
||||
return source, nil
|
||||
}
|
||||
|
||||
func selectSupportedLegacyFirewalldForwarding(items []legacyFirewalldForward) (
|
||||
[]forwarding.Rule, []legacyFirewalldForward, []legacyFirewalldForwardFailure,
|
||||
) {
|
||||
rules := make([]forwarding.Rule, 0, len(items))
|
||||
cleanup := make([]legacyFirewalldForward, 0, len(items))
|
||||
failures := make([]legacyFirewalldForwardFailure, 0)
|
||||
for _, item := range items {
|
||||
if _, err := forwarding.NormalizeRule(item.rule); err != nil {
|
||||
failures = append(failures, legacyFirewalldForwardFailure{spec: item.spec, err: err})
|
||||
continue
|
||||
}
|
||||
rules = append(rules, item.rule)
|
||||
cleanup = append(cleanup, item)
|
||||
}
|
||||
return rules, cleanup, failures
|
||||
}
|
||||
|
||||
func listLegacyIptablesForwarding() ([]forwarding.Rule, error) {
|
||||
exists, err := iptables_helper.CheckChainExist(iptables_helper.NatTab, forwarding.ChainPreRouting)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
stdout, err := iptables_helper.RunWithStd(
|
||||
iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list legacy iptables forwarding rules: %w", err)
|
||||
}
|
||||
return parseLegacyIptablesForwarding(stdout), nil
|
||||
}
|
||||
|
||||
func parseLegacyIptablesForwarding(stdout string) []forwarding.Rule {
|
||||
rules := make([]forwarding.Rule, 0)
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 13 {
|
||||
continue
|
||||
}
|
||||
rule := forwarding.Rule{
|
||||
Family: forwarding.FamilyIPv4, Protocol: loadLegacyIptablesProtocol(fields[4]),
|
||||
Interface: fields[6], Port: loadLegacyIptablesSourcePort(fields[11]),
|
||||
}
|
||||
if len(fields) == 15 && fields[13] == "ports" {
|
||||
rule.TargetPort = fields[14]
|
||||
}
|
||||
if len(fields) == 13 && strings.HasPrefix(fields[12], "to:") {
|
||||
target := strings.TrimPrefix(fields[12], "to:")
|
||||
separator := strings.LastIndex(target, ":")
|
||||
if separator > 0 {
|
||||
rule.TargetIP, rule.TargetPort = target[:separator], target[separator+1:]
|
||||
}
|
||||
}
|
||||
if rule.TargetIP == "" {
|
||||
rule.TargetIP = "127.0.0.1"
|
||||
}
|
||||
rule.TargetPort = strings.TrimPrefix(rule.TargetPort, ":")
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func loadLegacyIptablesProtocol(protocol string) string {
|
||||
switch protocol {
|
||||
case "6":
|
||||
return "tcp"
|
||||
case "17":
|
||||
return "udp"
|
||||
default:
|
||||
return protocol
|
||||
}
|
||||
}
|
||||
|
||||
func loadLegacyIptablesSourcePort(value string) string {
|
||||
port := ""
|
||||
if strings.Contains(value, "dpt:") {
|
||||
port = strings.ReplaceAll(value, "dpt:", "")
|
||||
}
|
||||
if strings.Contains(value, "dpts:") {
|
||||
port = strings.ReplaceAll(value, "dpts:", "")
|
||||
}
|
||||
return strings.ReplaceAll(port, ":", "-")
|
||||
}
|
||||
|
||||
func listLegacyFirewalldForwarding() ([]legacyFirewalldForward, error) {
|
||||
stdout, err := cmd.NewCommandMgr().RunWithStdout(
|
||||
"firewall-cmd", "--permanent", "--zone=public", "--list-forward-ports",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list legacy firewalld forwarding rules: %w", err)
|
||||
}
|
||||
rules, failures := parseLegacyFirewalldForwarding(stdout)
|
||||
for _, failure := range failures {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
|
||||
}
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func parseLegacyFirewalldForwarding(stdout string) ([]legacyFirewalldForward, []legacyFirewalldForwardFailure) {
|
||||
result := make([]legacyFirewalldForward, 0)
|
||||
failures := make([]legacyFirewalldForwardFailure, 0)
|
||||
for _, spec := range strings.Fields(stdout) {
|
||||
item, err := parseLegacyFirewalldForward(spec)
|
||||
if err != nil {
|
||||
failures = append(failures, legacyFirewalldForwardFailure{spec: spec, err: err})
|
||||
continue
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result, failures
|
||||
}
|
||||
|
||||
func parseLegacyFirewalldForward(spec string) (legacyFirewalldForward, error) {
|
||||
if !strings.HasPrefix(spec, "port=") {
|
||||
return legacyFirewalldForward{}, errors.New("missing port field")
|
||||
}
|
||||
port, rest, ok := strings.Cut(strings.TrimPrefix(spec, "port="), ":proto=")
|
||||
if !ok {
|
||||
return legacyFirewalldForward{}, errors.New("missing protocol field")
|
||||
}
|
||||
protocol, rest, ok := strings.Cut(rest, ":toport=")
|
||||
if !ok {
|
||||
return legacyFirewalldForward{}, errors.New("missing target port field")
|
||||
}
|
||||
targetPort, targetIP, ok := strings.Cut(rest, ":toaddr=")
|
||||
if !ok {
|
||||
return legacyFirewalldForward{}, errors.New("missing target address field")
|
||||
}
|
||||
if targetIP == "" {
|
||||
targetIP = "127.0.0.1"
|
||||
}
|
||||
return legacyFirewalldForward{
|
||||
rule: forwarding.Rule{
|
||||
Family: forwarding.FamilyIPv4, Protocol: protocol, Port: port,
|
||||
TargetIP: targetIP, TargetPort: targetPort,
|
||||
},
|
||||
spec: spec,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func cleanupLegacyFirewalldForwarding(rules []legacyFirewalldForward) error {
|
||||
manager := cmd.NewCommandMgr()
|
||||
for _, item := range rules {
|
||||
if err := manager.Run(
|
||||
"firewall-cmd", "--permanent", "--zone=public", "--remove-forward-port="+item.spec,
|
||||
); err != nil {
|
||||
return fmt.Errorf("remove legacy firewalld forwarding rule %q: %w", item.spec, err)
|
||||
}
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := manager.Run("firewall-cmd", "--reload"); err != nil {
|
||||
return fmt.Errorf("reload firewalld after forwarding transfer: %w", err)
|
||||
}
|
||||
return restartDockerAfterFirewalldReload(cmd.Which, controller.CheckActive, controller.HandleRestart)
|
||||
}
|
||||
|
||||
func restartDockerAfterFirewalldReload(
|
||||
which func(string) bool,
|
||||
checkActive func(string) (bool, error),
|
||||
restart func(string) error,
|
||||
) error {
|
||||
const service = "docker"
|
||||
if !which(service) {
|
||||
return nil
|
||||
}
|
||||
active, err := checkActive(service)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check Docker status after reloading firewalld: %w", err)
|
||||
}
|
||||
if !active {
|
||||
return nil
|
||||
}
|
||||
if err := restart(service); err != nil {
|
||||
return fmt.Errorf("restart Docker after reloading firewalld: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/google/uuid"
|
||||
filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld"
|
||||
filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const hostFirewallTransferMigrationID = "host-firewall-transfer"
|
||||
|
||||
var errUnsupportedLegacyHostFirewallRule = errors.New("unsupported legacy host firewall rule")
|
||||
|
||||
type legacyHostFirewallRecord struct {
|
||||
@@ -37,126 +33,225 @@ type legacyHostFirewallRecord struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
func TransferHostFirewall(ctx context.Context, provider string) error {
|
||||
if global.DB == nil {
|
||||
return errors.New("host firewall transfer database is required")
|
||||
}
|
||||
return transferHostFirewall(ctx, global.DB, filter.Provider(strings.ToLower(strings.TrimSpace(provider))))
|
||||
type legacyFirewallRuleDescription struct {
|
||||
UUID string
|
||||
Provider string
|
||||
ScopeKey string
|
||||
Location string
|
||||
Family string
|
||||
NativeKind string
|
||||
Protocol string
|
||||
SourceAddress string
|
||||
SourcePort string
|
||||
DestinationAddress string
|
||||
DestinationPort string
|
||||
Interface string
|
||||
ConnectionStates string
|
||||
CompatibilityError string
|
||||
Action string
|
||||
Priority *int
|
||||
Description string
|
||||
Owner string
|
||||
}
|
||||
|
||||
func TransferLegacyHostFirewallRuleOwnership(ctx context.Context, provider string, transfer func(context.Context) error) error {
|
||||
if global.DB == nil {
|
||||
return errors.New("host firewall transfer database is required")
|
||||
func MigrateHostFirewallDescriptions(db *gorm.DB) error {
|
||||
providers := []filter.Provider{filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW}
|
||||
descriptions := make(map[string][]string)
|
||||
add := func(id, description string) {
|
||||
if description != "" && !slices.Contains(descriptions[id], description) {
|
||||
descriptions[id] = append(descriptions[id], description)
|
||||
}
|
||||
}
|
||||
return transferLegacyHostFirewallRuleOwnership(
|
||||
ctx,
|
||||
global.DB,
|
||||
filter.Provider(strings.ToLower(strings.TrimSpace(provider))),
|
||||
transfer,
|
||||
)
|
||||
}
|
||||
|
||||
func transferLegacyHostFirewallRuleOwnership(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
provider filter.Provider,
|
||||
transfer func(context.Context) error,
|
||||
) error {
|
||||
if !legacyHostFirewallOwnershipProvider(provider) {
|
||||
return nil
|
||||
addRules := func(rules []filter.FirewallRule, description string) bool {
|
||||
matched := false
|
||||
for _, rule := range rules {
|
||||
var err error
|
||||
switch rule.Scope.Provider {
|
||||
case filter.ProviderFirewalld:
|
||||
rule, err = (&filterfirewalld.Adapter{}).PrepareRule(rule)
|
||||
case filter.ProviderUFW:
|
||||
rule, err = (&filterufw.Adapter{}).PrepareRule(rule)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
id, err := filter.DescriptionID(filter.ObservedRule{Rule: rule, ParseStatus: filter.ParseStatusSupported})
|
||||
if err == nil {
|
||||
add(id, description)
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
if db == nil {
|
||||
return errors.New("host firewall transfer database is required")
|
||||
}
|
||||
completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID)
|
||||
if err != nil || completed {
|
||||
return err
|
||||
}
|
||||
if transfer == nil {
|
||||
return errors.New("legacy host firewall ownership transfer is required")
|
||||
}
|
||||
if err := transfer(ctx); err != nil {
|
||||
return fmt.Errorf("transfer legacy host firewall rule ownership: %w", err)
|
||||
}
|
||||
return markMigrationRecord(db, hostFirewallTransferMigrationID)
|
||||
}
|
||||
|
||||
func legacyHostFirewallOwnershipProvider(provider filter.Provider) bool {
|
||||
return provider == filter.ProviderIptables || provider == filter.ProviderUFW
|
||||
}
|
||||
|
||||
func transferHostFirewall(ctx context.Context, db *gorm.DB, provider filter.Provider) error {
|
||||
if db == nil {
|
||||
return errors.New("host firewall transfer database is required")
|
||||
}
|
||||
completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID)
|
||||
if err != nil || completed {
|
||||
return err
|
||||
}
|
||||
if !isLegacyHostFirewallProvider(provider) {
|
||||
return fmt.Errorf("unsupported legacy host firewall provider %q", provider)
|
||||
}
|
||||
|
||||
models := make([]model.FirewallRule, 0)
|
||||
if db.Migrator().HasTable("firewalls") {
|
||||
var records []legacyHostFirewallRecord
|
||||
if err := db.WithContext(ctx).Table("firewalls").Order("id ASC").Find(&records).Error; err != nil {
|
||||
return fmt.Errorf("load legacy host firewall records: %w", err)
|
||||
}
|
||||
models = convertLegacyHostFirewallRecords(records, provider)
|
||||
}
|
||||
|
||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := importLegacyHostFirewallRules(tx, models); err != nil {
|
||||
if err := db.Table("firewalls").Order("id ASC").Find(&records).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if legacyHostFirewallOwnershipProvider(provider) {
|
||||
return nil
|
||||
for _, record := range records {
|
||||
if record.Description == "" {
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, provider := range providers {
|
||||
rules, err := legacyHostFirewallRules(record, provider)
|
||||
if err == nil && addRules(rules, record.Description) {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
add(fmt.Sprintf("firewall:legacy:firewalls:%d", record.ID), record.Description)
|
||||
}
|
||||
}
|
||||
return markMigrationRecord(tx, hostFirewallTransferMigrationID)
|
||||
})
|
||||
}
|
||||
|
||||
func isLegacyHostFirewallProvider(provider filter.Provider) bool {
|
||||
switch provider {
|
||||
case filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if db.Migrator().HasTable("firewall_rules") {
|
||||
var records []legacyFirewallRuleDescription
|
||||
if err := db.Table("firewall_rules").Order("uuid ASC").Find(&records).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, record := range records {
|
||||
if record.Description == "" {
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, provider := range providers {
|
||||
rules, err := legacyFirewallDescriptionRules(record, provider)
|
||||
if err == nil && addRules(rules, record.Description) {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
add("firewall:legacy:firewall_rules:"+record.UUID, record.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(descriptions) == 0 {
|
||||
return nil
|
||||
}
|
||||
var existing []model.CommonDescription
|
||||
if err := db.Where("type = ?", "firewall").Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
byID := make(map[string]model.CommonDescription, len(existing))
|
||||
for _, description := range existing {
|
||||
byID[description.ID] = description
|
||||
}
|
||||
ids := make([]string, 0, len(descriptions))
|
||||
for id := range descriptions {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
for _, id := range ids {
|
||||
value := strings.Join(descriptions[id], "\n")
|
||||
if current, ok := byID[id]; ok {
|
||||
if current.Description == value {
|
||||
continue
|
||||
}
|
||||
if current.Description == "" {
|
||||
if err := db.Model(&model.CommonDescription{}).Where("id = ?", id).Update("description", value).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
id = "firewall:legacy:description:" + strings.TrimPrefix(id, "firewall:")
|
||||
if _, ok := byID[id]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
record := model.CommonDescription{ID: id, Type: "firewall", Description: value}
|
||||
if strings.HasPrefix(id, "firewall:legacy:") {
|
||||
record.DetailType = "legacy"
|
||||
}
|
||||
if err := db.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertLegacyHostFirewallRecords(records []legacyHostFirewallRecord, provider filter.Provider) []model.FirewallRule {
|
||||
converted := make([]model.FirewallRule, 0, len(records))
|
||||
byIdentity := make(map[string]int)
|
||||
for _, record := range records {
|
||||
rules, err := legacyHostFirewallRules(record, provider)
|
||||
func legacyFirewallDescriptionRules(record legacyFirewallRuleDescription, provider filter.Provider) ([]filter.FirewallRule, error) {
|
||||
if record.CompatibilityError != "" {
|
||||
return nil, errUnsupportedLegacyHostFirewallRule
|
||||
}
|
||||
source := filter.Provider(strings.ToLower(strings.TrimSpace(record.Provider)))
|
||||
scopeParts := strings.Split(record.ScopeKey, ":")
|
||||
if source == "" && len(scopeParts) > 1 {
|
||||
source = filter.Provider(scopeParts[0])
|
||||
}
|
||||
if source != "" && source != provider {
|
||||
return nil, nil
|
||||
}
|
||||
base := filter.FirewallRule{
|
||||
Protocol: record.Protocol, SourceAddress: record.SourceAddress, SourcePort: record.SourcePort,
|
||||
DestinationAddress: record.DestinationAddress, DestinationPort: record.DestinationPort,
|
||||
Interface: record.Interface, Action: filter.Action(record.Action),
|
||||
}
|
||||
if record.ConnectionStates != "" {
|
||||
base.ConnectionStates = strings.Split(record.ConnectionStates, ",")
|
||||
}
|
||||
if source != "" {
|
||||
base.NativeKind = filter.NativeKind(record.NativeKind)
|
||||
}
|
||||
switch base.NativeKind {
|
||||
case filter.NativeKindOpaque, filter.NativeKindZoneService, filter.NativeKindUFWApplication:
|
||||
return nil, errUnsupportedLegacyHostFirewallRule
|
||||
}
|
||||
if provider != filter.ProviderUFW && strings.EqualFold(base.Protocol, "all") && base.SourcePort == "" && base.DestinationPort != "" {
|
||||
base.Protocol = "tcp/udp"
|
||||
}
|
||||
if provider == filter.ProviderFirewalld {
|
||||
base.Priority = record.Priority
|
||||
}
|
||||
families := []filter.Family{filter.Family(record.Family)}
|
||||
if families[0] == "" {
|
||||
families[0] = legacyRuleFamily(base.SourceAddress, base.DestinationAddress)
|
||||
}
|
||||
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
|
||||
if base.SourceAddress != "" || base.DestinationAddress != "" || base.Protocol == "icmpv6" {
|
||||
families = []filter.Family{legacyRuleFamily(base.SourceAddress, base.DestinationAddress)}
|
||||
if base.Protocol == "icmpv6" {
|
||||
families[0] = filter.FamilyIPv6
|
||||
}
|
||||
} else {
|
||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
}
|
||||
}
|
||||
var result []filter.FirewallRule
|
||||
for _, family := range families {
|
||||
rule := base
|
||||
rule.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
|
||||
switch provider {
|
||||
case filter.ProviderIptables, filter.ProviderNftables:
|
||||
rule.Scope.Table, rule.Scope.Chain = "filter", filter.IptablesInputChain
|
||||
if source != "" {
|
||||
if record.Location != "" {
|
||||
rule.Scope.Chain = record.Location
|
||||
}
|
||||
if len(scopeParts) == 5 {
|
||||
rule.Scope.Table, rule.Scope.Chain = scopeParts[2], scopeParts[3]
|
||||
}
|
||||
}
|
||||
case filter.ProviderFirewalld:
|
||||
rule.Scope.Zone = filter.FirewalldInputZone
|
||||
if source != "" && record.Location != "" {
|
||||
rule.Scope.Zone = record.Location
|
||||
}
|
||||
case filter.ProviderUFW:
|
||||
rule.Scope.Chain = filter.UFWInputChain
|
||||
}
|
||||
expanded, err := filter.ExpandAtomicRules(rule)
|
||||
if err != nil {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err)
|
||||
}
|
||||
continue
|
||||
return nil, err
|
||||
}
|
||||
for _, rule := range rules {
|
||||
item, err := hostFirewallRuleModel(rule)
|
||||
if err != nil {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Warnf("skip legacy host firewall record %d during transfer: %v", record.ID, err)
|
||||
}
|
||||
continue
|
||||
result = append(result, expanded...)
|
||||
if source == "" && (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && strings.HasPrefix(record.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) {
|
||||
for _, item := range expanded {
|
||||
item.Scope.Chain = filter.BasicBeforeChain
|
||||
result = append(result, item)
|
||||
}
|
||||
identity := hostFirewallPolicyKey(item)
|
||||
if index, exists := byIdentity[identity]; exists {
|
||||
if item.Description != "" {
|
||||
converted[index].Description = item.Description
|
||||
}
|
||||
continue
|
||||
}
|
||||
byIdentity[identity] = len(converted)
|
||||
converted = append(converted, item)
|
||||
}
|
||||
}
|
||||
return converted
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Provider) ([]filter.FirewallRule, error) {
|
||||
@@ -196,7 +291,7 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr
|
||||
switch provider {
|
||||
case filter.ProviderIptables:
|
||||
rule.Scope = filter.Scope{
|
||||
Provider: provider, Family: filter.FamilyIPv4, Table: "filter",
|
||||
Provider: provider, Family: legacyRuleFamily(rule.SourceAddress, rule.DestinationAddress), Table: "filter",
|
||||
Chain: legacyIptablesChain(record), Direction: filter.DirectionInput,
|
||||
}
|
||||
rule.NativeKind = filter.NativeKindRule
|
||||
@@ -337,84 +432,3 @@ func legacyIPOrPrefix(value string) bool {
|
||||
_, err := netip.ParsePrefix(value)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) {
|
||||
normalized, err := filter.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return model.FirewallRule{}, err
|
||||
}
|
||||
switch normalized.NativeKind {
|
||||
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
|
||||
default:
|
||||
return model.FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
|
||||
}
|
||||
record := model.FirewallRule{
|
||||
Family: string(normalized.Scope.Family),
|
||||
Protocol: normalized.Protocol,
|
||||
SourceAddress: normalized.SourceAddress,
|
||||
SourcePort: normalized.SourcePort,
|
||||
DestinationAddress: normalized.DestinationAddress,
|
||||
DestinationPort: normalized.DestinationPort,
|
||||
Interface: normalized.Interface,
|
||||
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
|
||||
Action: string(normalized.Action),
|
||||
Description: normalized.Description,
|
||||
}
|
||||
if normalized.Scope.Provider == filter.ProviderFirewalld {
|
||||
record.Priority = normalized.Priority
|
||||
}
|
||||
record.UUID = uuid.NewString()
|
||||
record.Origin = constant.FirewallRuleOriginAdopted
|
||||
record.Owner = constant.FirewallRuleSourceUser
|
||||
record.Revision = 1
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func hostFirewallPolicyKey(rule model.FirewallRule) string {
|
||||
payload, _ := json.Marshal(struct {
|
||||
Family string `json:"family"`
|
||||
Protocol string `json:"protocol"`
|
||||
SourceAddress string `json:"sourceAddress,omitempty"`
|
||||
SourcePort string `json:"sourcePort,omitempty"`
|
||||
DestinationAddress string `json:"destinationAddress,omitempty"`
|
||||
DestinationPort string `json:"destinationPort,omitempty"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
ConnectionStates string `json:"connectionStates,omitempty"`
|
||||
Action string `json:"action"`
|
||||
}{
|
||||
Family: rule.Family, Protocol: rule.Protocol,
|
||||
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
||||
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
||||
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
|
||||
})
|
||||
sum := sha256.Sum256(payload)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) error {
|
||||
var existing []model.FirewallRule
|
||||
if err := tx.Find(&existing).Error; err != nil {
|
||||
return fmt.Errorf("load current host firewall rules: %w", err)
|
||||
}
|
||||
byIdentity := make(map[string]model.FirewallRule, len(existing))
|
||||
for _, item := range existing {
|
||||
byIdentity[hostFirewallPolicyKey(item)] = item
|
||||
}
|
||||
for _, item := range rules {
|
||||
identity := hostFirewallPolicyKey(item)
|
||||
if current, exists := byIdentity[identity]; exists {
|
||||
if current.Description == "" && item.Description != "" {
|
||||
if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID).
|
||||
Update("description", item.Description).Error; err != nil {
|
||||
return fmt.Errorf("restore legacy host firewall description: %w", err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
return fmt.Errorf("import legacy host firewall rule: %w", err)
|
||||
}
|
||||
byIdentity[identity] = item
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ func Init() {
|
||||
if err := validator.RegisterValidation("password", checkPasswordPattern); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -55,3 +61,12 @@ func checkPasswordPattern(fl validator.FieldLevel) bool {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
|
||||
baRouter.GET("/network", baseApi.ListNetwork)
|
||||
baRouter.POST("/network/del", baseApi.DeleteNetwork)
|
||||
baRouter.POST("/network/clean", baseApi.CleanNetworks)
|
||||
baRouter.POST("/network/search", baseApi.SearchNetwork)
|
||||
baRouter.POST("/network", baseApi.CreateNetwork)
|
||||
baRouter.GET("/volume", baseApi.ListVolume)
|
||||
|
||||
@@ -28,6 +28,8 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
|
||||
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
|
||||
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend)
|
||||
hostRouter.POST("/firewall/family/operate", baseApi.OperateFirewallFamily)
|
||||
hostRouter.POST("/firewall/settings/ipv6", baseApi.OperateFirewallIPv6)
|
||||
hostRouter.POST("/firewall/settings/whitelist", baseApi.CreateFirewallPortWhitelist)
|
||||
hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist)
|
||||
hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist)
|
||||
@@ -36,13 +38,10 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules)
|
||||
hostRouter.POST("/firewall/forward/enable", baseApi.EnableForwarding)
|
||||
hostRouter.POST("/firewall/rules/search", baseApi.SearchFirewallRules)
|
||||
hostRouter.GET("/firewall/rules/backups", baseApi.ListFirewallRuleBackups)
|
||||
hostRouter.POST("/firewall/rules/reset", baseApi.ResetFirewallRules)
|
||||
hostRouter.POST("/firewall/rules/native/detail", baseApi.LoadFirewallNativeDetail)
|
||||
hostRouter.POST("/firewall/rules/adopt", baseApi.AdoptFirewallRule)
|
||||
hostRouter.POST("/firewall/rules", baseApi.CreateFirewallRules)
|
||||
hostRouter.POST("/firewall/rules/sync/preview", baseApi.PreviewFirewallRuleSync)
|
||||
hostRouter.GET("/firewall/rules/sync/task", baseApi.LoadFirewallRuleSyncTask)
|
||||
hostRouter.POST("/firewall/rules/sync", baseApi.SyncFirewallRules)
|
||||
hostRouter.POST("/firewall/rules/update", baseApi.UpdateFirewallRule)
|
||||
hostRouter.POST("/firewall/rules/delete", baseApi.DeleteFirewallRules)
|
||||
hostRouter.POST("/firewall/rules/reorder", baseApi.ReorderFirewallRule)
|
||||
@@ -50,7 +49,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
hostRouter.POST("/firewall/filter/operate", baseApi.OperateFilterChain)
|
||||
hostRouter.GET("/firewall/docker/ports", baseApi.ListDockerPortGuard)
|
||||
hostRouter.GET("/firewall/docker/endpoints", baseApi.ListDockerPublishedPorts)
|
||||
hostRouter.POST("/firewall/docker/sync", baseApi.SyncDockerPortGuard)
|
||||
hostRouter.POST("/firewall/docker/operate", baseApi.OperateDockerPortGuard)
|
||||
hostRouter.POST("/firewall/docker/policies/batch", baseApi.UpsertDockerPortGuardPolicies)
|
||||
hostRouter.POST("/firewall/docker/policies/delete/batch", baseApi.DeleteDockerPortGuardPolicies)
|
||||
|
||||
@@ -251,6 +251,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
|
||||
cmdItem.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
cmdItem.Cancel = func() error {
|
||||
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
|
||||
return cmdItem.Process.Kill()
|
||||
}
|
||||
cmds = append(cmds, cmdItem)
|
||||
}
|
||||
return cmds
|
||||
@@ -335,6 +339,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
if newContext != nil {
|
||||
cmd.Cancel = func() error {
|
||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
customWriter := &CustomWriter{taskItem: c.taskItem}
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -398,6 +408,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
if newContext != nil && newContext.Err() != nil {
|
||||
return "", c.pipeResultErr(newContext, err)
|
||||
}
|
||||
if err != nil {
|
||||
return handleErr(&stdout, &stderr, c.IgnoreExist1, err)
|
||||
}
|
||||
|
||||
@@ -1,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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package docker_guard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -13,8 +14,8 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
)
|
||||
|
||||
func ReadDNATRules(backend, family string) DNATRules {
|
||||
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
|
||||
func ReadDNATRules(ctx context.Context, backend, family string) DNATRules {
|
||||
manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
tableFamily := "ip"
|
||||
if family == constant.FirewallFamilyIPv6 {
|
||||
@@ -45,8 +46,8 @@ func ReadDNATRules(backend, family string) DNATRules {
|
||||
return DNATRules{Output: output, Inspected: err == nil}
|
||||
}
|
||||
|
||||
func ReadProxyEndpoints() ProxyEndpoints {
|
||||
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
|
||||
func ReadProxyEndpoints(ctx context.Context) ProxyEndpoints {
|
||||
manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
|
||||
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
|
||||
if err != nil {
|
||||
return ProxyEndpoints{}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package docker_guard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"sort"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
)
|
||||
@@ -22,26 +24,27 @@ type Runner interface {
|
||||
Exists(executable string) bool
|
||||
}
|
||||
|
||||
type commandRunner struct{}
|
||||
type commandRunner struct{ ctx context.Context }
|
||||
|
||||
func (commandRunner) Run(executable string, args ...string) (string, error) {
|
||||
func (r commandRunner) Run(executable string, args ...string) (string, error) {
|
||||
executable = dockerGuardExecutable(executable)
|
||||
manager := cmd.NewCommandMgr(cmd.WithTimeout(60 * time.Second))
|
||||
manager := cmd.NewCommandMgr(cmd.WithContext(r.ctx), cmd.WithTimeout(60*time.Second))
|
||||
stdout, err := manager.RunWithOptionalSudoAndStdout(executable, args...)
|
||||
err = errors.Join(err, r.ctx.Err())
|
||||
if err != nil {
|
||||
return stdout, fmt.Errorf("command=%s %s failed: %w", executable, strings.Join(args, " "), err)
|
||||
}
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
func (commandRunner) RunInput(executable, input string, args ...string) (string, error) {
|
||||
func (r commandRunner) RunInput(executable, input string, args ...string) (string, error) {
|
||||
executable = dockerGuardExecutable(executable)
|
||||
if executable == "nft" {
|
||||
return "", nftables_helper.RunScript(input)
|
||||
return "", errors.Join(nftables_helper.RunScriptContext(r.ctx, input), r.ctx.Err())
|
||||
}
|
||||
manager := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input)))
|
||||
manager := cmd.NewCommandMgr(cmd.WithContext(r.ctx), cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input)))
|
||||
stdout, err := manager.RunWithOptionalSudoAndStdout(executable, args...)
|
||||
return stdout, firewallutil.WrapBatchCommandError(executable+" "+strings.Join(args, " "), input, err)
|
||||
return stdout, errors.Join(firewallutil.WrapBatchCommandError(executable+" "+strings.Join(args, " "), input, err), r.ctx.Err())
|
||||
}
|
||||
|
||||
func (commandRunner) Exists(executable string) bool {
|
||||
@@ -74,9 +77,12 @@ type Iptables struct {
|
||||
|
||||
var mutationMu sync.Mutex
|
||||
|
||||
func NewIptables() *Iptables { return &Iptables{runner: commandRunner{}} }
|
||||
func NewIptables(ctx context.Context) *Iptables { return &Iptables{runner: commandRunner{ctx: ctx}} }
|
||||
|
||||
func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) error {
|
||||
func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory, families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := CheckIPv4Forwarding(); err != nil {
|
||||
@@ -88,7 +94,7 @@ func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) erro
|
||||
if err := m.ensureFamily("iptables", true); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.runner.Exists("ip6tables") {
|
||||
if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") {
|
||||
available, err := m.chainExists("ip6tables", DockerChain)
|
||||
if err != nil {
|
||||
return &FamilyError{Family: FamilyIPv6, Err: fmt.Errorf("inspect %s chain: %w", DockerChain, err)}
|
||||
@@ -102,16 +108,19 @@ func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) erro
|
||||
}
|
||||
}
|
||||
}
|
||||
return m.rebuildLocked(policies, inventory)
|
||||
return m.rebuildLocked(policies, inventory, families...)
|
||||
}
|
||||
|
||||
func (m *Iptables) Bind() error {
|
||||
func (m *Iptables) Bind(families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := m.bindExistingFamily("iptables", true); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.runner.Exists("ip6tables") {
|
||||
if slices.Contains(families, FamilyIPv6) && m.runner.Exists("ip6tables") {
|
||||
if err := m.bindExistingFamily("ip6tables", false); err != nil {
|
||||
return &FamilyError{Family: FamilyIPv6, Err: err}
|
||||
}
|
||||
@@ -126,13 +135,16 @@ func (m *Iptables) ReplacePolicies(policies []Policy, inventory PolicyInventory)
|
||||
}
|
||||
|
||||
func (m *Iptables) ListPolicies() (PolicyInventory, error) {
|
||||
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
|
||||
inventory := PolicyInventory{Policies: make([]Policy, 0), RuleOrders: make(map[string][]int64)}
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
executable := executableForFamily(family)
|
||||
if executable == "" || !m.runner.Exists(executable) {
|
||||
continue
|
||||
}
|
||||
exists, err := m.chainExists(executable, Chain)
|
||||
if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)}
|
||||
}
|
||||
@@ -148,22 +160,21 @@ func (m *Iptables) ListPolicies() (PolicyInventory, error) {
|
||||
return PolicyInventory{}, &FamilyError{Family: family, Err: err}
|
||||
}
|
||||
inventory.Policies = append(inventory.Policies, parsed.Policies...)
|
||||
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...)
|
||||
for key, orders := range parsed.ManagedRuleOrders {
|
||||
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
|
||||
for key, orders := range parsed.RuleOrders {
|
||||
inventory.RuleOrders[key] = append([]int64(nil), orders...)
|
||||
}
|
||||
}
|
||||
return inventory, nil
|
||||
}
|
||||
|
||||
func (m *Iptables) Unbind() error {
|
||||
func (m *Iptables) Unbind(families ...string) error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := m.unbindFamily("iptables"); err != nil {
|
||||
return err
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
if m.runner.Exists("ip6tables") {
|
||||
if err := m.unbindFamily("ip6tables"); err != nil {
|
||||
for _, family := range families {
|
||||
if err := m.unbindFamily(executableForFamily(family)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -315,13 +326,19 @@ func (m *Iptables) restoreLifecycle(executable string, rules [][]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory, families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
for _, family := range families {
|
||||
executable := executableForFamily(family)
|
||||
if executable == "" || !m.runner.Exists(executable) {
|
||||
continue
|
||||
}
|
||||
exists, err := m.chainExists(executable, Chain)
|
||||
if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) && !slices.ContainsFunc(policies, func(policy Policy) bool { return policy.Family == family }) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)}
|
||||
}
|
||||
@@ -346,41 +363,34 @@ func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) e
|
||||
return nil
|
||||
}
|
||||
|
||||
type orderedIPTablesRule struct {
|
||||
order int64
|
||||
index int
|
||||
rules [][]string
|
||||
}
|
||||
|
||||
func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInventory) [][]string {
|
||||
segments := make([]orderedIPTablesRule, 0, len(policies)+len(inventory.ReadOnly))
|
||||
segments := make([]orderedPolicyRule, 0, len(policies))
|
||||
maxOrder := int64(0)
|
||||
index := 0
|
||||
for _, orders := range inventory.ManagedRuleOrders {
|
||||
for _, orders := range inventory.RuleOrders {
|
||||
for _, order := range orders {
|
||||
if order > maxOrder {
|
||||
maxOrder = order
|
||||
}
|
||||
maxOrder = max(maxOrder, order)
|
||||
}
|
||||
}
|
||||
for _, item := range inventory.ReadOnly {
|
||||
for _, native := range item.NativeRules {
|
||||
if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain {
|
||||
continue
|
||||
}
|
||||
segments = append(segments, orderedIPTablesRule{order: native.Order, index: index, rules: [][]string{append([]string(nil), native.Tokens...)}})
|
||||
index++
|
||||
if native.Order > maxOrder {
|
||||
maxOrder = native.Order
|
||||
}
|
||||
for _, policy := range policies {
|
||||
for _, rule := range policy.NativeRules {
|
||||
maxOrder = max(maxOrder, rule.Order)
|
||||
}
|
||||
}
|
||||
for _, policy := range policies {
|
||||
if policy.Family != family {
|
||||
continue
|
||||
}
|
||||
if len(policy.NativeRules) > 0 {
|
||||
for _, native := range policy.NativeRules {
|
||||
if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain {
|
||||
continue
|
||||
}
|
||||
segments = append(segments, orderedPolicyRule{order: native.Order, rule: append([]string(nil), native.Tokens...)})
|
||||
}
|
||||
continue
|
||||
}
|
||||
compiled := compilePolicy(policy)
|
||||
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
|
||||
orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID]
|
||||
for ruleIndex, rule := range compiled {
|
||||
order := int64(0)
|
||||
if ruleIndex < len(orders) {
|
||||
@@ -389,21 +399,10 @@ func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInve
|
||||
maxOrder++
|
||||
order = maxOrder
|
||||
}
|
||||
segments = append(segments, orderedIPTablesRule{order: order, index: index, rules: [][]string{rule}})
|
||||
index++
|
||||
segments = append(segments, orderedPolicyRule{order: order, rule: rule})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(segments, func(left, right int) bool {
|
||||
if segments[left].order == segments[right].order {
|
||||
return segments[left].index < segments[right].index
|
||||
}
|
||||
return segments[left].order < segments[right].order
|
||||
})
|
||||
rules := make([][]string, 0)
|
||||
for _, segment := range segments {
|
||||
rules = append(rules, segment.rules...)
|
||||
}
|
||||
return rules
|
||||
return sortPolicyRules(segments)
|
||||
}
|
||||
|
||||
func buildRestoreScript(rules [][]string) (string, error) {
|
||||
@@ -436,14 +435,20 @@ func compilePolicy(policy Policy) [][]string {
|
||||
}
|
||||
base = append(base, "--ctorigdstport", strconv.Itoa(int(policy.HostPort)))
|
||||
comment := "1panel-docker:" + policy.UUID
|
||||
if policy.Mode == ModeAll {
|
||||
return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", "DROP")}
|
||||
if policy.Mode == ModeAll || policy.Mode == ModeAcceptAll {
|
||||
target := "DROP"
|
||||
if policy.Mode == ModeAcceptAll {
|
||||
target = "ACCEPT"
|
||||
}
|
||||
return [][]string{append(append([]string{}, base...), "-m", "comment", "--comment", comment, "-j", target)}
|
||||
}
|
||||
target := "DROP"
|
||||
capacity := len(policy.Sources)
|
||||
if policy.Mode == ModeAllow {
|
||||
target = "RETURN"
|
||||
capacity++
|
||||
} else if policy.Mode == ModeAcceptSources {
|
||||
target = "ACCEPT"
|
||||
}
|
||||
rules := make([][]string, 0, capacity)
|
||||
for _, source := range policy.Sources {
|
||||
@@ -491,7 +496,11 @@ func chainDeclared(output, chain string) bool {
|
||||
|
||||
func (m *Iptables) run(executable string, args ...string) (string, error) {
|
||||
commandArgs := append([]string{"-w", "-t", "filter"}, args...)
|
||||
return m.runner.Run(executable, commandArgs...)
|
||||
output, err := m.runner.Run(executable, commandArgs...)
|
||||
if executable == "ip6tables" && err != nil && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
|
||||
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
func executableForFamily(family string) string {
|
||||
@@ -533,3 +542,20 @@ func isWildcardHost(family, hostIP string) bool {
|
||||
return (family == FamilyIPv4 && (hostIP == "" || hostIP == "0.0.0.0")) ||
|
||||
(family == FamilyIPv6 && (hostIP == "" || hostIP == "::"))
|
||||
}
|
||||
|
||||
func (m *Iptables) OperateFamily(family string, initialize bool) error {
|
||||
if family != FamilyIPv4 && family != FamilyIPv6 {
|
||||
return fmt.Errorf("unsupported Docker firewall family %q", family)
|
||||
}
|
||||
if family == FamilyIPv4 {
|
||||
if err := CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if initialize {
|
||||
return m.ensureFamily(executableForFamily(family), true)
|
||||
}
|
||||
return m.bindExistingFamily(executableForFamily(family), true)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package docker_guard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"sort"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
)
|
||||
|
||||
@@ -23,9 +25,12 @@ type Nftables struct {
|
||||
runner Runner
|
||||
}
|
||||
|
||||
func NewNftables() *Nftables { return &Nftables{runner: commandRunner{}} }
|
||||
func NewNftables(ctx context.Context) *Nftables { return &Nftables{runner: commandRunner{ctx: ctx}} }
|
||||
|
||||
func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) error {
|
||||
func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory, families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if !m.runner.Exists("nft") {
|
||||
@@ -34,26 +39,33 @@ func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) erro
|
||||
if err := CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.checkForwardPolicy(); err != nil {
|
||||
if err := m.checkForwardPolicy(families...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.ensureFamily(FamilyIPv4, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.ensureFamily(FamilyIPv6, false); err != nil {
|
||||
return &FamilyError{Family: FamilyIPv6, Err: err}
|
||||
if slices.Contains(families, FamilyIPv6) {
|
||||
if err := m.ensureFamily(FamilyIPv6, false); err != nil {
|
||||
return &FamilyError{Family: FamilyIPv6, Err: err}
|
||||
}
|
||||
}
|
||||
return m.rebuildLocked(policies, inventory)
|
||||
return m.rebuildLocked(policies, inventory, families...)
|
||||
}
|
||||
|
||||
func (m *Nftables) Bind() error {
|
||||
func (m *Nftables) Bind(families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := m.bindExistingFamily(FamilyIPv4, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.bindExistingFamily(FamilyIPv6, false); err != nil {
|
||||
return &FamilyError{Family: FamilyIPv6, Err: err}
|
||||
if slices.Contains(families, FamilyIPv6) {
|
||||
if err := m.bindExistingFamily(FamilyIPv6, false); err != nil {
|
||||
return &FamilyError{Family: FamilyIPv6, Err: err}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -68,11 +80,11 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) {
|
||||
if !m.runner.Exists("nft") {
|
||||
return PolicyInventory{}, nil
|
||||
}
|
||||
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
|
||||
inventory := PolicyInventory{Policies: make([]Policy, 0), RuleOrders: make(map[string][]int64)}
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
tableFamily := nftTableFamily(family)
|
||||
output, err := nftables_helper.ReadChain(m.run, tableFamily, NftTable, NftChain)
|
||||
if errors.Is(err, nftables_helper.ErrChainNotFound) {
|
||||
if errors.Is(err, nftables_helper.ErrChainNotFound) || (family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable)) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
@@ -83,18 +95,20 @@ func (m *Nftables) ListPolicies() (PolicyInventory, error) {
|
||||
return PolicyInventory{}, &FamilyError{Family: family, Err: err}
|
||||
}
|
||||
inventory.Policies = append(inventory.Policies, parsed.Policies...)
|
||||
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...)
|
||||
for key, orders := range parsed.ManagedRuleOrders {
|
||||
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
|
||||
for key, orders := range parsed.RuleOrders {
|
||||
inventory.RuleOrders[key] = append([]int64(nil), orders...)
|
||||
}
|
||||
}
|
||||
return inventory, nil
|
||||
}
|
||||
|
||||
func (m *Nftables) Unbind() error {
|
||||
func (m *Nftables) Unbind(families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
for _, family := range families {
|
||||
if err := m.unbindFamily(family); err != nil {
|
||||
return &FamilyError{Family: family, Err: err}
|
||||
}
|
||||
@@ -106,12 +120,16 @@ func (m *Nftables) Cleanup() error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if !m.runner.Exists("nft") {
|
||||
return nil
|
||||
return errors.New("nft is not installed")
|
||||
}
|
||||
commands := make([][]string, 0, 2)
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
tableFamily := nftTableFamily(family)
|
||||
if !m.objectExists("table", tableFamily, NftTable) {
|
||||
_, exists, err := nftables_helper.ReadTable(m.run, tableFamily, NftTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
commands = append(commands, []string{"delete", "table", tableFamily, NftTable})
|
||||
@@ -160,7 +178,7 @@ func (m *Nftables) Status(family string) FamilyStatus {
|
||||
}
|
||||
}
|
||||
if !baseExists || !guardExists {
|
||||
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
|
||||
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing, Partial: baseExists || guardExists}
|
||||
}
|
||||
status := FamilyStatus{State: StatusNotEffective, Initialized: true}
|
||||
rules := baseRules.String()
|
||||
@@ -260,11 +278,14 @@ func (m *Nftables) ensureJump(family string) error {
|
||||
return m.runBatch(commands)
|
||||
}
|
||||
|
||||
func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
|
||||
func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory, families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
if !m.runner.Exists("nft") {
|
||||
return nil
|
||||
}
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
for _, family := range families {
|
||||
tableFamily := nftTableFamily(family)
|
||||
if !m.objectExists("chain", tableFamily, NftTable, NftChain) {
|
||||
continue
|
||||
@@ -284,44 +305,37 @@ func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) e
|
||||
return nil
|
||||
}
|
||||
|
||||
type orderedNftRule struct {
|
||||
order int64
|
||||
index int
|
||||
rules [][]string
|
||||
}
|
||||
|
||||
func orderedNftRules(family string, policies []Policy, inventory PolicyInventory) [][]string {
|
||||
tableFamily := nftTableFamily(family)
|
||||
segments := make([]orderedNftRule, 0, len(policies)+len(inventory.ReadOnly))
|
||||
segments := make([]orderedPolicyRule, 0, len(policies))
|
||||
maxOrder := int64(0)
|
||||
index := 0
|
||||
for _, orders := range inventory.ManagedRuleOrders {
|
||||
for _, orders := range inventory.RuleOrders {
|
||||
for _, order := range orders {
|
||||
if order > maxOrder {
|
||||
maxOrder = order
|
||||
}
|
||||
maxOrder = max(maxOrder, order)
|
||||
}
|
||||
}
|
||||
for _, item := range inventory.ReadOnly {
|
||||
for _, native := range item.NativeRules {
|
||||
if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" {
|
||||
continue
|
||||
}
|
||||
command := []string{"add", "rule", tableFamily, NftTable, NftChain}
|
||||
command = append(command, quoteNftTokens(native.Tokens)...)
|
||||
segments = append(segments, orderedNftRule{order: native.Order, index: index, rules: [][]string{command}})
|
||||
index++
|
||||
if native.Order > maxOrder {
|
||||
maxOrder = native.Order
|
||||
}
|
||||
for _, policy := range policies {
|
||||
for _, rule := range policy.NativeRules {
|
||||
maxOrder = max(maxOrder, rule.Order)
|
||||
}
|
||||
}
|
||||
for _, policy := range policies {
|
||||
if policy.Family != family {
|
||||
continue
|
||||
}
|
||||
if len(policy.NativeRules) > 0 {
|
||||
for _, native := range policy.NativeRules {
|
||||
if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" {
|
||||
continue
|
||||
}
|
||||
command := []string{"add", "rule", tableFamily, NftTable, NftChain}
|
||||
command = append(command, quoteNftTokens(native.Tokens)...)
|
||||
segments = append(segments, orderedPolicyRule{order: native.Order, rule: command})
|
||||
}
|
||||
continue
|
||||
}
|
||||
compiled := compileNftPolicy(policy)
|
||||
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
|
||||
orders := inventory.RuleOrders[policy.Family+"\x00"+policy.UUID]
|
||||
for ruleIndex, rule := range compiled {
|
||||
order := int64(0)
|
||||
if ruleIndex < len(orders) {
|
||||
@@ -330,27 +344,16 @@ func orderedNftRules(family string, policies []Policy, inventory PolicyInventory
|
||||
maxOrder++
|
||||
order = maxOrder
|
||||
}
|
||||
segments = append(segments, orderedNftRule{order: order, index: index, rules: [][]string{rule}})
|
||||
index++
|
||||
segments = append(segments, orderedPolicyRule{order: order, rule: rule})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(segments, func(left, right int) bool {
|
||||
if segments[left].order == segments[right].order {
|
||||
return segments[left].index < segments[right].index
|
||||
}
|
||||
return segments[left].order < segments[right].order
|
||||
})
|
||||
rules := make([][]string, 0)
|
||||
for _, segment := range segments {
|
||||
rules = append(rules, segment.rules...)
|
||||
}
|
||||
return rules
|
||||
return sortPolicyRules(segments)
|
||||
}
|
||||
|
||||
func quoteNftTokens(tokens []string) []string {
|
||||
quoted := make([]string, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
if strings.ContainsAny(token, " \t\\\"'") && !strings.HasPrefix(token, `"`) {
|
||||
for index, token := range tokens {
|
||||
if (index > 0 && tokens[index-1] == "comment" || strings.ContainsAny(token, " \t\\\"'")) && !strings.HasPrefix(token, `"`) {
|
||||
quoted = append(quoted, strconv.Quote(token))
|
||||
continue
|
||||
}
|
||||
@@ -367,15 +370,22 @@ func compileNftPolicy(policy Policy) [][]string {
|
||||
base = append(base, "ct", "original", addressKeyword, "daddr", policy.HostIP)
|
||||
}
|
||||
base = append(base, "ct", "original", "proto-dst", strconv.Itoa(int(policy.HostPort)))
|
||||
comment := strconv.Quote("1panel-docker:" + policy.UUID)
|
||||
if policy.Mode == ModeAll {
|
||||
return [][]string{append(append([]string{}, base...), "drop", "comment", comment)}
|
||||
marker := "1panel-docker:" + policy.UUID
|
||||
comment := strconv.Quote(marker)
|
||||
if policy.Mode == ModeAll || policy.Mode == ModeAcceptAll {
|
||||
target := "drop"
|
||||
if policy.Mode == ModeAcceptAll {
|
||||
target = "accept"
|
||||
}
|
||||
return [][]string{append(append([]string{}, base...), target, "comment", comment)}
|
||||
}
|
||||
target := "drop"
|
||||
capacity := len(policy.Sources)
|
||||
if policy.Mode == ModeAllow {
|
||||
target = "return"
|
||||
capacity++
|
||||
} else if policy.Mode == ModeAcceptSources {
|
||||
target = "accept"
|
||||
}
|
||||
rules := make([][]string, 0, capacity)
|
||||
for _, source := range policy.Sources {
|
||||
@@ -506,11 +516,14 @@ func nftHasFirstUniqueJump(output string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Nftables) checkForwardPolicy() error {
|
||||
func (m *Nftables) checkForwardPolicy(families ...string) error {
|
||||
for _, family := range []struct{ command, name string }{
|
||||
{"iptables", FamilyIPv4},
|
||||
{"ip6tables", FamilyIPv6},
|
||||
} {
|
||||
if len(families) > 0 && !slices.Contains(families, family.name) {
|
||||
continue
|
||||
}
|
||||
if !m.runner.Exists(family.command) {
|
||||
continue
|
||||
}
|
||||
@@ -526,11 +539,7 @@ func (m *Nftables) checkForwardPolicy() error {
|
||||
}
|
||||
found = true
|
||||
if fields[2] == "DROP" {
|
||||
label := "IPv4"
|
||||
if family.name == FamilyIPv6 {
|
||||
label = "IPv6"
|
||||
}
|
||||
return &FamilyError{Family: family.name, Err: buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": label}, nil)}
|
||||
return &FamilyError{Family: family.name, Err: buserr.New("ErrDockerForwardPolicyDrop")}
|
||||
}
|
||||
if fields[2] != "ACCEPT" {
|
||||
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
|
||||
@@ -542,3 +551,23 @@ func (m *Nftables) checkForwardPolicy() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Nftables) OperateFamily(family string, initialize bool) error {
|
||||
if family != FamilyIPv4 && family != FamilyIPv6 {
|
||||
return fmt.Errorf("unsupported Docker firewall family %q", family)
|
||||
}
|
||||
if family == FamilyIPv4 {
|
||||
if err := CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := m.checkForwardPolicy(family); err != nil {
|
||||
return err
|
||||
}
|
||||
if initialize {
|
||||
return m.ensureFamily(family, true)
|
||||
}
|
||||
return m.bindExistingFamily(family, true)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package docker_guard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -12,9 +15,7 @@ import (
|
||||
|
||||
type observedPolicy struct {
|
||||
policy Policy
|
||||
sequence int64
|
||||
nativeRules []NativeRule
|
||||
managedOrders []int64
|
||||
orders []int64
|
||||
dropAll bool
|
||||
droppedSource []string
|
||||
allowedSource []string
|
||||
@@ -22,6 +23,106 @@ type observedPolicy struct {
|
||||
acceptAll bool
|
||||
}
|
||||
|
||||
func ConvertPolicyBackend(policy Policy, sourceBackend, targetBackend string) (Policy, error) {
|
||||
compiled := make(map[string][][]string, 2)
|
||||
for _, backend := range []string{sourceBackend, targetBackend} {
|
||||
switch backend {
|
||||
case "iptables":
|
||||
compiled[backend] = compilePolicy(policy)
|
||||
case "nftables":
|
||||
compiled[backend] = compileNftPolicy(policy)
|
||||
for i := range compiled[backend] {
|
||||
compiled[backend][i] = compiled[backend][i][5:]
|
||||
}
|
||||
default:
|
||||
return Policy{}, fmt.Errorf("unsupported Docker firewall backend %q", backend)
|
||||
}
|
||||
}
|
||||
if len(policy.NativeRules) == 0 {
|
||||
return policy, nil
|
||||
}
|
||||
sourceRules, targetRules := compiled[sourceBackend], compiled[targetBackend]
|
||||
if len(policy.NativeRules) != len(sourceRules) {
|
||||
return Policy{}, fmt.Errorf("Docker policy %s contains native rules that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend)
|
||||
}
|
||||
byRule := make(map[string]int, len(sourceRules))
|
||||
for index, rule := range sourceRules {
|
||||
byRule[dockerPolicyRuleKey(rule, sourceBackend)] = index
|
||||
}
|
||||
converted := make([]NativeRule, 0, len(policy.NativeRules))
|
||||
for _, native := range policy.NativeRules {
|
||||
key := dockerPolicyRuleKey(native.Tokens, sourceBackend)
|
||||
index, exists := byRule[key]
|
||||
if !exists || native.Family != policy.Family {
|
||||
return Policy{}, fmt.Errorf("Docker policy %s contains native conditions that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend)
|
||||
}
|
||||
delete(byRule, key)
|
||||
tokens := append([]string(nil), targetRules[index]...)
|
||||
for i, token := range tokens {
|
||||
if unquoted, err := strconv.Unquote(token); err == nil {
|
||||
tokens[i] = unquoted
|
||||
}
|
||||
}
|
||||
converted = append(converted, NativeRule{Family: native.Family, Order: native.Order, Tokens: tokens})
|
||||
}
|
||||
policy.NativeRules = converted
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func dockerPolicyRuleKey(tokens []string, backend string) string {
|
||||
tokens = nativeRuleTokens(tokens)
|
||||
if backend == "iptables" {
|
||||
if len(tokens)%2 != 0 {
|
||||
return ""
|
||||
}
|
||||
protocol := ""
|
||||
if index := slices.Index(tokens, "-p"); index >= 0 && index+1 < len(tokens) {
|
||||
protocol = tokens[index+1]
|
||||
}
|
||||
parts := make([]string, 0, len(tokens)/2)
|
||||
for i := 0; i < len(tokens); i += 2 {
|
||||
option, value := tokens[i], tokens[i+1]
|
||||
if option == "-m" && value == protocol && (value == "tcp" || value == "udp") {
|
||||
continue
|
||||
}
|
||||
if option == "--ctorigdst" {
|
||||
value = normalizeObservedHost(value)
|
||||
}
|
||||
if option == "-s" {
|
||||
if address, err := netip.ParseAddr(value); err == nil {
|
||||
value = netip.PrefixFrom(address, address.BitLen()).String()
|
||||
} else if prefix, err := netip.ParsePrefix(value); err == nil {
|
||||
value = prefix.Masked().String()
|
||||
}
|
||||
}
|
||||
parts = append(parts, option+"\x00"+value)
|
||||
}
|
||||
sort.Strings(parts)
|
||||
return strings.Join(parts, "\x01")
|
||||
}
|
||||
parts := make([]string, 0, len(tokens))
|
||||
for i, token := range tokens {
|
||||
if token == "counter" {
|
||||
continue
|
||||
}
|
||||
if unquoted, err := strconv.Unquote(token); err == nil {
|
||||
token = unquoted
|
||||
}
|
||||
if i > 0 && tokens[i-1] == "daddr" {
|
||||
token = normalizeObservedHost(token)
|
||||
}
|
||||
if i > 0 && tokens[i-1] == "saddr" {
|
||||
if address, err := netip.ParseAddr(token); err == nil {
|
||||
token = netip.PrefixFrom(address, address.BitLen()).String()
|
||||
} else if prefix, err := netip.ParsePrefix(token); err == nil {
|
||||
token = prefix.Masked().String()
|
||||
}
|
||||
}
|
||||
parts = append(parts, token)
|
||||
}
|
||||
return strings.Join(parts, "\x00")
|
||||
}
|
||||
|
||||
func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
|
||||
groups := make(map[string]*observedPolicy)
|
||||
order := make([]string, 0)
|
||||
@@ -35,33 +136,26 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
|
||||
if err != nil {
|
||||
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
|
||||
}
|
||||
managed := strings.Contains(line, "1panel-docker:")
|
||||
if !managed && !hasAcceptAction(tokens) {
|
||||
continue
|
||||
}
|
||||
sequence++
|
||||
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
|
||||
if err != nil {
|
||||
return PolicyInventory{}, err
|
||||
}
|
||||
identity := fragment.UUID
|
||||
if action == "accept" {
|
||||
identity = action
|
||||
if action == "" || (fragment.HostPort == 0 && action == "return") {
|
||||
continue
|
||||
}
|
||||
key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
|
||||
key := strings.Join([]string{fragment.UUID, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
|
||||
group, exists := groups[key]
|
||||
if !exists {
|
||||
group = &observedPolicy{policy: fragment, sequence: sequence}
|
||||
group = &observedPolicy{policy: fragment}
|
||||
groups[key] = group
|
||||
order = append(order, key)
|
||||
}
|
||||
switch {
|
||||
case action == "accept" && source != "":
|
||||
group.acceptedSource = append(group.acceptedSource, source)
|
||||
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
|
||||
case action == "accept":
|
||||
group.acceptAll = true
|
||||
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
|
||||
case action == "return" && source != "":
|
||||
group.allowedSource = append(group.allowedSource, source)
|
||||
case action == "drop" && source != "":
|
||||
@@ -71,21 +165,19 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
|
||||
default:
|
||||
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
|
||||
}
|
||||
if action != "accept" {
|
||||
group.managedOrders = append(group.managedOrders, sequence)
|
||||
}
|
||||
group.policy.NativeRules = append(group.policy.NativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
|
||||
group.orders = append(group.orders, sequence)
|
||||
}
|
||||
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)}
|
||||
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), RuleOrders: make(map[string][]int64)}
|
||||
for _, key := range order {
|
||||
group := groups[key]
|
||||
if group.acceptAll || len(group.acceptedSource) > 0 {
|
||||
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
|
||||
inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{
|
||||
Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules,
|
||||
})
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case group.acceptAll:
|
||||
group.policy.Mode = ModeAcceptAll
|
||||
group.policy.Sources = []string{}
|
||||
case len(group.acceptedSource) > 0:
|
||||
group.policy.Mode = ModeAcceptSources
|
||||
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
|
||||
case len(group.allowedSource) > 0:
|
||||
group.policy.Mode = ModeAllow
|
||||
group.policy.Sources = uniqueSortedStrings(group.allowedSource)
|
||||
@@ -97,8 +189,16 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
|
||||
default:
|
||||
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
|
||||
}
|
||||
if _, err := uuid.Parse(group.policy.UUID); err != nil {
|
||||
rules := make([][]string, 0, len(group.policy.NativeRules))
|
||||
for _, rule := range group.policy.NativeRules {
|
||||
rules = append(rules, rule.Tokens)
|
||||
}
|
||||
fingerprint, _ := json.Marshal(rules)
|
||||
group.policy.UUID = uuid.NewSHA1(uuid.NameSpaceOID, append([]byte(family+"\x00"), fingerprint...)).String()
|
||||
}
|
||||
inventory.Policies = append(inventory.Policies, group.policy)
|
||||
inventory.ManagedRuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.managedOrders...)
|
||||
inventory.RuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.orders...)
|
||||
}
|
||||
return inventory, nil
|
||||
}
|
||||
@@ -185,7 +285,10 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
|
||||
action = tokens[index]
|
||||
}
|
||||
}
|
||||
if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) {
|
||||
if action != "accept" && action != "drop" && action != "return" {
|
||||
return policy, "", "", nil
|
||||
}
|
||||
if action == "drop" && (policy.Protocol == "" || policy.HostPort == 0) {
|
||||
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
|
||||
}
|
||||
if action == "accept" && policy.Protocol == "" {
|
||||
@@ -194,18 +297,6 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
|
||||
return policy, source, action, nil
|
||||
}
|
||||
|
||||
func hasAcceptAction(tokens []string) bool {
|
||||
for index, token := range tokens {
|
||||
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(token, "accept") && !(index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeObservedHost(value string) string {
|
||||
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
|
||||
return prefix.Addr().String()
|
||||
@@ -241,3 +332,17 @@ func uniqueSortedStrings(values []string) []string {
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
type orderedPolicyRule struct {
|
||||
order int64
|
||||
rule []string
|
||||
}
|
||||
|
||||
func sortPolicyRules(segments []orderedPolicyRule) [][]string {
|
||||
sort.SliceStable(segments, func(i, j int) bool { return segments[i].order < segments[j].order })
|
||||
rules := make([][]string, 0, len(segments))
|
||||
for _, segment := range segments {
|
||||
rules = append(rules, segment.rule)
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ const (
|
||||
ModeSources = "deny_sources"
|
||||
ModeAllow = "allow_sources"
|
||||
ModeAll = "deny_all"
|
||||
ModeAcceptSources = "accept_sources"
|
||||
ModeAcceptAll = "accept_all"
|
||||
StatusEffective = "effective"
|
||||
StatusDisabled = "disabled"
|
||||
StatusNotEffective = "not_effective"
|
||||
@@ -35,7 +37,12 @@ type FamilyError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *FamilyError) Error() string { return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) }
|
||||
func (e *FamilyError) Error() string {
|
||||
if err, ok := e.Err.(buserr.BusinessError); ok && err.Msg == "ErrDockerForwardPolicyDrop" {
|
||||
return err.Error()
|
||||
}
|
||||
return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err)
|
||||
}
|
||||
func (e *FamilyError) Unwrap() error { return e.Err }
|
||||
|
||||
type ProxyEndpoint struct {
|
||||
@@ -55,16 +62,18 @@ type DNATRules struct {
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
UUID string
|
||||
Family string
|
||||
HostIP string
|
||||
HostPort uint16
|
||||
Protocol string
|
||||
Mode string
|
||||
Sources []string
|
||||
UUID string
|
||||
Family string
|
||||
HostIP string
|
||||
HostPort uint16
|
||||
Protocol string
|
||||
Mode string
|
||||
Sources []string
|
||||
NativeRules []NativeRule `json:",omitempty"`
|
||||
}
|
||||
|
||||
type FamilyStatus struct {
|
||||
Partial bool
|
||||
State string
|
||||
Reason string
|
||||
Initialized bool
|
||||
@@ -78,24 +87,17 @@ type NativeRule struct {
|
||||
Tokens []string `json:"tokens"`
|
||||
}
|
||||
|
||||
type ReadOnlyPolicy struct {
|
||||
Policy Policy
|
||||
Action string
|
||||
Sequence int64
|
||||
NativeRules []NativeRule
|
||||
}
|
||||
|
||||
type PolicyInventory struct {
|
||||
Policies []Policy
|
||||
ReadOnly []ReadOnlyPolicy
|
||||
ManagedRuleOrders map[string][]int64
|
||||
Policies []Policy
|
||||
RuleOrders map[string][]int64
|
||||
}
|
||||
|
||||
type Runtime interface {
|
||||
Initialize([]Policy, PolicyInventory) error
|
||||
Bind() error
|
||||
Initialize([]Policy, PolicyInventory, ...string) error
|
||||
Bind(...string) error
|
||||
OperateFamily(string, bool) error
|
||||
ReplacePolicies([]Policy, PolicyInventory) error
|
||||
Unbind() error
|
||||
Unbind(...string) error
|
||||
Cleanup() error
|
||||
Initialized(string) (bool, error)
|
||||
Status(string) FamilyStatus
|
||||
|
||||
@@ -9,28 +9,29 @@ var (
|
||||
ErrAdapterUnavailable = errors.New("firewall rule adapter is unavailable")
|
||||
ErrInventoryUnavailable = errors.New("firewall rule inventory is unavailable")
|
||||
ErrFamilyUnavailable = errors.New("firewall address family is unavailable")
|
||||
ErrRuleNotFound = errors.New("firewall rule does not exist")
|
||||
)
|
||||
|
||||
type ChangeOperation string
|
||||
|
||||
const (
|
||||
ChangeCreate ChangeOperation = "create"
|
||||
ChangeAdopt ChangeOperation = "adopt"
|
||||
ChangeUpdate ChangeOperation = "update"
|
||||
ChangeDelete ChangeOperation = "delete"
|
||||
ChangeReorder ChangeOperation = "reorder"
|
||||
)
|
||||
|
||||
type RuleChange struct {
|
||||
CommandOnly bool `json:"-"`
|
||||
UnmarkedAdopted bool `json:"-"`
|
||||
Operation ChangeOperation `json:"operation"`
|
||||
Before *FirewallRule `json:"before,omitempty"`
|
||||
After *FirewallRule `json:"after,omitempty"`
|
||||
Locator *Locator `json:"locator,omitempty"`
|
||||
PreviousMarker string `json:"previousMarker,omitempty"`
|
||||
Append bool `json:"append,omitempty"`
|
||||
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
|
||||
Target *ObservedRule `json:"-"`
|
||||
Raw string `json:"raw,omitempty"`
|
||||
CommandOnly bool `json:"-"`
|
||||
Operation ChangeOperation `json:"operation"`
|
||||
Before *FirewallRule `json:"before,omitempty"`
|
||||
After *FirewallRule `json:"after,omitempty"`
|
||||
Locator *Locator `json:"locator,omitempty"`
|
||||
PreviousMarker string `json:"previousMarker,omitempty"`
|
||||
Append bool `json:"append,omitempty"`
|
||||
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
|
||||
}
|
||||
|
||||
type NativeCommand struct {
|
||||
@@ -69,7 +70,6 @@ func (p CommandBatch) CreatesOnly() bool {
|
||||
|
||||
type Adapter interface {
|
||||
Provider() Provider
|
||||
Capabilities(context.Context) (Capabilities, error)
|
||||
ListRules(context.Context, Scope) (RuleSet, error)
|
||||
BuildCommands(RuleSet, []RuleChange) (CommandBatch, error)
|
||||
RunCommands(context.Context, CommandBatch) error
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -53,28 +54,6 @@ func RuleMatchKey(rule FirewallRule) (string, error) {
|
||||
return normalizedRuleKey(normalized)
|
||||
}
|
||||
|
||||
func OppositeActions(left, right Action) bool {
|
||||
return left == ActionAccept && (right == ActionDrop || right == ActionReject) ||
|
||||
right == ActionAccept && (left == ActionDrop || left == ActionReject)
|
||||
}
|
||||
|
||||
func SameRuleContent(before, after FirewallRule) (bool, error) {
|
||||
before, err := NormalizeRule(before)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
after, err = NormalizeRule(after)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
previous, err := RuleMatchKey(before)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
requested, err := RuleMatchKey(after)
|
||||
return err == nil && previous == requested && before.Action == after.Action, err
|
||||
}
|
||||
|
||||
func normalizedRuleKey(normalized FirewallRule) (string, error) {
|
||||
identity := ruleIdentity{
|
||||
Scope: normalized.Scope.Key(),
|
||||
@@ -231,3 +210,29 @@ func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) b
|
||||
wantKey, wantErr := RuleKey(expected)
|
||||
return gotErr == nil && wantErr == nil && gotKey == wantKey
|
||||
}
|
||||
|
||||
var descriptionCounterPattern = regexp.MustCompile(`\bcounter packets \d+ bytes \d+\b`)
|
||||
|
||||
func DescriptionID(observed ObservedRule) (string, error) {
|
||||
if observed.ParseStatus == ParseStatusSupported {
|
||||
key, err := RuleKey(observed.Rule)
|
||||
return "firewall:" + key, err
|
||||
}
|
||||
raw := observed.Raw
|
||||
if observed.Rule.Scope.Provider == ProviderUFW {
|
||||
if end := strings.Index(raw, "]"); strings.HasPrefix(strings.TrimSpace(raw), "[") && end >= 0 {
|
||||
raw = strings.Join(strings.Fields(raw[end+1:]), " ")
|
||||
}
|
||||
}
|
||||
if observed.Rule.Scope.Provider == ProviderNftables {
|
||||
raw = descriptionCounterPattern.ReplaceAllString(raw, "counter")
|
||||
}
|
||||
payload, err := json.Marshal(struct {
|
||||
Scope Scope
|
||||
Raw string
|
||||
}{observed.Rule.Scope.Normalize(), raw})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("firewall:%x", sha256.Sum256(payload)), nil
|
||||
}
|
||||
|
||||
@@ -1,70 +1,13 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
type RuleOrigin string
|
||||
|
||||
const (
|
||||
RuleOriginCreated RuleOrigin = constant.FirewallRuleOriginCreated
|
||||
RuleOriginAdopted RuleOrigin = constant.FirewallRuleOriginAdopted
|
||||
)
|
||||
|
||||
type InventoryState string
|
||||
|
||||
const (
|
||||
InventoryStateManaged InventoryState = "managed"
|
||||
InventoryStateAdopted InventoryState = "adopted"
|
||||
InventoryStateExternal InventoryState = "external"
|
||||
InventoryStateDrifted InventoryState = "drifted"
|
||||
InventoryStateProtected InventoryState = "protected"
|
||||
)
|
||||
|
||||
type InventoryMatch string
|
||||
|
||||
const (
|
||||
InventoryMatchNone InventoryMatch = "none"
|
||||
InventoryMatchExact InventoryMatch = "exact"
|
||||
InventoryMatchChanged InventoryMatch = "changed"
|
||||
InventoryMatchMissing InventoryMatch = "missing"
|
||||
InventoryMatchAmbiguous InventoryMatch = "ambiguous"
|
||||
InventoryMatchOpaque InventoryMatch = "opaque"
|
||||
)
|
||||
|
||||
type DesiredRule struct {
|
||||
UUID string `json:"uuid"`
|
||||
Rule FirewallRule `json:"rule"`
|
||||
RuleKey string `json:"ruleKey"`
|
||||
Origin RuleOrigin `json:"origin"`
|
||||
Protected bool `json:"protected,omitempty"`
|
||||
Expanded bool `json:"expanded,omitempty"`
|
||||
Marker string `json:"marker,omitempty"`
|
||||
ObservedInstanceKey string `json:"observedInstanceKey,omitempty"`
|
||||
}
|
||||
|
||||
type PositionRange struct {
|
||||
Min int `json:"min"`
|
||||
Max int `json:"max"`
|
||||
}
|
||||
|
||||
type InventoryItem struct {
|
||||
Incompatible bool `json:"incompatible,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Rule FirewallRule `json:"rule"`
|
||||
Observed *ObservedRule `json:"observed,omitempty"`
|
||||
Desired *DesiredRule `json:"desired,omitempty"`
|
||||
State InventoryState `json:"state"`
|
||||
Match InventoryMatch `json:"match"`
|
||||
}
|
||||
|
||||
type Inventory struct {
|
||||
Items []InventoryItem `json:"items"`
|
||||
Notices []ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type InventoryMergeInput struct {
|
||||
Observed []ObservedRule
|
||||
Desired []DesiredRule
|
||||
ProtectedObservedKeys map[string]struct{}
|
||||
Rule FirewallRule `json:"rule"`
|
||||
Observed *ObservedRule `json:"observed"`
|
||||
IsWhitelist bool `json:"isWhitelist"`
|
||||
DescriptionID string `json:"descriptionID"`
|
||||
}
|
||||
|
||||
@@ -88,9 +88,7 @@ const (
|
||||
type ScopeNoticeCode string
|
||||
|
||||
const (
|
||||
ScopeNoticeDefaultScopeMismatch ScopeNoticeCode = "default_scope_mismatch"
|
||||
ScopeNoticeManagedScopeInactive ScopeNoticeCode = "managed_scope_inactive"
|
||||
ScopeNoticeUnmanagedActiveScopes ScopeNoticeCode = "unmanaged_active_scopes"
|
||||
ScopeNoticeRuntimePermanentMismatch ScopeNoticeCode = "runtime_permanent_mismatch"
|
||||
ScopeNoticeManagedScopeMissing ScopeNoticeCode = "managed_scope_missing"
|
||||
ScopeNoticeFamilyUnavailable ScopeNoticeCode = "family_unavailable"
|
||||
@@ -303,10 +301,3 @@ type RuleSet struct {
|
||||
Rules []ObservedRule `json:"rules"`
|
||||
Notices []ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type Capabilities struct {
|
||||
Marker bool
|
||||
OwnedChains bool
|
||||
ExplicitPosition bool
|
||||
ExplicitPriority bool
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -47,17 +46,6 @@ func NewAdapterWithBackend(reader CommandReader, writer CommandWriter) *Adapter
|
||||
|
||||
func (a *Adapter) Provider() filter.Provider { return filter.ProviderFirewalld }
|
||||
|
||||
func (a *Adapter) Capabilities(ctx context.Context) (filter.Capabilities, error) {
|
||||
explicitPriority, err := a.supportsRichRulePriority(ctx)
|
||||
if err != nil {
|
||||
return filter.Capabilities{}, err
|
||||
}
|
||||
return filter.Capabilities{
|
||||
|
||||
ExplicitPriority: explicitPriority,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error {
|
||||
if rule.Priority == nil || *rule.Priority == 0 {
|
||||
return nil
|
||||
@@ -215,8 +203,13 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err
|
||||
return errors.New("firewalld writer is required")
|
||||
}
|
||||
executed, alreadyEnabled := 0, 0
|
||||
var missing error
|
||||
for index, command := range commands.Commands {
|
||||
err := a.writer.Run(ctx, command)
|
||||
if errors.Is(err, filter.ErrRuleNotFound) && plan.CommandOnly {
|
||||
missing = err
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, ErrAlreadyEnabled) {
|
||||
alreadyEnabled++
|
||||
err = nil
|
||||
@@ -232,7 +225,7 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err
|
||||
if commands.Operation == filter.ChangeCreate && alreadyEnabled > 0 && alreadyEnabled == len(commands.Commands) {
|
||||
return ErrAlreadyEnabled
|
||||
}
|
||||
return nil
|
||||
return missing
|
||||
}
|
||||
|
||||
func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
|
||||
@@ -299,10 +292,66 @@ func batchCommands(plan filter.CommandBatch) (filter.RuleCommands, error) {
|
||||
}
|
||||
|
||||
func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) {
|
||||
rule := change.After
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
rule = change.Before
|
||||
if change.Operation == filter.ChangeCreate && change.Raw != "" && change.After != nil {
|
||||
raw := strings.TrimSpace(change.Raw)
|
||||
if strings.ContainsAny(raw, "\r\n\x00") {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
var expected filter.ObservedRule
|
||||
kind := "rich-rule"
|
||||
if change.After.NativeKind == filter.NativeKindZoneService {
|
||||
if !validServiceName(raw) {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
expected = opaqueZoneService(snapshot.Scope, raw)
|
||||
kind = "service"
|
||||
} else {
|
||||
if !strings.HasPrefix(raw, "rule ") {
|
||||
return filter.RuleCommands{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
expected = opaqueRichRule(change.After.Scope, raw)
|
||||
}
|
||||
commands, rollback := ruleCommands("--add-"+kind+"="+raw, "--remove-"+kind+"="+raw)
|
||||
return filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected, Commands: commands, RollbackCommands: rollback}, nil
|
||||
}
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
kind, value, ok := strings.Cut(change.Target.Locator.Canonical, ":")
|
||||
if !ok || value == "" || strings.ContainsAny(value, "\r\n\x00") {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
var target filter.ObservedRule
|
||||
switch kind {
|
||||
case "port":
|
||||
rules := parseZonePorts(snapshot.Scope, value)
|
||||
if len(rules) != 1 {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
target = rules[0]
|
||||
case "service":
|
||||
if !validServiceName(value) {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
target = opaqueZoneService(snapshot.Scope, value)
|
||||
case "rich":
|
||||
kind = "rich-rule"
|
||||
if !strings.HasPrefix(value, "rule ") {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
var parsed bool
|
||||
target, _, parsed = parseRichRule(snapshot.Scope, value)
|
||||
if !parsed {
|
||||
target = opaqueRichRule(snapshot.Scope, value)
|
||||
}
|
||||
default:
|
||||
return filter.RuleCommands{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
commands, rollback := ruleCommands("--remove-"+kind+"="+value, "--add-"+kind+"="+value)
|
||||
return filter.RuleCommands{Operation: filter.ChangeDelete, Previous: &target, Expected: target, Commands: commands, RollbackCommands: rollback}, nil
|
||||
}
|
||||
rule := change.After
|
||||
if rule == nil {
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation)
|
||||
}
|
||||
@@ -322,16 +371,8 @@ func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChang
|
||||
switch change.Operation {
|
||||
case filter.ChangeCreate:
|
||||
plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove"))
|
||||
case filter.ChangeAdopt:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, false)
|
||||
if targetErr != nil {
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
plan.Previous = &target
|
||||
plan.Expected = target
|
||||
plan.Expected.Rule.UUID = normalized.UUID
|
||||
case filter.ChangeUpdate:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, true)
|
||||
target, targetErr := validateMutationTarget(snapshot, change)
|
||||
if targetErr != nil {
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
@@ -343,19 +384,6 @@ func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChang
|
||||
addCommands, removeNewCommands := ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove"))
|
||||
plan.Commands = append(removeCommands, addCommands...)
|
||||
plan.RollbackCommands = append(restoreCommands, removeNewCommands...)
|
||||
case filter.ChangeDelete:
|
||||
if change.CommandOnly && change.Locator == nil {
|
||||
plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "remove"), nativeOption(normalized, "add"))
|
||||
break
|
||||
}
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, true)
|
||||
if targetErr != nil {
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
plan.Previous = &target
|
||||
plan.Expected = target
|
||||
plan.Expected.Rule.UUID = normalized.UUID
|
||||
plan.Commands, plan.RollbackCommands = observedRuleCommands(target, "remove", "add")
|
||||
default:
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation)
|
||||
}
|
||||
@@ -441,42 +469,20 @@ func nativeOption(rule filter.FirewallRule, operation string) string {
|
||||
return "--" + operation + "-rich-rule=" + canonicalRichRule(rule)
|
||||
}
|
||||
|
||||
func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, normalized filter.FirewallRule, requireOwned bool) (filter.ObservedRule, error) {
|
||||
if change.Locator == nil || change.Locator.Canonical == "" {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: firewalld mutation requires canonical locator", filter.ErrInvalidRule)
|
||||
}
|
||||
if change.Locator.Provider != "" && change.Locator.Provider != filter.ProviderFirewalld {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: locator provider mismatch", filter.ErrInvalidRule)
|
||||
}
|
||||
if change.Locator.ScopeKey != "" && change.Locator.ScopeKey != snapshot.Scope.Key() {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: locator scope mismatch", filter.ErrInvalidRule)
|
||||
}
|
||||
matches := make([]filter.ObservedRule, 0, 1)
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.Locator.Canonical == change.Locator.Canonical {
|
||||
matches = append(matches, observed)
|
||||
}
|
||||
}
|
||||
if len(matches) != 1 {
|
||||
return filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
target := matches[0]
|
||||
if target.Protected {
|
||||
return filter.ObservedRule{}, filter.ErrProtectedRule
|
||||
func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange) (filter.ObservedRule, error) {
|
||||
target, err := filter.LocateRule(snapshot, change.Locator)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
if target.ParseStatus != filter.ParseStatusSupported {
|
||||
return filter.ObservedRule{}, filter.ErrRuleStale
|
||||
return filter.ObservedRule{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
want := normalized
|
||||
if requireOwned {
|
||||
if change.Before == nil || change.Before.UUID == "" {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: managed previous rule is required", filter.ErrInvalidRule)
|
||||
}
|
||||
prepared, err := (&Adapter{}).PrepareRule(*change.Before)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
want = prepared
|
||||
if change.Before == nil {
|
||||
return filter.ObservedRule{}, filter.ErrInvalidRule
|
||||
}
|
||||
want, err := (&Adapter{}).PrepareRule(*change.Before)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
wantKey, wantErr := filter.RuleKey(want)
|
||||
targetKey, targetErr := filter.RuleKey(target.Rule)
|
||||
@@ -665,6 +671,7 @@ type mergedObject struct {
|
||||
|
||||
func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filter.ObservedRule, error) {
|
||||
objects := make(map[string]*mergedObject)
|
||||
var ordered []*mergedObject
|
||||
richObjects := make(map[string]*mergedObject)
|
||||
add := func(rule filter.ObservedRule, runtimeState bool) *mergedObject {
|
||||
key := string(rule.Rule.NativeKind) + "\x00" + rule.Locator.Canonical
|
||||
@@ -673,6 +680,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt
|
||||
copy := rule
|
||||
object = &mergedObject{rule: copy}
|
||||
objects[key] = object
|
||||
ordered = append(ordered, object)
|
||||
}
|
||||
if runtimeState {
|
||||
object.runtime = true
|
||||
@@ -716,7 +724,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt
|
||||
}
|
||||
|
||||
rules := make([]filter.ObservedRule, 0, len(objects))
|
||||
for _, object := range objects {
|
||||
for _, object := range ordered {
|
||||
switch {
|
||||
case object.runtime && object.permanent:
|
||||
object.rule.Persistence = filter.PersistenceStatusConverged
|
||||
@@ -727,17 +735,7 @@ func mergeZoneObjects(scope filter.Scope, runtime, permanent zoneOutput) ([]filt
|
||||
}
|
||||
rules = append(rules, object.rule)
|
||||
}
|
||||
sort.SliceStable(rules, func(i, j int) bool {
|
||||
left, right := rules[i], rules[j]
|
||||
leftRank, rightRank := bucketRank(left.Rule.OrderBucket), bucketRank(right.Rule.OrderBucket)
|
||||
if leftRank != rightRank {
|
||||
return leftRank < rightRank
|
||||
}
|
||||
if left.Rule.Priority != nil && right.Rule.Priority != nil && *left.Rule.Priority != *right.Rule.Priority {
|
||||
return *left.Rule.Priority < *right.Rule.Priority
|
||||
}
|
||||
return left.Locator.Canonical < right.Locator.Canonical
|
||||
})
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
@@ -1009,21 +1007,6 @@ func sameStringSet(left, right []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func bucketRank(bucket string) int {
|
||||
switch bucket {
|
||||
case filter.OrderBucketRichPre:
|
||||
return 0
|
||||
case filter.OrderBucketRichZeroDeny:
|
||||
return 1
|
||||
case filter.OrderBucketZonePrimitiveAllow, filter.OrderBucketRichZeroAllow:
|
||||
return 2
|
||||
case filter.OrderBucketRichPost:
|
||||
return 3
|
||||
default:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
func richOrderBucket(priority int, action filter.Action) string {
|
||||
if priority < 0 {
|
||||
return filter.OrderBucketRichPre
|
||||
@@ -1049,7 +1032,7 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
|
||||
if err := validateSystemCommand(command); err != nil {
|
||||
return err
|
||||
}
|
||||
options, removals, alreadyEnabled := 0, 0, 0
|
||||
options, removals, alreadyEnabled, missing := 0, 0, 0, 0
|
||||
for _, arg := range command.Args {
|
||||
if strings.HasPrefix(arg, "--add-") || strings.HasPrefix(arg, "--remove-") {
|
||||
options++
|
||||
@@ -1078,6 +1061,7 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
|
||||
case strings.HasPrefix(line, "Warning: ALREADY_ENABLED:"):
|
||||
alreadyEnabled++
|
||||
case strings.HasPrefix(line, "Warning: NOT_ENABLED:"):
|
||||
missing++
|
||||
if removals != options {
|
||||
return fmt.Errorf("%w: %s", filter.ErrRuleStale, stderr.String())
|
||||
}
|
||||
@@ -1088,6 +1072,9 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
|
||||
if alreadyEnabled > 0 && alreadyEnabled == options {
|
||||
return ErrAlreadyEnabled
|
||||
}
|
||||
if missing > 0 {
|
||||
return fmt.Errorf("%w: %s", filter.ErrRuleNotFound, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -49,12 +49,6 @@ func NewAdapterWithBackend(reader RuleReader, writer RuleWriter) *Adapter {
|
||||
|
||||
func (a *Adapter) Provider() filter.Provider { return filter.ProviderIptables }
|
||||
|
||||
func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
|
||||
return filter.Capabilities{
|
||||
Marker: true, OwnedChains: true, ExplicitPosition: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
|
||||
rule, err := filter.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
@@ -163,8 +157,7 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh
|
||||
if createOnly {
|
||||
return compileCreateBatch(snapshot, changes)
|
||||
}
|
||||
externalDelete := len(changes) == 1 && changes[0].Locator == nil && (changes[0].UnmarkedAdopted || changes[0].PreviousMarker != "")
|
||||
if deleteOnly && !externalDelete {
|
||||
if deleteOnly {
|
||||
return compileDeleteBatch(snapshot, changes)
|
||||
}
|
||||
if len(changes) != 1 {
|
||||
@@ -221,14 +214,15 @@ func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f
|
||||
var script strings.Builder
|
||||
fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
|
||||
for _, change := range changes {
|
||||
rulePlan, err := compileChange(snapshot, change)
|
||||
if err != nil {
|
||||
return filter.CommandBatch{}, err
|
||||
if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() || change.Target.Raw == "" {
|
||||
return filter.CommandBatch{}, filter.ErrInvalidRule
|
||||
}
|
||||
line, err := restoreRuleLine(snapshot.Scope, *rulePlan.Previous)
|
||||
line, err := restoreRuleLine(snapshot.Scope, *change.Target)
|
||||
if err != nil {
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
target := parseRule(snapshot.Scope, line, 0)
|
||||
rulePlan := filter.RuleCommands{Operation: filter.ChangeDelete, Previous: &target, Expected: target}
|
||||
script.WriteString(strings.Replace(line, "-A ", "-D ", 1))
|
||||
script.WriteByte('\n')
|
||||
rulePlan.Commands, rulePlan.RollbackCommands = nil, nil
|
||||
@@ -238,6 +232,14 @@ func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f
|
||||
plan.Rules[0].Commands = []filter.NativeCommand{{
|
||||
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: script.String(),
|
||||
}}
|
||||
if len(changes) == 1 {
|
||||
args, err := shellwords.Parse(plan.Rules[0].Expected.Raw)
|
||||
if err != nil {
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
args[0] = "-D"
|
||||
plan.Rules[0].Commands = []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: append([]string{"-w", "-t", snapshot.Scope.Table}, args...)}}
|
||||
}
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
@@ -246,6 +248,17 @@ func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f
|
||||
var script strings.Builder
|
||||
fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
|
||||
for _, change := range changes {
|
||||
if change.Raw != "" {
|
||||
observed := parseRule(snapshot.Scope, change.Raw, len(snapshot.Rules)+1)
|
||||
line, err := restoreRuleLine(snapshot.Scope, observed)
|
||||
if err != nil {
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
script.WriteString(line + "\n")
|
||||
observed.Locator.Position = nil
|
||||
plan.Rules = append(plan.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: observed})
|
||||
continue
|
||||
}
|
||||
rulePlan, err := compileChange(snapshot, change)
|
||||
if err != nil {
|
||||
return filter.CommandBatch{}, err
|
||||
@@ -379,17 +392,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: protocol %q does not match %s", filter.ErrInvalidRule, normalized.Protocol, normalized.Scope.Family)
|
||||
}
|
||||
marker := "1panel-rule:" + normalized.UUID
|
||||
if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil {
|
||||
previous := filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported}
|
||||
var commands []filter.NativeCommand
|
||||
if change.UnmarkedAdopted || change.PreviousMarker != "" {
|
||||
previous.Marker = change.PreviousMarker
|
||||
args := []string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain}
|
||||
args = append(args, compileObservedRuleArgs(previous)...)
|
||||
commands = []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: args}}
|
||||
}
|
||||
return filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Previous: &previous, Expected: previous, Commands: commands}, nil
|
||||
}
|
||||
|
||||
position := len(snapshot.Rules) + 1
|
||||
verb := "-I"
|
||||
var target filter.ObservedRule
|
||||
@@ -400,12 +403,6 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule)
|
||||
}
|
||||
position = insertionPosition(snapshot, normalized)
|
||||
case filter.ChangeAdopt:
|
||||
position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
|
||||
if err != nil {
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
verb = "-R"
|
||||
case filter.ChangeUpdate:
|
||||
position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
|
||||
if err != nil {
|
||||
@@ -450,7 +447,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
|
||||
case filter.ChangeCreate:
|
||||
rollbackArgs = append(rollbackArgs, "-D", snapshot.Scope.Chain)
|
||||
rollbackArgs = append(rollbackArgs, compileRuleArgs(normalized, marker)...)
|
||||
case filter.ChangeAdopt, filter.ChangeUpdate:
|
||||
case filter.ChangeUpdate:
|
||||
rollbackArgs = append(rollbackArgs, "-R", snapshot.Scope.Chain, strconv.Itoa(position))
|
||||
rollbackArgs = append(rollbackArgs, compileObservedRuleArgs(target)...)
|
||||
case filter.ChangeDelete:
|
||||
@@ -483,10 +480,11 @@ func positionalMutationPlan(snapshot filter.RuleSet, rule filter.FirewallRule, p
|
||||
RuleUUID: rule.UUID, Operation: operation, Previous: &previous, Expected: expected,
|
||||
}
|
||||
if position == targetPosition {
|
||||
plan.Expected = previous
|
||||
return plan
|
||||
}
|
||||
executable := executableForFamily(snapshot.Scope.Family)
|
||||
deleteArgs := append([]string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain}, compileObservedRuleArgs(previous)...)
|
||||
deleteArgs := []string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain, strconv.Itoa(position)}
|
||||
insertArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(targetPosition)}
|
||||
insertArgs = append(insertArgs, compileRuleArgs(rule, marker)...)
|
||||
restoreArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(position)}
|
||||
@@ -587,8 +585,7 @@ func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, a
|
||||
if wantErr != nil || observedErr != nil || wantKey != observedKey {
|
||||
return 0, filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
if change.Operation != filter.ChangeAdopt && observed.Marker != marker &&
|
||||
!(change.Operation == filter.ChangeDelete && change.UnmarkedAdopted && observed.Marker == "") {
|
||||
if observed.Marker != change.PreviousMarker {
|
||||
return 0, filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
return position, observed, nil
|
||||
@@ -654,11 +651,15 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options := []cmd.Option{cmd.WithContext(ctx), cmd.WithTimeout(60 * time.Second)}
|
||||
options := []cmd.Option{cmd.WithContext(ctx), cmd.WithTimeout(60 * time.Second), cmd.WithEnv("LC_ALL=C")}
|
||||
if command.Stdin != "" {
|
||||
options = append(options, cmd.WithStdin(strings.NewReader(command.Stdin)))
|
||||
}
|
||||
return cmd.NewCommandMgr(options...).RunWithOptionalSudo(executable, command.Args...)
|
||||
err = cmd.NewCommandMgr(options...).RunWithOptionalSudo(executable, command.Args...)
|
||||
if err != nil && (strings.Contains(err.Error(), "Bad rule (does a matching rule exist in that chain?)") || strings.Contains(err.Error(), "No chain/target/match by that name")) {
|
||||
return fmt.Errorf("%w: %v", filter.ErrRuleNotFound, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (systemBackend) Save(ctx context.Context, scope filter.Scope) error {
|
||||
|
||||
@@ -33,12 +33,6 @@ func NewAdapterWithBackend(backend Backend) *Adapter { return &Adapter{backend:
|
||||
|
||||
func (a *Adapter) Provider() filter.Provider { return filter.ProviderNftables }
|
||||
|
||||
func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
|
||||
return filter.Capabilities{
|
||||
Marker: true, OwnedChains: true, ExplicitPosition: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
|
||||
rule, err := filter.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
@@ -164,7 +158,7 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh
|
||||
return filter.CommandBatch{}, fmt.Errorf("%w: nftables mutation requires exactly one change", filter.ErrInvalidRule)
|
||||
}
|
||||
change := changes[0]
|
||||
if change.Operation != filter.ChangeUpdate && change.Operation != filter.ChangeAdopt && change.Operation != filter.ChangeReorder {
|
||||
if change.Operation != filter.ChangeUpdate && change.Operation != filter.ChangeReorder {
|
||||
return filter.CommandBatch{}, fmt.Errorf("%w: unsupported nftables mutation %s", filter.ErrInvalidRule, change.Operation)
|
||||
}
|
||||
expected, previous, err := compileChange(snapshot, change)
|
||||
@@ -180,7 +174,9 @@ func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleCh
|
||||
target := *expected.Locator.Position
|
||||
var script string
|
||||
if target == *previous.Locator.Position {
|
||||
if change.Operation != filter.ChangeReorder {
|
||||
if change.Operation == filter.ChangeReorder {
|
||||
rulePlan.Expected = *previous
|
||||
} else {
|
||||
script = fmt.Sprintf("replace rule %s handle %s %s\n", chain, handle, expected.Raw)
|
||||
if strings.ContainsAny(previous.Raw, "\r\n") || previous.Raw == "" {
|
||||
return filter.CommandBatch{}, fmt.Errorf("%w: invalid native nftables rule", filter.ErrInvalidRule)
|
||||
@@ -213,17 +209,17 @@ func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f
|
||||
plan := filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: true}
|
||||
var script strings.Builder
|
||||
for _, change := range changes {
|
||||
expected, previous, err := compileChange(snapshot, change)
|
||||
if err != nil {
|
||||
return filter.CommandBatch{}, err
|
||||
if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() || change.Target.Raw == "" {
|
||||
return filter.CommandBatch{}, filter.ErrInvalidRule
|
||||
}
|
||||
handle := previous.Locator.NativeID
|
||||
if _, err := strconv.ParseUint(handle, 10, 64); err != nil || change.Locator.NativeID != handle {
|
||||
return filter.CommandBatch{}, filter.ErrRuleStale
|
||||
handle := change.Target.Locator.NativeID
|
||||
if _, err := strconv.ParseUint(handle, 10, 64); err != nil {
|
||||
return filter.CommandBatch{}, filter.ErrInvalidRule
|
||||
}
|
||||
target := parseRule(snapshot.Scope, change.Target.Raw, handle, 0)
|
||||
fmt.Fprintf(&script, "delete rule %s %s %s handle %s\n", nftables_helper.TableFamily(snapshot.Scope.Family), nftables_helper.TableName, nativeChainName(snapshot.Scope), handle)
|
||||
plan.Rules = append(plan.Rules, filter.RuleCommands{
|
||||
RuleUUID: ruleUUID(change), Operation: filter.ChangeDelete, Previous: previous, Expected: expected,
|
||||
Operation: filter.ChangeDelete, Previous: &target, Expected: target,
|
||||
})
|
||||
}
|
||||
plan.Rules[0].Commands = []filter.NativeCommand{{Executable: "nft", Stdin: script.String()}}
|
||||
@@ -234,6 +230,16 @@ func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (f
|
||||
plan := filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: true}
|
||||
var script strings.Builder
|
||||
for _, change := range changes {
|
||||
if change.Raw != "" {
|
||||
if strings.ContainsAny(change.Raw, "\r\n;\x00") {
|
||||
return filter.CommandBatch{}, filter.ErrInvalidRule
|
||||
}
|
||||
expected := parseRule(snapshot.Scope, change.Raw, "", len(snapshot.Rules)+1)
|
||||
expected.Locator.Position = nil
|
||||
fmt.Fprintf(&script, "add rule %s %s %s %s\n", nftables_helper.TableFamily(snapshot.Scope.Family), nftables_helper.TableName, nativeChainName(snapshot.Scope), change.Raw)
|
||||
plan.Rules = append(plan.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected})
|
||||
continue
|
||||
}
|
||||
if change.After == nil {
|
||||
return filter.CommandBatch{}, fmt.Errorf("%w: create rule is required", filter.ErrInvalidRule)
|
||||
}
|
||||
@@ -664,7 +670,11 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
|
||||
return fmt.Errorf("unexpected nftables executable %q", command.Executable)
|
||||
}
|
||||
if command.Stdin != "" {
|
||||
return nftables_helper.RunScriptContext(ctx, command.Stdin)
|
||||
err := nftables_helper.RunScriptContext(ctx, command.Stdin)
|
||||
if err != nil && strings.Contains(err.Error(), "No such file or directory") && strings.HasPrefix(command.Stdin, "delete rule ") {
|
||||
return fmt.Errorf("%w: %v", filter.ErrRuleNotFound, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).
|
||||
RunWithOptionalSudo(command.Executable, command.Args...)
|
||||
|
||||
@@ -9,12 +9,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
)
|
||||
|
||||
var ErrAlreadyEnabled = errors.New("ufw rule already exists")
|
||||
|
||||
type CommandReader interface {
|
||||
filter.CommentRuleReader
|
||||
Read(context.Context, ...string) (string, error)
|
||||
@@ -79,13 +80,6 @@ func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule
|
||||
return a.writer.Run(ctx, command)
|
||||
}
|
||||
|
||||
func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
|
||||
return filter.Capabilities{
|
||||
|
||||
Marker: true, ExplicitPosition: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, comment string) ([]filter.ObservedRule, error) {
|
||||
var rules []filter.ObservedRule
|
||||
var output string
|
||||
@@ -101,7 +95,7 @@ func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope,
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
rules = append(rules, parseNumberedRules(scope, output)...)
|
||||
rules = append(rules, ParseRules(scope, output)...)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
@@ -150,7 +144,7 @@ func (a *Adapter) ListRuleScopes(ctx context.Context, scopes []filter.Scope) ([]
|
||||
notices := statusNotices(numbered)
|
||||
snapshots := make([]filter.RuleSet, 0, len(normalizedScopes))
|
||||
for _, scope := range normalizedScopes {
|
||||
snapshot, err := filter.NewRuleSet(scope, parseNumberedRules(scope, numbered))
|
||||
snapshot, err := filter.NewRuleSet(scope, ParseRules(scope, numbered))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,35 +209,11 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err
|
||||
executed := 0
|
||||
for index, command := range plan.Rules[0].Commands {
|
||||
err := a.writer.Run(ctx, command)
|
||||
if err != nil && plan.CommandOnly && plan.CreatesOnly() && command.Args[0] == "insert" &&
|
||||
(strings.Contains(err.Error(), "Invalid position") || strings.Contains(err.Error(), "Cannot insert rule at position")) {
|
||||
ipv4, ipv6 := plan.Scope, plan.Scope
|
||||
ipv4.Family, ipv6.Family = filter.FamilyIPv4, filter.FamilyIPv6
|
||||
snapshots, readErr := a.ListRuleScopes(ctx, []filter.Scope{ipv4, ipv6})
|
||||
if readErr != nil {
|
||||
return errors.Join(err, readErr)
|
||||
}
|
||||
lastPosition := maximumObservedPosition(snapshots[0])
|
||||
if plan.Scope.Family == filter.FamilyIPv6 {
|
||||
lastPosition = max(lastPosition, maximumObservedPosition(snapshots[1]))
|
||||
}
|
||||
position, _ := strconv.Atoi(command.Args[1])
|
||||
if position == lastPosition+1 && !hasScopeNotice(snapshots[0].Notices, filter.ScopeNoticeManagedScopeInactive) {
|
||||
err = a.writer.Run(ctx, commentCommand(plan.Rules[0].Expected.Rule, plan.Rules[0].Expected.Marker))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if plan.CommandOnly {
|
||||
return fmt.Errorf("execute UFW rule: %w", err)
|
||||
}
|
||||
if !plan.CreatesOnly() {
|
||||
probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
|
||||
if a.failedCommandApplied(probeCtx, plan.Rules[0], index) {
|
||||
executed = index + 1
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
cause := ufwApplyError(plan.Rules[0], fmt.Errorf("execute UFW rule: %w", err))
|
||||
cause := fmt.Errorf("execute UFW rule: %w", err)
|
||||
return a.compensate(ctx, plan.Rules[0], executed, cause)
|
||||
}
|
||||
executed = index + 1
|
||||
@@ -251,39 +221,6 @@ func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func ufwApplyError(plan filter.RuleCommands, cause error) error {
|
||||
if plan.Operation == filter.ChangeAdopt {
|
||||
return buserr.WithDetail("ErrUFWRuleAdopt", cause.Error(), cause)
|
||||
}
|
||||
return cause
|
||||
}
|
||||
|
||||
func (a *Adapter) failedCommandApplied(ctx context.Context, plan filter.RuleCommands, commandIndex int) bool {
|
||||
snapshot, err := a.ListRules(ctx, plan.Expected.Rule.Scope)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
markerCount := countMarker(snapshot, plan.Expected.Marker)
|
||||
switch plan.Operation {
|
||||
case filter.ChangeCreate:
|
||||
return markerCount > 0
|
||||
case filter.ChangeAdopt:
|
||||
if commandIndex == 0 {
|
||||
return plan.Previous == nil || !containsObservedRule(snapshot, *plan.Previous)
|
||||
}
|
||||
return markerCount > 0
|
||||
case filter.ChangeUpdate, filter.ChangeReorder:
|
||||
if commandIndex == 0 {
|
||||
return markerCount == 0
|
||||
}
|
||||
return markerCount > 0
|
||||
case filter.ChangeDelete:
|
||||
return markerCount == 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
|
||||
if err := validateBackendPlan(plan); err != nil {
|
||||
return err
|
||||
@@ -317,17 +254,71 @@ func validateBackendPlan(plan filter.CommandBatch) error {
|
||||
if err := validateScope(plan.Scope); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(plan.Rules[0].Commands) != len(plan.Rules[0].RollbackCommands) {
|
||||
if !plan.CommandOnly && len(plan.Rules[0].Commands) != len(plan.Rules[0].RollbackCommands) {
|
||||
return fmt.Errorf("%w: incomplete ufw rollback plan", filter.ErrInvalidRule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) {
|
||||
rule := change.After
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
rule = change.Before
|
||||
if change.Operation == filter.ChangeCreate && change.Raw != "" {
|
||||
observed := ParseRules(snapshot.Scope, change.Raw)
|
||||
if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported && change.After != nil {
|
||||
rule := observed[0].Rule
|
||||
rule.UUID, rule.OrderIndex = change.After.UUID, nil
|
||||
if rule.UUID == "" {
|
||||
rule.UUID = "import"
|
||||
}
|
||||
change.After, change.Raw = &rule, ""
|
||||
return compileChange(snapshot, change)
|
||||
}
|
||||
if len(observed) != 1 || observed[0].Rule.NativeKind != filter.NativeKindUFWApplication || observed[0].ParseStatus != filter.ParseStatusOpaque {
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: UFW listing cannot fully describe this rule", filter.ErrUnsupportedScope)
|
||||
}
|
||||
target := observed[0]
|
||||
rule := target.Rule
|
||||
if !validApplicationProfileName(rule.Description) || nativeAction(rule.Action) == "" {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
args := []string{nativeAction(rule.Action), "in"}
|
||||
if rule.Interface != "" {
|
||||
args = append(args, "on", rule.Interface)
|
||||
}
|
||||
args = append(args, "from", nativeAddress(rule.SourceAddress, rule.Scope.Family), "to", nativeAddress("", rule.Scope.Family), "app", rule.Description)
|
||||
if _, comment, ok := strings.Cut(change.Raw, "#"); ok {
|
||||
args = append(args, "comment", strings.TrimSpace(comment))
|
||||
}
|
||||
return filter.RuleCommands{Operation: filter.ChangeCreate, Expected: target, Commands: []filter.NativeCommand{{Executable: "ufw", Args: args}}}, nil
|
||||
}
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
if change.Target == nil || change.Target.Rule.Scope.Key() != snapshot.Scope.Key() {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
targets := ParseRules(snapshot.Scope, change.Target.Raw)
|
||||
if len(targets) != 1 {
|
||||
return filter.RuleCommands{}, filter.ErrInvalidRule
|
||||
}
|
||||
target := targets[0]
|
||||
var command filter.NativeCommand
|
||||
if target.ParseStatus == filter.ParseStatusSupported {
|
||||
command = deleteRuleCommand(target.Rule, observedComment(target))
|
||||
} else if target.Rule.NativeKind == filter.NativeKindUFWApplication && target.ParseStatus == filter.ParseStatusOpaque {
|
||||
matches := re.UFWNumberedRuleRegex.FindStringSubmatch(strings.TrimSpace(target.Raw))
|
||||
if len(matches) == 0 || matches[3] == "LIMIT" {
|
||||
return filter.RuleCommands{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
created, err := compileChange(snapshot, filter.RuleChange{Operation: filter.ChangeCreate, After: &target.Rule, Raw: target.Raw})
|
||||
if err != nil {
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
command = created.Commands[0]
|
||||
command.Args = append([]string{"--force", "delete"}, command.Args...)
|
||||
} else {
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: UFW listing cannot fully describe this rule", filter.ErrUnsupportedScope)
|
||||
}
|
||||
return filter.RuleCommands{Operation: filter.ChangeDelete, Previous: &target, Expected: target, Commands: []filter.NativeCommand{command}}, nil
|
||||
}
|
||||
rule := change.After
|
||||
if rule == nil {
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation)
|
||||
}
|
||||
@@ -345,17 +336,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule)
|
||||
}
|
||||
marker := "1panel-rule:" + normalized.UUID
|
||||
if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil {
|
||||
if change.UnmarkedAdopted || change.PreviousMarker != "" {
|
||||
marker = observedComment(filter.ObservedRule{Rule: normalized, Marker: change.PreviousMarker})
|
||||
}
|
||||
return filter.RuleCommands{
|
||||
RuleUUID: normalized.UUID, Operation: change.Operation,
|
||||
Expected: filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported},
|
||||
Commands: []filter.NativeCommand{deleteRuleCommand(normalized, marker)},
|
||||
RollbackCommands: []filter.NativeCommand{commentCommand(normalized, marker)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
position := insertionPosition(snapshot, normalized)
|
||||
expected := observedForRule(normalized, marker, position)
|
||||
plan := filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected}
|
||||
@@ -378,28 +359,6 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
|
||||
}
|
||||
plan.Commands = []filter.NativeCommand{command}
|
||||
plan.RollbackCommands = []filter.NativeCommand{deleteRuleCommand(normalized, marker)}
|
||||
case filter.ChangeAdopt:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, false)
|
||||
if targetErr != nil {
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
position = *target.Locator.Position
|
||||
appendAtEnd := position == maximumObservedPosition(snapshot)
|
||||
restoreAtEnd := change.RestoreAtEnd || appendAtEnd
|
||||
plan.Previous = &target
|
||||
plan.Expected = observedForRule(normalized, marker, position)
|
||||
if appendAtEnd {
|
||||
plan.Expected.Locator.NativeID = ""
|
||||
plan.Expected.Locator.Position = nil
|
||||
}
|
||||
plan.Commands = []filter.NativeCommand{
|
||||
deletePositionCommand(position),
|
||||
positionedCommand(position, normalized, marker, appendAtEnd),
|
||||
}
|
||||
plan.RollbackCommands = []filter.NativeCommand{
|
||||
positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd),
|
||||
deleteRuleCommand(normalized, marker),
|
||||
}
|
||||
case filter.ChangeUpdate, filter.ChangeReorder:
|
||||
if change.Before == nil {
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule)
|
||||
@@ -408,7 +367,7 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
|
||||
if err != nil {
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
target, targetErr := validateMutationTarget(snapshot, change, before, marker, true)
|
||||
target, targetErr := validateMutationTarget(snapshot, change, before)
|
||||
if targetErr != nil {
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
@@ -443,19 +402,6 @@ func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.Ru
|
||||
positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd),
|
||||
deleteRuleCommand(normalized, marker),
|
||||
}
|
||||
case filter.ChangeDelete:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, !change.UnmarkedAdopted)
|
||||
if targetErr != nil {
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
position = *target.Locator.Position
|
||||
plan.Previous = &target
|
||||
plan.Expected = target
|
||||
plan.Commands = []filter.NativeCommand{deleteRuleCommand(target.Rule, observedComment(target))}
|
||||
restoreAtEnd := change.RestoreAtEnd || position == maximumObservedPosition(snapshot)
|
||||
plan.RollbackCommands = []filter.NativeCommand{
|
||||
positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd),
|
||||
}
|
||||
default:
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation)
|
||||
}
|
||||
@@ -476,71 +422,20 @@ func validateWritableRule(rule filter.FirewallRule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, desired filter.FirewallRule, marker string, requireOwned bool) (filter.ObservedRule, error) {
|
||||
if change.Locator == nil || change.Locator.Position == nil {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw mutation requires a numbered locator", filter.ErrInvalidRule)
|
||||
func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, desired filter.FirewallRule) (filter.ObservedRule, error) {
|
||||
target, err := filter.LocateRule(snapshot, change.Locator)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
if change.Locator.Provider != "" && change.Locator.Provider != filter.ProviderUFW {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw locator provider mismatch", filter.ErrInvalidRule)
|
||||
}
|
||||
if change.Locator.ScopeKey != "" && change.Locator.ScopeKey != snapshot.Scope.Key() {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw locator scope mismatch", filter.ErrInvalidRule)
|
||||
}
|
||||
var matches []filter.ObservedRule
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.Locator.Position == nil || *observed.Locator.Position != *change.Locator.Position {
|
||||
continue
|
||||
}
|
||||
if change.Locator.NativeID != "" && observed.Locator.NativeID != change.Locator.NativeID {
|
||||
continue
|
||||
}
|
||||
if change.Locator.Canonical != "" && observed.Locator.Canonical != change.Locator.Canonical {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, observed)
|
||||
}
|
||||
if len(matches) != 1 {
|
||||
if target.Marker != strings.TrimSpace(change.PreviousMarker) {
|
||||
return filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
target := matches[0]
|
||||
if target.Protected {
|
||||
return filter.ObservedRule{}, filter.ErrProtectedRule
|
||||
}
|
||||
previousMarker := strings.TrimSpace(change.PreviousMarker)
|
||||
if requireOwned {
|
||||
if target.Marker != marker {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw rule is not owned by this rule UUID", filter.ErrInvalidRule)
|
||||
}
|
||||
} else if target.Marker != previousMarker {
|
||||
if target.Marker != "" {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw adoption target marker changed", filter.ErrRuleStale)
|
||||
}
|
||||
return filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
semanticMatch := filter.ObservedRuleMatchesExpected(target, desired)
|
||||
canHydrateOwned := target.Marker == marker &&
|
||||
(target.ParseStatus == filter.ParseStatusOpaque || semanticMatch)
|
||||
canHydrateAdopted := !requireOwned && target.Marker == previousMarker &&
|
||||
target.ParseStatus != filter.ParseStatusOpaque && semanticMatch
|
||||
if target.ParseStatus != filter.ParseStatusSupported && (canHydrateOwned || canHydrateAdopted) {
|
||||
orderIndex := target.Rule.OrderIndex
|
||||
target.Rule = desired
|
||||
target.Rule.OrderIndex = orderIndex
|
||||
target.ParseStatus = filter.ParseStatusSupported
|
||||
target.UncertainFields = nil
|
||||
}
|
||||
if target.ParseStatus != filter.ParseStatusSupported {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: opaque ufw rules cannot be modified", filter.ErrInvalidRule)
|
||||
return filter.ObservedRule{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
wantKey, err := filter.RuleKey(desired)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
gotKey, err := filter.RuleKey(target.Rule)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
if change.Operation == filter.ChangeAdopt && wantKey != gotKey {
|
||||
wantKey, wantErr := filter.RuleKey(desired)
|
||||
gotKey, gotErr := filter.RuleKey(target.Rule)
|
||||
if wantErr != nil || gotErr != nil || wantKey != gotKey {
|
||||
return filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
return target, nil
|
||||
@@ -680,29 +575,6 @@ func (a *Adapter) rollback(ctx context.Context, plan filter.RuleCommands, execut
|
||||
return rollbackErr
|
||||
}
|
||||
|
||||
func countMarker(snapshot filter.RuleSet, marker string) int {
|
||||
count := 0
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.Marker == marker {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func containsObservedRule(snapshot filter.RuleSet, expected filter.ObservedRule) bool {
|
||||
for _, observed := range snapshot.Rules {
|
||||
if expected.Locator.Position != nil &&
|
||||
(observed.Locator.Position == nil || *observed.Locator.Position != *expected.Locator.Position) {
|
||||
continue
|
||||
}
|
||||
if observed.Marker == expected.Marker && filter.ObservedRuleMatchesExpected(observed, expected.Rule) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasScopeNotice(notices []filter.ScopeNotice, code filter.ScopeNoticeCode) bool {
|
||||
for _, notice := range notices {
|
||||
if notice.Code == code {
|
||||
@@ -731,7 +603,7 @@ func validateCommand(command filter.NativeCommand) error {
|
||||
}
|
||||
}
|
||||
|
||||
func parseNumberedRules(scope filter.Scope, output string) []filter.ObservedRule {
|
||||
func ParseRules(scope filter.Scope, output string) []filter.ObservedRule {
|
||||
rules := make([]filter.ObservedRule, 0)
|
||||
for _, rawLine := range strings.Split(output, "\n") {
|
||||
raw := strings.TrimSpace(rawLine)
|
||||
@@ -802,9 +674,13 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
|
||||
return opaque()
|
||||
}
|
||||
|
||||
sourceProtocol := "all"
|
||||
sourceAddress, ok := parseSource(source)
|
||||
if !ok {
|
||||
return opaque()
|
||||
sourceAddress, sourceProtocol, ok = parseAddressProtocol(source)
|
||||
if !ok {
|
||||
return opaque()
|
||||
}
|
||||
}
|
||||
partialRule.SourceAddress = sourceAddress
|
||||
if ruleAction == "" {
|
||||
@@ -827,6 +703,12 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
|
||||
Persistence: filter.PersistenceStatusConverged,
|
||||
}
|
||||
}
|
||||
if sourceProtocol != "all" {
|
||||
if protocol != "all" && protocol != sourceProtocol || destinationPort != "" {
|
||||
return opaque()
|
||||
}
|
||||
protocol = sourceProtocol
|
||||
}
|
||||
partialRule.Protocol = protocol
|
||||
partialRule.DestinationAddress = destinationAddress
|
||||
partialRule.DestinationPort = destinationPort
|
||||
@@ -861,15 +743,9 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
|
||||
return opaque()
|
||||
}
|
||||
locator.Canonical = canonicalRule(normalized)
|
||||
parseStatus := filter.ParseStatusSupported
|
||||
var uncertainFields []string
|
||||
if normalized.Protocol == "all" && normalized.DestinationPort == "" {
|
||||
parseStatus = filter.ParseStatusPartial
|
||||
uncertainFields = []string{filter.ObservedFieldProtocol}
|
||||
}
|
||||
return filter.ObservedRule{
|
||||
Rule: normalized, Locator: locator, Marker: marker, ParseStatus: parseStatus,
|
||||
UncertainFields: uncertainFields, Raw: raw, Persistence: filter.PersistenceStatusConverged,
|
||||
Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported,
|
||||
Raw: raw, Persistence: filter.PersistenceStatusConverged,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,6 +813,9 @@ func familyForNumberedRule(destination, source string) filter.Family {
|
||||
func containsIPv6Address(value string) bool {
|
||||
for _, token := range strings.Fields(value) {
|
||||
token = strings.Trim(token, "(),")
|
||||
if address, _, ok := parseAddressProtocol(token); ok {
|
||||
token = address
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(token); err == nil && prefix.Addr().Is6() {
|
||||
return true
|
||||
}
|
||||
@@ -1225,11 +1104,14 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
|
||||
output, err := cmd.NewCommandMgr(
|
||||
cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"),
|
||||
).RunWithOptionalSudoAndStdout(command.Executable, command.Args...)
|
||||
if strings.Contains(output, "Could not delete non-existent rule") || err != nil && strings.Contains(err.Error(), "Could not delete non-existent rule") {
|
||||
return fmt.Errorf("%w: %s", filter.ErrRuleNotFound, strings.TrimSpace(output))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(output, "Could not delete non-existent rule") {
|
||||
return fmt.Errorf("%w: %s", filter.ErrRuleStale, strings.TrimSpace(output))
|
||||
if strings.Contains(output, "Skipping adding existing rule") {
|
||||
return fmt.Errorf("%w: %s", ErrAlreadyEnabled, strings.TrimSpace(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,28 +2,20 @@ package filter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRuleConflict = errors.New("firewall rule has identical conditions and an opposing action")
|
||||
ErrRuleStale = errors.New("firewall rule state is stale")
|
||||
ErrRuleOperation = errors.New("firewall rule operation is not allowed")
|
||||
)
|
||||
|
||||
var ErrVerificationFailed = errors.New("firewall rule verification failed")
|
||||
|
||||
func ProtectRuleSet(snapshot RuleSet, ports []PortWhitelist) (RuleSet, error) {
|
||||
rules := slices.Clone(snapshot.Rules)
|
||||
whitelist := NewPortWhitelistIndex(ports)
|
||||
for index := range rules {
|
||||
if rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule) {
|
||||
rules[index].Protected = true
|
||||
}
|
||||
rules[index].Protected = rules[index].Protected || rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule)
|
||||
}
|
||||
protected := snapshot
|
||||
protected.Rules = rules
|
||||
@@ -71,19 +63,11 @@ func NewPortWhitelistIndex(ports []PortWhitelist) PortWhitelistIndex {
|
||||
return index
|
||||
}
|
||||
|
||||
func RuleMatchesPortWhitelist(rule FirewallRule, ports []PortWhitelist) bool {
|
||||
return NewPortWhitelistIndex(ports).Matches(rule)
|
||||
}
|
||||
|
||||
func (index PortWhitelistIndex) Matches(rule FirewallRule) bool {
|
||||
rule, err := NormalizeRule(rule)
|
||||
if err != nil || rule.Action != ActionAccept || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" || len(rule.ConnectionStates) != 0 {
|
||||
return false
|
||||
}
|
||||
if rule.Scope.Provider == ProviderFirewalld && (rule.NativeKind == NativeKindZonePort ||
|
||||
(rule.NativeKind == NativeKindRule && rule.Scope.Family == FamilyInet && rule.Priority == nil)) {
|
||||
return false
|
||||
}
|
||||
families := []Family{rule.Scope.Family}
|
||||
if rule.Scope.Family == FamilyInet {
|
||||
families = []Family{FamilyIPv4, FamilyIPv6}
|
||||
@@ -144,118 +128,21 @@ func SameLocator(left, right Locator) bool {
|
||||
return left.Canonical != "" && left.Canonical == right.Canonical
|
||||
}
|
||||
|
||||
func MatchObservedByRuleKey(observed []ObservedRule, rule FirewallRule) ([]ObservedRule, error) {
|
||||
wanted, err := RuleKey(rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func LocateRule(snapshot RuleSet, locator *Locator) (ObservedRule, error) {
|
||||
if locator == nil || locator.Provider != snapshot.Scope.Provider || locator.ScopeKey != snapshot.Scope.Key() {
|
||||
return ObservedRule{}, ErrInvalidRule
|
||||
}
|
||||
matches := make([]ObservedRule, 0, 1)
|
||||
for _, candidate := range observed {
|
||||
if candidate.ParseStatus != ParseStatusSupported {
|
||||
continue
|
||||
var matches []ObservedRule
|
||||
for _, observed := range snapshot.Rules {
|
||||
if SameLocator(observed.Locator, *locator) {
|
||||
matches = append(matches, observed)
|
||||
}
|
||||
candidateKey, keyErr := RuleKey(candidate.Rule)
|
||||
if keyErr == nil && candidateKey == wanted {
|
||||
matches = append(matches, candidate)
|
||||
}
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func FindCommittedObserved(snapshot RuleSet, requested FirewallRule, plan CommandBatch) (ObservedRule, error) {
|
||||
if len(plan.Rules) == 1 && plan.Rules[0].Expected.Marker != "" {
|
||||
matches := make([]ObservedRule, 0, 1)
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.Marker == plan.Rules[0].Expected.Marker {
|
||||
matches = append(matches, observed)
|
||||
}
|
||||
}
|
||||
if len(matches) == 1 {
|
||||
return matches[0], nil
|
||||
}
|
||||
}
|
||||
matches, err := MatchObservedByRuleKey(snapshot.Rules, requested)
|
||||
if err != nil {
|
||||
return ObservedRule{}, err
|
||||
}
|
||||
if len(matches) != 1 {
|
||||
return ObservedRule{}, fmt.Errorf("%w: expected one committed rule, found %d", ErrVerificationFailed, len(matches))
|
||||
return ObservedRule{}, ErrRuleStale
|
||||
}
|
||||
if err := GuardMutation(matches[0]); err != nil {
|
||||
return ObservedRule{}, err
|
||||
}
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
func RulesOverlap(left, right FirewallRule) bool {
|
||||
left, leftErr := NormalizeRule(left)
|
||||
right, rightErr := NormalizeRule(right)
|
||||
if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() {
|
||||
return false
|
||||
}
|
||||
return (left.Scope.Family == FamilyInet || right.Scope.Family == FamilyInet || left.Scope.Family == right.Scope.Family) &&
|
||||
(left.Protocol == "all" || right.Protocol == "all" || left.Protocol == right.Protocol) &&
|
||||
addressesOverlap(left.SourceAddress, right.SourceAddress) &&
|
||||
addressesOverlap(left.DestinationAddress, right.DestinationAddress) &&
|
||||
portsOverlap(left.SourcePort, right.SourcePort) &&
|
||||
portsOverlap(left.DestinationPort, right.DestinationPort) &&
|
||||
(left.Interface == "" || right.Interface == "" || left.Interface == right.Interface)
|
||||
}
|
||||
|
||||
func addressesOverlap(left, right string) bool {
|
||||
if left == "" || right == "" {
|
||||
return true
|
||||
}
|
||||
leftPrefix, leftErr := netip.ParsePrefix(left)
|
||||
rightPrefix, rightErr := netip.ParsePrefix(right)
|
||||
if leftErr != nil || rightErr != nil {
|
||||
return false
|
||||
}
|
||||
return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr())
|
||||
}
|
||||
|
||||
func portsOverlap(left, right string) bool {
|
||||
if left == "" || right == "" {
|
||||
return true
|
||||
}
|
||||
leftIntervals, leftErr := portIntervals(left)
|
||||
rightIntervals, rightErr := portIntervals(right)
|
||||
if leftErr != nil || rightErr != nil {
|
||||
return false
|
||||
}
|
||||
for _, leftInterval := range leftIntervals {
|
||||
for _, rightInterval := range rightIntervals {
|
||||
if leftInterval[0] <= rightInterval[1] && rightInterval[0] <= leftInterval[1] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func portIntervals(value string) ([][2]int, error) {
|
||||
parts := strings.Split(value, ",")
|
||||
intervals := make([][2]int, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
start, end, err := portInterval(strings.TrimSpace(part))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
intervals = append(intervals, [2]int{start, end})
|
||||
}
|
||||
return intervals, nil
|
||||
}
|
||||
|
||||
func portInterval(value string) (int, int, error) {
|
||||
parts := strings.Split(value, "-")
|
||||
if len(parts) == 1 {
|
||||
port, err := strconv.Atoi(parts[0])
|
||||
return port, port, err
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, fmt.Errorf("invalid port interval %q", value)
|
||||
}
|
||||
start, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
end, err := strconv.Atoi(parts[1])
|
||||
return start, end, err
|
||||
}
|
||||
|
||||
@@ -49,10 +49,13 @@ type Adapter interface {
|
||||
CreateRules(context.Context, []Rule) error
|
||||
DeleteRules(context.Context, []Rule) error
|
||||
ReplaceRules(rules []Rule) error
|
||||
Enable() error
|
||||
Enable(...string) error
|
||||
OperateFamily(string, bool) error
|
||||
UnbindFamily(string) error
|
||||
Cleanup() error
|
||||
InitStatus() (bool, bool, error)
|
||||
FamilyStatus(family string) (bool, bool, error)
|
||||
FamilyState(family string) (bool, bool, bool, error)
|
||||
Replay() error
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/mattn/go-shellwords"
|
||||
@@ -29,33 +31,35 @@ type iptablesBackend interface {
|
||||
LoadIPv6RulesFromFile(table, chain, fileName string) error
|
||||
}
|
||||
|
||||
type systemIptablesBackend struct{}
|
||||
type systemIptablesBackend struct{ ctx context.Context }
|
||||
|
||||
func (systemIptablesBackend) IPv6Available() bool {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
return err == nil && commands.IPv6Available()
|
||||
}
|
||||
|
||||
func (systemIptablesBackend) Run(table string, args ...string) error {
|
||||
return iptables_helper.Run(table, args...)
|
||||
func (b systemIptablesBackend) Run(table string, args ...string) error {
|
||||
_, err := iptables_helper.RunWithStdContext(b.ctx, table, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (systemIptablesBackend) RunWithStd(table string, args ...string) (string, error) {
|
||||
func (b systemIptablesBackend) RunWithStd(table string, args ...string) (string, error) {
|
||||
if len(args) == 1 && args[0] == "-S" {
|
||||
return iptables_helper.ReadTable(context.Background(), table, false)
|
||||
return iptables_helper.ReadTable(b.ctx, table, false)
|
||||
}
|
||||
return iptables_helper.RunWithStd(table, args...)
|
||||
return iptables_helper.RunWithStdContext(b.ctx, table, args...)
|
||||
}
|
||||
|
||||
func (systemIptablesBackend) RunIPv6(table string, args ...string) error {
|
||||
return iptables_helper.RunIPv6(table, args...)
|
||||
func (b systemIptablesBackend) RunIPv6(table string, args ...string) error {
|
||||
_, err := iptables_helper.RunIPv6WithStdContext(b.ctx, table, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (string, error) {
|
||||
func (b systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (string, error) {
|
||||
if len(args) == 1 && args[0] == "-S" {
|
||||
return iptables_helper.ReadTable(context.Background(), table, true)
|
||||
return iptables_helper.ReadTable(b.ctx, table, true)
|
||||
}
|
||||
return iptables_helper.RunIPv6WithStd(table, args...)
|
||||
return iptables_helper.RunIPv6WithStdContext(b.ctx, table, args...)
|
||||
}
|
||||
|
||||
func (systemIptablesBackend) Restore(ctx context.Context, family, input string) error {
|
||||
@@ -76,7 +80,7 @@ func (systemIptablesBackend) Restore(ctx context.Context, family, input string)
|
||||
if err == nil && strings.TrimSpace(stderr.String()) != "" {
|
||||
err = fmt.Errorf("firewall command warning: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return firewallutil.WrapBatchCommandError(executable+" --noflush --wait", input, err)
|
||||
return errors.Join(firewallutil.WrapBatchCommandError(executable+" --noflush --wait", input, err), ctx.Err())
|
||||
}
|
||||
|
||||
func (systemIptablesBackend) LoadRulesFromFile(table, chain, fileName string) error {
|
||||
@@ -103,15 +107,17 @@ func (defaultForwardingSystem) WriteFile(name string, data []byte, perm os.FileM
|
||||
}
|
||||
|
||||
type Iptables struct {
|
||||
ctx context.Context
|
||||
provider string
|
||||
backend iptablesBackend
|
||||
system forwardingSystem
|
||||
}
|
||||
|
||||
func NewIptables(provider string) *Iptables {
|
||||
func NewIptables(ctx context.Context, provider string) *Iptables {
|
||||
return &Iptables{
|
||||
ctx: ctx,
|
||||
provider: provider,
|
||||
backend: systemIptablesBackend{},
|
||||
backend: systemIptablesBackend{ctx: ctx},
|
||||
system: defaultForwardingSystem{},
|
||||
}
|
||||
}
|
||||
@@ -130,6 +136,9 @@ func (l *Iptables) List() ([]Rule, error) {
|
||||
return rules, nil
|
||||
}
|
||||
stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-S")
|
||||
if errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
return rules, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err)
|
||||
}
|
||||
@@ -156,31 +165,25 @@ func (l *Iptables) ReplaceRules(rules []Rule) error {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if family == FamilyIPv6 {
|
||||
if len(byFamily[family]) > 0 {
|
||||
if err := ensureForwardingSysctls(l.system, true); err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
initialized, _, err := l.familyInitStatus(family)
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
if !initialized {
|
||||
continue
|
||||
}
|
||||
}
|
||||
initialized, _, err := l.FamilyStatus(family)
|
||||
if family == FamilyIPv6 && len(byFamily[family]) == 0 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
continue
|
||||
}
|
||||
if err := l.batchEnsureChains(family); err != nil {
|
||||
return err
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
if !initialized {
|
||||
if len(byFamily[family]) > 0 {
|
||||
failures = append(failures, fmt.Errorf("%s forwarding chains are not initialized", family))
|
||||
}
|
||||
continue
|
||||
}
|
||||
script, err := buildIptablesForwardScript(byFamily[family], OperationAdd, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.backend.Restore(context.Background(), family, script); err != nil {
|
||||
if err := l.backend.Restore(l.ctx, family, script); err != nil {
|
||||
return fmt.Errorf("restore %s forwarding rules: %w", family, err)
|
||||
}
|
||||
}
|
||||
@@ -199,17 +202,6 @@ func (l *Iptables) CreateRules(ctx context.Context, rules []Rule) error {
|
||||
if family == "" {
|
||||
family = FamilyIPv4
|
||||
}
|
||||
if family == FamilyIPv6 {
|
||||
if !l.backend.IPv6Available() {
|
||||
return fmt.Errorf("ip6tables command family is unavailable")
|
||||
}
|
||||
if err := ensureForwardingSysctls(l.system, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.batchEnsureChains(family); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return l.backend.Restore(ctx, family, script)
|
||||
}
|
||||
|
||||
@@ -310,14 +302,21 @@ func isRemoteTarget(family, target string) bool {
|
||||
return target != "" && target != "127.0.0.1" && target != "localhost"
|
||||
}
|
||||
|
||||
func (l *Iptables) Enable() error {
|
||||
if err := ensureForwardingSysctls(l.system, false); err != nil {
|
||||
func (l *Iptables) Enable(families ...string) error {
|
||||
if err := l.OperateFamily(FamilyIPv4, true); err != nil {
|
||||
return err
|
||||
}
|
||||
return l.batchEnsureChains(FamilyIPv4)
|
||||
if len(families) > 0 && !slices.Contains(families, FamilyIPv6) {
|
||||
return nil
|
||||
}
|
||||
initialized, _, err := l.FamilyStatus(FamilyIPv6)
|
||||
if err != nil || !initialized {
|
||||
return err
|
||||
}
|
||||
return l.OperateFamily(FamilyIPv6, false)
|
||||
}
|
||||
|
||||
func (l *Iptables) batchEnsureChains(family string) error {
|
||||
func (l *Iptables) batchEnsureChains(family string, initialize bool) error {
|
||||
list := l.backend.RunWithStd
|
||||
if family == FamilyIPv6 {
|
||||
list = l.backend.RunIPv6WithStd
|
||||
@@ -330,11 +329,16 @@ func (l *Iptables) batchEnsureChains(family string) error {
|
||||
}
|
||||
outputs[table] = output
|
||||
}
|
||||
if !initialize && (!containsExactLine(outputs[iptables_helper.NatTab], "-N "+ChainPreRouting) ||
|
||||
!containsExactLine(outputs[iptables_helper.NatTab], "-N "+ChainPostRouting) ||
|
||||
!containsExactLine(outputs[iptables_helper.FilterTab], "-N "+ChainForward)) {
|
||||
return fmt.Errorf("%s forwarding chains are not initialized", family)
|
||||
}
|
||||
script := buildIptablesForwardLifecycleScript(outputs, true)
|
||||
if script == "" {
|
||||
return nil
|
||||
}
|
||||
if err := l.backend.Restore(context.Background(), family, script); err != nil {
|
||||
if err := l.backend.Restore(l.ctx, family, script); err != nil {
|
||||
return fmt.Errorf("batch initialize %s forwarding chains: %w", family, err)
|
||||
}
|
||||
return nil
|
||||
@@ -359,7 +363,7 @@ func (l *Iptables) Cleanup() error {
|
||||
}
|
||||
script := buildIptablesForwardLifecycleScript(outputs, false)
|
||||
if script != "" {
|
||||
if err := l.backend.Restore(context.Background(), family, script); err != nil {
|
||||
if err := l.backend.Restore(l.ctx, family, script); err != nil {
|
||||
return fmt.Errorf("batch delete %s forwarding chains: %w", family, err)
|
||||
}
|
||||
}
|
||||
@@ -518,6 +522,9 @@ func (l *Iptables) InitStatus() (bool, bool, error) {
|
||||
return ipv4Init, ipv4Bind, nil
|
||||
}
|
||||
ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6)
|
||||
if errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
return ipv4Init, ipv4Bind, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
@@ -525,6 +532,14 @@ func (l *Iptables) InitStatus() (bool, bool, error) {
|
||||
}
|
||||
|
||||
func (l *Iptables) familyInitStatus(family string) (bool, bool, error) {
|
||||
initialized, bound, _, err := l.FamilyState(family)
|
||||
return initialized, bound, err
|
||||
}
|
||||
|
||||
func (l *Iptables) FamilyState(family string) (bool, bool, bool, error) {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
return false, false, false, nil
|
||||
}
|
||||
sysctlPath := "/proc/sys/net/ipv4/ip_forward"
|
||||
label := "IPv4"
|
||||
list := l.backend.RunWithStd
|
||||
@@ -535,27 +550,24 @@ func (l *Iptables) familyInitStatus(family string) (bool, bool, error) {
|
||||
}
|
||||
data, err := l.system.ReadFile(sysctlPath)
|
||||
if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) {
|
||||
return false, false, nil
|
||||
return false, false, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("read %s forwarding status: %w", label, err)
|
||||
return false, false, false, fmt.Errorf("read %s forwarding status: %w", label, err)
|
||||
}
|
||||
forwardingEnabled := strings.TrimSpace(string(data)) != "0"
|
||||
natRules, err := list(iptables_helper.NatTab, "-S")
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err)
|
||||
return false, false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err)
|
||||
}
|
||||
natInit, natBind := checkInitAndBind(
|
||||
[]string{"-N " + ChainPreRouting, "-N " + ChainPostRouting},
|
||||
[]string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting},
|
||||
strings.Split(natRules, "\n"),
|
||||
)
|
||||
if !natInit {
|
||||
return false, false, nil
|
||||
}
|
||||
filterRules, err := list(iptables_helper.FilterTab, "-S")
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err)
|
||||
return false, false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err)
|
||||
}
|
||||
filterInit, _ := checkInitAndBind(
|
||||
[]string{"-N " + ChainForward},
|
||||
@@ -563,7 +575,9 @@ func (l *Iptables) familyInitStatus(family string) (bool, bool, error) {
|
||||
strings.Split(filterRules, "\n"),
|
||||
)
|
||||
filterBind := forwardBindingEffective(filterRules)
|
||||
return natInit && filterInit, forwardingEnabled && natBind && filterInit && filterBind, nil
|
||||
initialized := natInit && filterInit
|
||||
present := containsExactLine(natRules, "-N "+ChainPreRouting) || containsExactLine(natRules, "-N "+ChainPostRouting) || containsExactLine(filterRules, "-N "+ChainForward)
|
||||
return initialized, forwardingEnabled && natBind && filterInit && filterBind, present && !initialized, nil
|
||||
}
|
||||
|
||||
func (l *Iptables) FamilyStatus(family string) (bool, bool, error) {
|
||||
@@ -601,7 +615,7 @@ func (l *Iptables) Replay() error {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
continue
|
||||
}
|
||||
if err := l.batchEnsureChains(family); err != nil {
|
||||
if err := l.batchEnsureChains(family, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -726,3 +740,59 @@ func loadProtocol(protocol string) string {
|
||||
return protocol
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Iptables) OperateFamily(family string, initialize bool) error {
|
||||
if family != FamilyIPv4 && family != FamilyIPv6 {
|
||||
return fmt.Errorf("unsupported forwarding family %q", family)
|
||||
}
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
return fmt.Errorf("ip6tables command family is unavailable")
|
||||
}
|
||||
if !initialize {
|
||||
initialized, _, err := l.FamilyStatus(family)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !initialized {
|
||||
return fmt.Errorf("%s forwarding chains are not initialized", family)
|
||||
}
|
||||
}
|
||||
if err := ensureFamilyForwardingSysctls(l.system, family); err != nil {
|
||||
return err
|
||||
}
|
||||
return l.batchEnsureChains(family, initialize)
|
||||
}
|
||||
|
||||
func (l *Iptables) UnbindFamily(family string) error {
|
||||
if family != FamilyIPv4 && family != FamilyIPv6 {
|
||||
return fmt.Errorf("unsupported forwarding family %q", family)
|
||||
}
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
return nil
|
||||
}
|
||||
read := l.backend.RunWithStd
|
||||
if family == FamilyIPv6 {
|
||||
read = l.backend.RunIPv6WithStd
|
||||
}
|
||||
var script strings.Builder
|
||||
for _, table := range []string{iptables_helper.NatTab, iptables_helper.FilterTab} {
|
||||
output, err := read(table, "-S")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var removals []string
|
||||
for _, pair := range [][2]string{{"PREROUTING", ChainPreRouting}, {"POSTROUTING", ChainPostRouting}, {"FORWARD", ChainForward}} {
|
||||
rule := "-A " + pair[0] + " -j " + pair[1]
|
||||
for range countExactLines(output, rule) {
|
||||
removals = append(removals, "-D "+pair[0]+" -j "+pair[1])
|
||||
}
|
||||
}
|
||||
if len(removals) > 0 {
|
||||
script.WriteString("*" + table + "\n" + strings.Join(removals, "\n") + "\nCOMMIT\n")
|
||||
}
|
||||
}
|
||||
if script.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.backend.Restore(l.ctx, family, script.String())
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package forwarding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
)
|
||||
|
||||
@@ -24,10 +25,13 @@ const (
|
||||
nftForwardMarker = "1panel-forward:"
|
||||
)
|
||||
|
||||
type Nftables struct{ system forwardingSystem }
|
||||
type Nftables struct {
|
||||
ctx context.Context
|
||||
system forwardingSystem
|
||||
}
|
||||
|
||||
func NewNftables() *Nftables {
|
||||
return &Nftables{system: defaultForwardingSystem{}}
|
||||
func NewNftables(ctx context.Context) *Nftables {
|
||||
return &Nftables{ctx: ctx, system: defaultForwardingSystem{}}
|
||||
}
|
||||
|
||||
func (n *Nftables) Name() string { return "nftables" }
|
||||
@@ -35,8 +39,8 @@ func (n *Nftables) Name() string { return "nftables" }
|
||||
func (n *Nftables) List() ([]Rule, error) {
|
||||
rules := make([]Rule, 0)
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, "NFT_"+ChainPreRouting)
|
||||
if errors.Is(err, nftables_helper.ErrChainNotFound) {
|
||||
stdout, err := nftables_helper.ReadChain(n.run, nftTableFamily(family), nftForwardTable, "NFT_"+ChainPreRouting)
|
||||
if errors.Is(err, nftables_helper.ErrChainNotFound) || (family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable)) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
@@ -58,31 +62,25 @@ func (n *Nftables) ReplaceRules(rules []Rule) error {
|
||||
}
|
||||
var failures []error
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
if family == FamilyIPv6 {
|
||||
if len(byFamily[family]) > 0 {
|
||||
if err := ensureForwardingSysctls(n.system, true); err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
_, exists, err := nftables_helper.ReadTable(nftRun, nftTableFamily(family), nftForwardTable)
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
}
|
||||
initialized, _, err := n.FamilyStatus(family)
|
||||
if family == FamilyIPv6 && len(byFamily[family]) == 0 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
continue
|
||||
}
|
||||
if err := ensureNftForwardTables(family); err != nil {
|
||||
return fmt.Errorf("initialize nftables forwarding table: %w", err)
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
if !initialized {
|
||||
if len(byFamily[family]) > 0 {
|
||||
failures = append(failures, fmt.Errorf("%s forwarding chains are not initialized", family))
|
||||
}
|
||||
continue
|
||||
}
|
||||
commands, err := rebuildNftForwardCommands(byFamily[family], family)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := nftRunCommands(context.Background(), commands); err != nil {
|
||||
if err := nftRunCommands(n.ctx, commands); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -97,17 +95,6 @@ func (n *Nftables) CreateRules(ctx context.Context, rules []Rule) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if strings.EqualFold(strings.TrimSpace(rule.Family), FamilyIPv6) {
|
||||
if err := ensureForwardingSysctls(n.system, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureNftForwardTables(FamilyIPv6); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nftRunCommands(ctx, commands)
|
||||
}
|
||||
|
||||
@@ -154,27 +141,35 @@ func (n *Nftables) DeleteRules(ctx context.Context, rules []Rule) error {
|
||||
return nftRunCommands(ctx, commands)
|
||||
}
|
||||
|
||||
func (n *Nftables) Enable() error {
|
||||
if err := ensureForwardingSysctls(n.system, false); err != nil {
|
||||
func (n *Nftables) Enable(families ...string) error {
|
||||
if err := n.OperateFamily(FamilyIPv4, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureNftForwardTables(FamilyIPv4); err != nil {
|
||||
return fmt.Errorf("initialize nftables forwarding table: %w", err)
|
||||
if len(families) > 0 && !slices.Contains(families, FamilyIPv6) {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
initialized, _, err := n.FamilyStatus(FamilyIPv6)
|
||||
if err != nil || !initialized {
|
||||
return err
|
||||
}
|
||||
return n.OperateFamily(FamilyIPv6, false)
|
||||
}
|
||||
|
||||
func (n *Nftables) Cleanup() error {
|
||||
commands := make([][]string, 0, 2)
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
tableFamily := nftTableFamily(family)
|
||||
if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil {
|
||||
_, exists, err := nftables_helper.ReadTable(n.run, tableFamily, nftForwardTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
commands = append(commands, []string{"delete", "table", tableFamily, nftForwardTable})
|
||||
}
|
||||
if len(commands) > 0 {
|
||||
if err := nftRunCommands(context.Background(), commands); err != nil {
|
||||
if err := nftRunCommands(n.ctx, commands); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -189,6 +184,9 @@ func (n *Nftables) InitStatus() (bool, bool, error) {
|
||||
var anyInitialized, anyBound bool
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
initialized, bound, err := n.FamilyStatus(family)
|
||||
if family == FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
@@ -199,31 +197,37 @@ func (n *Nftables) InitStatus() (bool, bool, error) {
|
||||
}
|
||||
|
||||
func (n *Nftables) FamilyStatus(family string) (bool, bool, error) {
|
||||
initialized, bound, _, err := n.FamilyState(family)
|
||||
return initialized, bound, err
|
||||
}
|
||||
|
||||
func (n *Nftables) FamilyState(family string) (bool, bool, bool, error) {
|
||||
sysctlPath := "/proc/sys/net/ipv4/ip_forward"
|
||||
if family == FamilyIPv6 {
|
||||
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
|
||||
}
|
||||
data, err := n.system.ReadFile(sysctlPath)
|
||||
if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) {
|
||||
return false, false, nil
|
||||
return false, false, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("read %s forwarding status: %w", family, err)
|
||||
return false, false, false, fmt.Errorf("read %s forwarding status: %w", family, err)
|
||||
}
|
||||
output, exists, err := nftables_helper.ReadTable(nftRun, nftTableFamily(family), nftForwardTable)
|
||||
output, exists, err := nftables_helper.ReadTable(n.run, nftTableFamily(family), nftForwardTable)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
return false, false, false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, false, nil
|
||||
return false, false, false, nil
|
||||
}
|
||||
chains := nftables_helper.ParseTableChains(output)
|
||||
count := 0
|
||||
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
|
||||
if _, exists := chains["NFT_"+chain]; !exists {
|
||||
return false, false, nil
|
||||
if _, exists := chains["NFT_"+chain]; exists {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return true, strings.TrimSpace(string(data)) != "0", nil
|
||||
return count == 3, count == 3 && !nftables_helper.TableDormant(output) && strings.TrimSpace(string(data)) != "0", count > 0 && count < 3, nil
|
||||
}
|
||||
|
||||
func (n *Nftables) Replay() error {
|
||||
@@ -235,21 +239,21 @@ func (n *Nftables) Replay() error {
|
||||
}
|
||||
allPresent := true
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
if _, err := nftRun("list", "table", nftTableFamily(family), nftForwardTable); err != nil {
|
||||
if _, err := n.run("list", "table", nftTableFamily(family), nftForwardTable); err != nil {
|
||||
allPresent = false
|
||||
}
|
||||
}
|
||||
if allPresent {
|
||||
return nil
|
||||
}
|
||||
return nftRunCommand("-f", file)
|
||||
return n.runCommand("-f", file)
|
||||
}
|
||||
|
||||
func ensureNftForwardTables(families ...string) error {
|
||||
func (n *Nftables) ensureTables(families ...string) error {
|
||||
commands := make([][]string, 0, 8)
|
||||
for _, family := range families {
|
||||
tableFamily := nftTableFamily(family)
|
||||
output, tableExists, err := nftables_helper.ReadTable(nftRun, tableFamily, nftForwardTable)
|
||||
output, tableExists, err := nftables_helper.ReadTable(n.run, tableFamily, nftForwardTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -277,7 +281,7 @@ func ensureNftForwardTables(families ...string) error {
|
||||
if len(commands) == 0 {
|
||||
return nil
|
||||
}
|
||||
return nftRunCommands(context.Background(), commands)
|
||||
return nftRunCommands(n.ctx, commands)
|
||||
}
|
||||
|
||||
func rebuildNftForwardCommands(rules []Rule, family string) ([][]string, error) {
|
||||
@@ -350,13 +354,6 @@ func decodeNftForwardRule(value string) (Rule, bool) {
|
||||
return Rule{}, false
|
||||
}
|
||||
value = strings.TrimPrefix(value, nftForwardMarker)
|
||||
if strings.HasPrefix(value, "v2|") {
|
||||
return decodeCompactNftForwardRule(value)
|
||||
}
|
||||
return decodeLegacyNftForwardRule(value)
|
||||
}
|
||||
|
||||
func decodeCompactNftForwardRule(value string) (Rule, bool) {
|
||||
parts := strings.Split(value, "|")
|
||||
if len(parts) != 7 || parts[0] != "v2" {
|
||||
return Rule{}, false
|
||||
@@ -383,22 +380,6 @@ func decodeCompactNftForwardRule(value string) (Rule, bool) {
|
||||
}, true
|
||||
}
|
||||
|
||||
func decodeLegacyNftForwardRule(value string) (Rule, bool) {
|
||||
parts := strings.Split(value, ".")
|
||||
if len(parts) != 6 {
|
||||
return Rule{}, false
|
||||
}
|
||||
decoded := make([]string, len(parts))
|
||||
for index, part := range parts {
|
||||
data, err := base64.RawURLEncoding.DecodeString(part)
|
||||
if err != nil {
|
||||
return Rule{}, false
|
||||
}
|
||||
decoded[index] = string(data)
|
||||
}
|
||||
return Rule{Family: decoded[0], Protocol: decoded[1], Port: decoded[2], TargetIP: decoded[3], TargetPort: decoded[4], Interface: decoded[5]}, true
|
||||
}
|
||||
|
||||
func parseNftForwardRules(stdout string) []Rule {
|
||||
result := make([]Rule, 0)
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
@@ -422,16 +403,17 @@ func parseNftForwardRules(stdout string) []Rule {
|
||||
return result
|
||||
}
|
||||
|
||||
func nftRun(args ...string) (string, error) {
|
||||
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...)
|
||||
func (n *Nftables) run(args ...string) (string, error) {
|
||||
stdout, err := cmd.NewCommandMgr(cmd.WithContext(n.ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...)
|
||||
err = errors.Join(err, n.ctx.Err())
|
||||
if err != nil {
|
||||
return stdout, fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err)
|
||||
}
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
func nftRunCommand(args ...string) error {
|
||||
err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudo("nft", args...)
|
||||
func (n *Nftables) runCommand(args ...string) error {
|
||||
err := cmd.NewCommandMgr(cmd.WithContext(n.ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudo("nft", args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err)
|
||||
}
|
||||
@@ -448,7 +430,7 @@ func nftRunCommands(ctx context.Context, commands [][]string) error {
|
||||
if err == nil && strings.TrimSpace(stderr.String()) != "" {
|
||||
err = fmt.Errorf("firewall command warning: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return firewallutil.WrapBatchCommandError("nft -f -", script, err)
|
||||
return errors.Join(firewallutil.WrapBatchCommandError("nft -f -", script, err), ctx.Err())
|
||||
}
|
||||
|
||||
func nftCommandsScript(commands [][]string) (string, error) {
|
||||
@@ -467,3 +449,34 @@ func nftCommandsScript(commands [][]string) (string, error) {
|
||||
}
|
||||
return script.String(), nil
|
||||
}
|
||||
|
||||
func (n *Nftables) OperateFamily(family string, initialize bool) error {
|
||||
if family != FamilyIPv4 && family != FamilyIPv6 {
|
||||
return fmt.Errorf("unsupported forwarding family %q", family)
|
||||
}
|
||||
if !initialize {
|
||||
initialized, _, err := n.FamilyStatus(family)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !initialized {
|
||||
return fmt.Errorf("%s forwarding chains are not initialized", family)
|
||||
}
|
||||
}
|
||||
if err := ensureFamilyForwardingSysctls(n.system, family); err != nil {
|
||||
return err
|
||||
}
|
||||
if initialize {
|
||||
if err := n.ensureTables(family); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nftRunCommands(n.ctx, [][]string{{"add", "table", nftTableFamily(family), nftForwardTable}})
|
||||
}
|
||||
|
||||
func (n *Nftables) UnbindFamily(family string) error {
|
||||
if family != FamilyIPv4 && family != FamilyIPv6 {
|
||||
return fmt.Errorf("unsupported forwarding family %q", family)
|
||||
}
|
||||
return nftables_helper.SetTableDormant(n.ctx, nftTableFamily(family), nftForwardTable)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forwarding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -8,23 +10,32 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/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 {
|
||||
interfaces, err := IPv6RAInterfaces(system.ReadFile)
|
||||
interfaces, err := IPv6RAInterfaces(system.ReadFile, LoadDockerIPv4BridgePorts)
|
||||
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 {
|
||||
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"}
|
||||
if withIPv6 {
|
||||
paths = append(paths, "/proc/sys/net/ipv6/conf/all/forwarding")
|
||||
paths = []string{"/proc/sys/net/ipv6/conf/all/forwarding"}
|
||||
}
|
||||
for _, path := range paths {
|
||||
if err := system.WriteFile(path, []byte("1"), constant.FilePerm); err != nil {
|
||||
@@ -35,14 +46,14 @@ func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error {
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to read /etc/sysctl.conf: %w", err)
|
||||
}
|
||||
content := enableForwardingSysctls(string(data), withIPv6)
|
||||
content := enableFamilyForwardingSysctls(string(data), family)
|
||||
if err := system.WriteFile("/etc/sysctl.conf", []byte(content), constant.FilePerm); err != nil {
|
||||
return fmt.Errorf("failed to persist IP forwarding: %w", err)
|
||||
}
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,21 +77,16 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, exists := globalAddresses[name]; !exists {
|
||||
globalAddresses[name] = false
|
||||
}
|
||||
if fields[3] == "00" {
|
||||
globalAddresses[name] = true
|
||||
if flags&permanentAddress == 0 {
|
||||
raInterfaces[name] = true
|
||||
}
|
||||
globalAddresses[name] = globalAddresses[name] || fields[3] == "00"
|
||||
if fields[3] == "00" && flags&permanentAddress == 0 {
|
||||
raInterfaces[name] = true
|
||||
}
|
||||
}
|
||||
routes, err := readFile("/proc/net/ipv6_route")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const raRouteFlags = 0x00010000 | 0x00800000
|
||||
const raRouteFlags = 0x00010000 | 0x00080000 | 0x00800000
|
||||
for _, line := range strings.Split(string(routes), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
@@ -97,12 +103,15 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
|
||||
raInterfaces[fields[9]] = true
|
||||
}
|
||||
}
|
||||
uncertainInterfaces := make(map[string]bool)
|
||||
for name, hasGlobalAddress := range globalAddresses {
|
||||
if !hasGlobalAddress {
|
||||
if !hasGlobalAddress && !raInterfaces[name] {
|
||||
raInterfaces[name] = true
|
||||
uncertainInterfaces[name] = true
|
||||
}
|
||||
}
|
||||
var interfaces []string
|
||||
checkDockerPorts := false
|
||||
for name := range raInterfaces {
|
||||
if name == "lo" {
|
||||
continue
|
||||
@@ -117,20 +126,109 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
|
||||
switch strings.TrimSpace(string(value)) {
|
||||
case "1":
|
||||
interfaces = append(interfaces, name)
|
||||
checkDockerPorts = checkDockerPorts || uncertainInterfaces[name]
|
||||
case "0", "2":
|
||||
default:
|
||||
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)
|
||||
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")
|
||||
wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"}
|
||||
if withIPv6 {
|
||||
wanted["net.ipv6.conf.all.forwarding"] = "net.ipv6.conf.all.forwarding = 1"
|
||||
if family == FamilyIPv6 {
|
||||
wanted = map[string]string{"net.ipv6.conf.all.forwarding": "net.ipv6.conf.all.forwarding = 1"}
|
||||
}
|
||||
found := make(map[string]bool, len(wanted))
|
||||
for index, line := range lines {
|
||||
|
||||
@@ -9,18 +9,39 @@ import (
|
||||
)
|
||||
|
||||
func LoadInitStatus(tab string) (bool, bool, error) {
|
||||
return loadInitStatus(tab, RunWithStd, true)
|
||||
return loadInitStatus(tab, RunWithStd)
|
||||
}
|
||||
|
||||
func LoadFamilyInitStatus(family, tab string) (bool, bool, error) {
|
||||
initialized, bound, _, err := LoadFamilyState(family, tab)
|
||||
return initialized, bound, err
|
||||
}
|
||||
|
||||
func LoadFamilyState(family, tab string) (bool, bool, bool, error) {
|
||||
runner := RunWithStd
|
||||
switch family {
|
||||
case constant.FirewallFamilyIPv4:
|
||||
return loadInitStatus(tab, RunWithStd, true)
|
||||
case constant.FirewallFamilyIPv6:
|
||||
return loadInitStatus(tab, RunIPv6WithStd, true)
|
||||
runner = RunIPv6WithStd
|
||||
default:
|
||||
return false, false, fmt.Errorf("unsupported iptables family %q", family)
|
||||
return false, false, false, fmt.Errorf("unsupported iptables family %q", family)
|
||||
}
|
||||
if tab != "base" {
|
||||
return false, false, false, nil
|
||||
}
|
||||
output, err := runner(FilterTab, "-S")
|
||||
if err != nil {
|
||||
return false, false, false, err
|
||||
}
|
||||
count := 0
|
||||
for _, chain := range BasicChains() {
|
||||
if containsIptablesRule(output, "-N "+chain) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
initialized := count == len(BasicChains())
|
||||
_, bound := checkWithInitAndBind([]string{"-N " + BasicBeforeChain, "-N " + BasicChain, "-N " + BasicAfterChain}, []string{"-A INPUT -j " + BasicBeforeChain, "-A INPUT -j " + BasicChain, "-A INPUT -j " + BasicAfterChain}, strings.Split(output, "\n"))
|
||||
return initialized, bound, count > 0 && !initialized, nil
|
||||
}
|
||||
|
||||
func LoadFamilyBindStatus(family string) (bool, error) {
|
||||
@@ -54,7 +75,7 @@ func hasBaseChainBinding(output string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func loadInitStatus(tab string, runner func(string, ...string) (string, error), requireTerminalRules bool) (bool, bool, error) {
|
||||
func loadInitStatus(tab string, runner func(string, ...string) (string, error)) (bool, bool, error) {
|
||||
switch tab {
|
||||
case "base":
|
||||
filterRules, err := runner(FilterTab, "-S")
|
||||
@@ -66,14 +87,6 @@ func loadInitStatus(tab string, runner func(string, ...string) (string, error),
|
||||
"-N " + BasicBeforeChain,
|
||||
"-N " + BasicChain,
|
||||
"-N " + BasicAfterChain,
|
||||
fmt.Sprintf("-A %s %s -j ACCEPT", BasicBeforeChain, strings.ReplaceAll(strings.ReplaceAll(IoRuleIn, "'", "\""), " -j ACCEPT", "")),
|
||||
fmt.Sprintf("-A %s %s -j ACCEPT", BasicBeforeChain, strings.ReplaceAll(strings.ReplaceAll(EstablishedRule, "'", "\""), " -j ACCEPT", "")),
|
||||
}
|
||||
if requireTerminalRules {
|
||||
initRules = append(initRules,
|
||||
fmt.Sprintf("-A %s %s", BasicAfterChain, DropAllTcp),
|
||||
fmt.Sprintf("-A %s %s", BasicAfterChain, DropAllUdp),
|
||||
)
|
||||
}
|
||||
bindRules := []string{
|
||||
fmt.Sprintf("-A %s -j %s", InputChain, BasicBeforeChain),
|
||||
@@ -88,15 +101,9 @@ func loadInitStatus(tab string, runner func(string, ...string) (string, error),
|
||||
}
|
||||
|
||||
func checkWithInitAndBind(initRules, bindRules []string, lines []string) (bool, bool) {
|
||||
output := strings.Join(lines, "\n")
|
||||
for _, rule := range initRules {
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == strings.TrimSpace(rule) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if !containsIptablesRule(output, rule) {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Debugf("not found init rule: %s", rule)
|
||||
}
|
||||
|
||||
@@ -52,8 +52,9 @@ func BindIPv6BaseChains() error {
|
||||
}
|
||||
|
||||
func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelist, output string) (string, error) {
|
||||
existing := iptablesRuleIndex(output)
|
||||
for _, chain := range BasicChains() {
|
||||
if !containsIptablesRule(output, "-N "+chain) {
|
||||
if !existing[canonicalIptablesRule("-N "+chain)] {
|
||||
return buildBaseChainsRestoreScript(dir, true, ports...)
|
||||
}
|
||||
}
|
||||
@@ -64,7 +65,9 @@ func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelis
|
||||
var script strings.Builder
|
||||
script.WriteString("*filter\n")
|
||||
for _, rule := range defaults {
|
||||
if !containsIptablesRule(output, rule) {
|
||||
key := canonicalIptablesRule(rule)
|
||||
if !existing[key] {
|
||||
existing[key] = true
|
||||
script.WriteString(rule + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package iptables_helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
const (
|
||||
LegacyInputChain = "1PANEL_INPUT"
|
||||
LegacyOutputChain = "1PANEL_OUTPUT"
|
||||
legacyInputFileName = "1panel_input.rules"
|
||||
legacyOutputFileName = "1panel_out.rules"
|
||||
)
|
||||
|
||||
func CleanupLegacyAdvancedChains(ctx context.Context) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := RunWithStdContext(ctx, FilterTab, "-S")
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect legacy iptables advanced chains: %w", err)
|
||||
}
|
||||
if script := buildLegacyAdvancedChainCleanupScript(output); script != "" {
|
||||
if err := restoreRules(commands.Restore4, script); err != nil {
|
||||
return fmt.Errorf("remove legacy iptables advanced chains: %w", err)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{legacyInputFileName, legacyOutputFileName} {
|
||||
file := filepath.Join(global.Dir.FirewallDir, name)
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("remove legacy iptables rules file %s: %w", file, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildLegacyAdvancedChainCleanupScript(output string) string {
|
||||
legacyChains := map[string]struct{}{LegacyInputChain: {}, LegacyOutputChain: {}}
|
||||
existing := make(map[string]bool, len(legacyChains))
|
||||
deletions := make([]string, 0)
|
||||
for _, raw := range strings.Split(output, "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
fields, err := shellwords.Parse(line)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(fields) == 2 && fields[0] == "-N" {
|
||||
if _, legacy := legacyChains[fields[1]]; legacy {
|
||||
existing[fields[1]] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(fields) < 4 || fields[0] != "-A" {
|
||||
continue
|
||||
}
|
||||
for index := 2; index+1 < len(fields); index++ {
|
||||
if fields[index] != "-j" && fields[index] != "-g" {
|
||||
continue
|
||||
}
|
||||
if _, legacy := legacyChains[fields[index+1]]; legacy {
|
||||
deletions = append(deletions, strings.Replace(line, "-A ", "-D ", 1))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, chain := range []string{LegacyInputChain, LegacyOutputChain} {
|
||||
if existing[chain] {
|
||||
deletions = append(deletions, "-F "+chain, "-X "+chain)
|
||||
}
|
||||
}
|
||||
if len(deletions) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "*filter\n" + strings.Join(deletions, "\n") + "\nCOMMIT\n"
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -253,7 +254,7 @@ func saveBaseChainsFamily(ipv6 bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
|
||||
func RestoreBaseChains(requiredPorts []firewall.PortWhitelist, families ...string) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -268,7 +269,7 @@ func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
|
||||
if err := restoreRules(commands.Restore4, input); err != nil {
|
||||
return fmt.Errorf("batch restore IPv4 base chains: %w", err)
|
||||
}
|
||||
if !commands.IPv6Available() {
|
||||
if !commands.IPv6Available() || (len(families) > 0 && !slices.Contains(families, constant.FirewallFamilyIPv6)) {
|
||||
return nil
|
||||
}
|
||||
if err := ensureBaseChainsFamily(true); err != nil {
|
||||
@@ -285,6 +286,7 @@ func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
|
||||
}
|
||||
|
||||
func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) {
|
||||
existing := make(map[string]bool)
|
||||
var script strings.Builder
|
||||
script.WriteString("*filter\n")
|
||||
for _, chain := range BasicChains() {
|
||||
@@ -314,6 +316,7 @@ func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts .
|
||||
if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") {
|
||||
continue
|
||||
}
|
||||
existing[canonicalIptablesRule(line)] = true
|
||||
script.WriteString(line)
|
||||
script.WriteByte('\n')
|
||||
}
|
||||
@@ -327,7 +330,9 @@ func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts .
|
||||
return "", err
|
||||
}
|
||||
for _, rule := range defaults {
|
||||
if !containsIptablesRule(script.String(), rule) {
|
||||
key := canonicalIptablesRule(rule)
|
||||
if !existing[key] {
|
||||
existing[key] = true
|
||||
if strings.HasPrefix(rule, "-A "+BasicBeforeChain+" ") && strings.Contains(rule, " --dport ") {
|
||||
rule = strings.Replace(rule, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1)
|
||||
}
|
||||
@@ -419,23 +424,29 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
|
||||
}
|
||||
|
||||
func buildRequiredPortsRestoreScript(desired []firewall.SystemPort, family string, beforeRaw, afterRaw string, includeDefaults bool) string {
|
||||
existing := iptablesRuleIndex(beforeRaw + "\n" + afterRaw)
|
||||
var commands []string
|
||||
for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} {
|
||||
if !containsIptablesRule(beforeRaw, line) {
|
||||
key := canonicalIptablesRule(line)
|
||||
if !existing[key] {
|
||||
existing[key] = true
|
||||
commands = append(commands, line)
|
||||
}
|
||||
}
|
||||
for _, rule := range desired {
|
||||
line := iptablesSystemPortRuleLine(rule)
|
||||
if rule.Family == family && !containsIptablesRule(beforeRaw, line) {
|
||||
key := canonicalIptablesRule(line)
|
||||
if rule.Family == family && !existing[key] {
|
||||
existing[key] = true
|
||||
commands = append(commands, strings.Replace(line, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1))
|
||||
beforeRaw += "\n" + line
|
||||
}
|
||||
}
|
||||
if includeDefaults {
|
||||
for _, rule := range []string{DropAllTcp, DropAllUdp} {
|
||||
line := "-A " + BasicAfterChain + " " + rule
|
||||
if !containsIptablesRule(afterRaw, line) {
|
||||
key := canonicalIptablesRule(line)
|
||||
if !existing[key] {
|
||||
existing[key] = true
|
||||
commands = append(commands, line)
|
||||
}
|
||||
}
|
||||
@@ -459,35 +470,101 @@ func containsIptablesRule(output, rule string) bool {
|
||||
}
|
||||
|
||||
func countIptablesRule(output, rule string) int {
|
||||
canonical := func(value string) string {
|
||||
fields, err := shellwords.Parse(value)
|
||||
if err != nil || len(fields)%2 != 0 {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
var options []string
|
||||
for index := 0; index < len(fields); index += 2 {
|
||||
key, value := fields[index], fields[index+1]
|
||||
if key == "--comment" || key == "-m" && (value == "comment" || value == "tcp" || value == "udp") {
|
||||
continue
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(value); err == nil {
|
||||
prefix = prefix.Masked()
|
||||
value = prefix.String()
|
||||
if prefix.Bits() == prefix.Addr().BitLen() {
|
||||
value = prefix.Addr().String()
|
||||
}
|
||||
}
|
||||
options = append(options, key+" "+value)
|
||||
}
|
||||
sort.Strings(options)
|
||||
return strings.Join(options, " ")
|
||||
}
|
||||
rule = canonical(rule)
|
||||
rule = canonicalIptablesRule(rule)
|
||||
count := 0
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
if canonical(line) == rule {
|
||||
if canonicalIptablesRule(line) == rule {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func canonicalIptablesRule(value string) string {
|
||||
fields, err := shellwords.Parse(value)
|
||||
if err != nil || len(fields)%2 != 0 {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
var options []string
|
||||
for index := 0; index < len(fields); index += 2 {
|
||||
key, value := fields[index], fields[index+1]
|
||||
if key == "--comment" || key == "-m" && (value == "comment" || value == "tcp" || value == "udp") {
|
||||
continue
|
||||
}
|
||||
if key == "-m" && value == "state" {
|
||||
value = "conntrack"
|
||||
}
|
||||
if key == "--state" || key == "--ctstate" {
|
||||
key = "--ctstate"
|
||||
states := strings.Split(strings.ToUpper(value), ",")
|
||||
sort.Strings(states)
|
||||
value = strings.Join(states, ",")
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(value); err == nil {
|
||||
prefix = prefix.Masked()
|
||||
value = prefix.String()
|
||||
if prefix.Bits() == prefix.Addr().BitLen() {
|
||||
value = prefix.Addr().String()
|
||||
}
|
||||
}
|
||||
options = append(options, key+" "+value)
|
||||
}
|
||||
sort.Strings(options)
|
||||
return strings.Join(options, " ")
|
||||
}
|
||||
|
||||
func iptablesRuleIndex(output string) map[string]bool {
|
||||
rules := make(map[string]bool)
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
rules[canonicalIptablesRule(line)] = true
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func OperateFamily(family string, initialize bool, ports []firewall.PortWhitelist) error {
|
||||
if family != constant.FirewallFamilyIPv4 && family != constant.FirewallFamilyIPv6 {
|
||||
return fmt.Errorf("unsupported iptables family %q", family)
|
||||
}
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipv6 := family == constant.FirewallFamilyIPv6
|
||||
read, executable := RunWithStd, commands.Restore4
|
||||
if ipv6 {
|
||||
if !commands.IPv6Available() {
|
||||
return fmt.Errorf("ip6tables and ip6tables-restore are required")
|
||||
}
|
||||
read, executable = RunIPv6WithStd, commands.Restore6
|
||||
}
|
||||
output, err := read(FilterTab, "-S")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := make([]string, 0)
|
||||
for _, chain := range BasicChains() {
|
||||
if !containsIptablesRule(output, "-N "+chain) {
|
||||
if !initialize {
|
||||
return fmt.Errorf("%s chain %s is not initialized", family, chain)
|
||||
}
|
||||
lines = append(lines, "-N "+chain)
|
||||
}
|
||||
}
|
||||
if initialize {
|
||||
defaults, err := baseDefaultRules(ports, family)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing := iptablesRuleIndex(output)
|
||||
for _, rule := range defaults {
|
||||
if !existing[canonicalIptablesRule(rule)] {
|
||||
lines = append(lines, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
lines = append(lines, baseChainBindingCommands(output, true)...)
|
||||
if err := restoreRules(executable, "*filter\n"+strings.Join(lines, "\n")+"\nCOMMIT\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
return saveBaseChainsFamily(ipv6)
|
||||
}
|
||||
|
||||
@@ -14,12 +14,15 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
)
|
||||
|
||||
func RepairBaseChains(ports []firewall.PortWhitelist) error {
|
||||
func RepairBaseChains(ports []firewall.PortWhitelist, families ...string) error {
|
||||
if len(families) == 0 {
|
||||
families = []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}
|
||||
}
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
||||
for _, family := range families {
|
||||
ipv6 := family == constant.FirewallFamilyIPv6
|
||||
run, executable := RunWithStd, commands.Restore4
|
||||
if ipv6 {
|
||||
|
||||
@@ -21,7 +21,11 @@ func Cleanup() error {
|
||||
commands := make([][]string, 0, 2)
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
tableFamily := TableFamily(family)
|
||||
if _, err := run("list", "table", tableFamily, TableName); err != nil {
|
||||
_, exists, err := ReadTable(run, tableFamily, TableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
commands = append(commands, []string{"delete", "table", tableFamily, TableName})
|
||||
@@ -64,9 +68,12 @@ func enableBase(prepare bool, requiredPorts []firewall.PortWhitelist) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureBaseChains() error {
|
||||
func ensureBaseChains(families ...filter.Family) error {
|
||||
if len(families) == 0 {
|
||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
}
|
||||
commands := make([][]string, 0, 10)
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
for _, family := range families {
|
||||
tableFamily := TableFamily(family)
|
||||
output, tableExists, err := ReadTable(run, tableFamily, TableName)
|
||||
if err != nil {
|
||||
@@ -106,22 +113,35 @@ func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string
|
||||
"accept", "comment", `"`+requiredPortComment+`"`)
|
||||
}
|
||||
|
||||
func initPreRules(requiredPorts []firewall.PortWhitelist) error {
|
||||
func initPreRules(requiredPorts []firewall.PortWhitelist, families ...filter.Family) error {
|
||||
if len(families) == 0 {
|
||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
}
|
||||
ports, err := firewall.NormalizeRequiredPorts(requiredPorts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rules := firewall.ExpandPortWhitelist(ports)
|
||||
var commands [][]string
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
for _, family := range families {
|
||||
tableFamily := TableFamily(family)
|
||||
output, _, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain)
|
||||
output, _, err := ReadTable(run, tableFamily, TableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chains := ParseTableChains(output)
|
||||
existing := make(map[string]map[string]bool)
|
||||
for _, chain := range []string{BasicBeforeChain, BasicAfterChain} {
|
||||
existing[chain] = make(map[string]bool)
|
||||
for _, line := range strings.Split(chains[chain], "\n") {
|
||||
existing[chain][canonicalRequiredPortRule(line)] = true
|
||||
}
|
||||
}
|
||||
candidates := [][]string{
|
||||
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
|
||||
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
|
||||
{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"},
|
||||
{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "udp", "drop"},
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if rule.Family == string(family) {
|
||||
@@ -129,90 +149,86 @@ func initPreRules(requiredPorts []firewall.PortWhitelist) error {
|
||||
}
|
||||
}
|
||||
for _, command := range candidates {
|
||||
chain := command[4]
|
||||
expression := strings.Join(command[5:], " ")
|
||||
if !containsRequiredPortRule(output, expression) {
|
||||
key := canonicalRequiredPortRule(expression)
|
||||
if !existing[chain][key] {
|
||||
existing[chain][key] = true
|
||||
commands = append(commands, command)
|
||||
output += "\n" + expression
|
||||
}
|
||||
}
|
||||
commands = append(commands,
|
||||
[]string{"flush", "chain", tableFamily, TableName, BasicAfterChain},
|
||||
[]string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"},
|
||||
[]string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "udp", "drop"},
|
||||
)
|
||||
}
|
||||
return runBatch(commands...)
|
||||
}
|
||||
|
||||
func containsRequiredPortRule(output, expression string) bool {
|
||||
canonical := func(line string) string {
|
||||
line, _, _ = strings.Cut(line, " comment ")
|
||||
line, _, _ = strings.Cut(line, " # handle ")
|
||||
for _, protocol := range []string{"tcp", "udp"} {
|
||||
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "")
|
||||
}
|
||||
line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
|
||||
fields := strings.Fields(line)
|
||||
for index, field := range fields {
|
||||
if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" {
|
||||
states := strings.Split(field, ",")
|
||||
for i, state := range states {
|
||||
value, err := strconv.ParseUint(state, 0, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch value {
|
||||
case 1:
|
||||
states[i] = "invalid"
|
||||
case 2:
|
||||
states[i] = "established"
|
||||
case 4:
|
||||
states[i] = "related"
|
||||
case 8:
|
||||
states[i] = "new"
|
||||
case 64:
|
||||
states[i] = "untracked"
|
||||
}
|
||||
func canonicalRequiredPortRule(line string) string {
|
||||
line, _, _ = strings.Cut(line, " comment ")
|
||||
line, _, _ = strings.Cut(line, " # handle ")
|
||||
for _, protocol := range []string{"tcp", "udp"} {
|
||||
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" "+protocol+" ", protocol+" ")
|
||||
}
|
||||
line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
|
||||
fields := strings.Fields(line)
|
||||
for index, field := range fields {
|
||||
if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" {
|
||||
states := strings.Split(field, ",")
|
||||
for i, state := range states {
|
||||
value, err := strconv.ParseUint(state, 0, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
slices.Sort(states)
|
||||
fields[index] = strings.Join(states, ",")
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(field); err == nil {
|
||||
prefix = prefix.Masked()
|
||||
fields[index] = prefix.String()
|
||||
if prefix.Bits() == prefix.Addr().BitLen() {
|
||||
fields[index] = prefix.Addr().String()
|
||||
switch value {
|
||||
case 1:
|
||||
states[i] = "invalid"
|
||||
case 2:
|
||||
states[i] = "established"
|
||||
case 4:
|
||||
states[i] = "related"
|
||||
case 8:
|
||||
states[i] = "new"
|
||||
case 64:
|
||||
states[i] = "untracked"
|
||||
}
|
||||
}
|
||||
slices.Sort(states)
|
||||
fields[index] = strings.Join(states, ",")
|
||||
}
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
wanted := canonical(expression)
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
if canonical(line) == wanted {
|
||||
return true
|
||||
if prefix, err := netip.ParsePrefix(field); err == nil {
|
||||
prefix = prefix.Masked()
|
||||
fields[index] = prefix.String()
|
||||
if prefix.Bits() == prefix.Addr().BitLen() {
|
||||
fields[index] = prefix.Addr().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
|
||||
func Bind() error {
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
func Bind(families ...filter.Family) error {
|
||||
if len(families) == 0 {
|
||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
}
|
||||
for _, family := range families {
|
||||
tableFamily := TableFamily(family)
|
||||
if _, err := run("list", "chain", tableFamily, TableName, InputChain); err != nil {
|
||||
return fmt.Errorf("1Panel nftables %s input chain is not initialized: %w", tableFamily, err)
|
||||
initialized, _, err := LoadFamilyInitStatus(family, "base")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !initialized {
|
||||
return fmt.Errorf("1Panel nftables %s chains are not initialized", tableFamily)
|
||||
}
|
||||
}
|
||||
commands := make([][]string, 0, 8)
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
for _, family := range families {
|
||||
tableFamily := TableFamily(family)
|
||||
commands = append(commands, []string{"add", "table", tableFamily, TableName})
|
||||
commands = append(commands, []string{"flush", "chain", tableFamily, TableName, InputChain})
|
||||
for _, chain := range BasicChains() {
|
||||
commands = append(commands, []string{"add", "rule", tableFamily, TableName, InputChain, "jump", chain})
|
||||
}
|
||||
}
|
||||
if err := runBatch(commands...); err != nil {
|
||||
cleanupErr := flushInputChains()
|
||||
cleanupErr := flushInputChains(families...)
|
||||
return errors.Join(err, cleanupErr)
|
||||
}
|
||||
return PersistRuleset(context.Background())
|
||||
@@ -225,10 +241,28 @@ func Unbind() error {
|
||||
return PersistRuleset(context.Background())
|
||||
}
|
||||
|
||||
func flushInputChains() error {
|
||||
func flushInputChains(families ...filter.Family) error {
|
||||
if len(families) == 0 {
|
||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
}
|
||||
commands := make([][]string, 0, 2)
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
for _, family := range families {
|
||||
commands = append(commands, []string{"flush", "chain", TableFamily(family), TableName, InputChain})
|
||||
}
|
||||
return runBatch(commands...)
|
||||
}
|
||||
|
||||
func OperateFamily(family filter.Family, initialize bool, ports []firewall.PortWhitelist) error {
|
||||
if family != filter.FamilyIPv4 && family != filter.FamilyIPv6 {
|
||||
return fmt.Errorf("unsupported nftables family %q", family)
|
||||
}
|
||||
if initialize {
|
||||
if err := ensureBaseChains(family); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initPreRules(ports, family); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return Bind(family)
|
||||
}
|
||||
|
||||
@@ -40,15 +40,19 @@ func PersistRuleset(ctx context.Context) error {
|
||||
return atomicWrite(filepath.Join(global.Dir.FirewallDir, RulesFile), []byte(ruleset.String()))
|
||||
}
|
||||
|
||||
func Restore() error {
|
||||
file := filepath.Join(global.Dir.FirewallDir, RulesFile)
|
||||
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
||||
func Restore(families ...filter.Family) error {
|
||||
if len(families) == 0 {
|
||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(global.Dir.FirewallDir, RulesFile))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing := make([]string, 0, 2)
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
var script strings.Builder
|
||||
for _, family := range families {
|
||||
tableFamily := TableFamily(family)
|
||||
_, exists, err := readNftObject(run, "list", "table", tableFamily, TableName)
|
||||
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
@@ -58,18 +62,35 @@ func Restore() error {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
existing = append(existing, tableFamily)
|
||||
continue
|
||||
}
|
||||
table, err := savedTableRules(string(data), tableFamily, TableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
script.WriteString(table)
|
||||
}
|
||||
if len(existing) == 2 {
|
||||
if script.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, tableFamily := range existing {
|
||||
if _, err := run("delete", "table", tableFamily, TableName); err != nil {
|
||||
return fmt.Errorf("remove partial nftables %s table before restore: %w", tableFamily, err)
|
||||
return RunScript(script.String())
|
||||
}
|
||||
|
||||
func savedTableRules(ruleset, family, table string) (string, error) {
|
||||
header := "table " + family + " " + table + " {"
|
||||
lines := strings.Split(ruleset, "\n")
|
||||
for index, line := range lines {
|
||||
if line != header {
|
||||
continue
|
||||
}
|
||||
for end := index + 1; end < len(lines); end++ {
|
||||
if lines[end] == "}" {
|
||||
return strings.Join(lines[index:end+1], "\n") + "\n", nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("incomplete saved nftables table %s %s", family, table)
|
||||
}
|
||||
return runCommand("-f", file)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func atomicWrite(target string, data []byte) error {
|
||||
|
||||
@@ -81,6 +81,7 @@ func RunScriptContext(ctx context.Context, script string) error {
|
||||
return runScriptFile(script, func(file string) error {
|
||||
return cmd.NewCommandMgr(
|
||||
cmd.WithContext(ctx),
|
||||
cmd.WithEnv("LC_ALL=C"),
|
||||
cmd.WithTimeout(60*time.Second),
|
||||
).RunWithOptionalSudo("nft", "-f", file)
|
||||
})
|
||||
@@ -169,26 +170,35 @@ func hasBaseChainBinding(output string) bool {
|
||||
}
|
||||
|
||||
func loadFamilyInitStatus(family filter.Family) (bool, bool, error) {
|
||||
initialized, bound, _, err := LoadFamilyState(family)
|
||||
return initialized, bound, err
|
||||
}
|
||||
|
||||
func LoadFamilyState(family filter.Family) (bool, bool, bool, error) {
|
||||
output, exists, err := ReadTable(run, TableFamily(family), TableName)
|
||||
if err != nil || !exists {
|
||||
return false, false, err
|
||||
return false, false, false, err
|
||||
}
|
||||
chains := ParseTableChains(output)
|
||||
for _, chain := range BasicChains() {
|
||||
if _, exists := chains[chain]; !exists {
|
||||
return false, false, nil
|
||||
count := 0
|
||||
for _, chain := range append(BasicChains(), InputChain) {
|
||||
if _, ok := chains[chain]; ok {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != len(BasicChains())+1 {
|
||||
return false, false, count > 0, nil
|
||||
}
|
||||
input, exists := chains[InputChain]
|
||||
if !exists {
|
||||
return false, false, nil
|
||||
return false, false, false, nil
|
||||
}
|
||||
for _, chain := range BasicChains() {
|
||||
if !strings.Contains(input, "jump "+chain) {
|
||||
return true, false, nil
|
||||
return true, false, false, nil
|
||||
}
|
||||
}
|
||||
return true, true, nil
|
||||
return true, !TableDormant(output), false, nil
|
||||
}
|
||||
|
||||
func ReadTable(run func(...string) (string, error), family, table string) (string, bool, error) {
|
||||
@@ -228,3 +238,33 @@ func ReadChain(run func(...string) (string, error), family, table, chain string)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func TableDormant(output string) bool {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(strings.TrimSuffix(strings.TrimSpace(line), ";"))
|
||||
if len(fields) > 0 && fields[0] == "chain" {
|
||||
break
|
||||
}
|
||||
if len(fields) > 1 && fields[0] == "flags" {
|
||||
for _, flag := range strings.Split(strings.Join(fields[1:], ""), ",") {
|
||||
if flag == "dormant" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SetTableDormant(ctx context.Context, family, table string) error {
|
||||
manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second))
|
||||
output, exists, err := ReadTable(func(args ...string) (string, error) { return manager.RunWithOptionalSudoAndStdout("nft", args...) }, family, table)
|
||||
if err != nil || !exists || TableDormant(output) {
|
||||
return err
|
||||
}
|
||||
script, err := buildBatchScript([]string{"add", "table", family, table, "{", "flags", "dormant", ";", "}"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return RunScriptContext(ctx, script)
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) {
|
||||
}
|
||||
|
||||
func SystemPortKey(port SystemPort) string {
|
||||
key := LegacySystemPortKey(port)
|
||||
key := strings.ToLower(strings.TrimSpace(port.Protocol)) + "/" + strings.TrimSpace(port.Port)
|
||||
if family := strings.ToLower(strings.TrimSpace(port.Family)); family != "" {
|
||||
key = family + "/" + key
|
||||
}
|
||||
@@ -273,10 +273,6 @@ func SystemPortKey(port SystemPort) string {
|
||||
return key
|
||||
}
|
||||
|
||||
func LegacySystemPortKey(port SystemPort) string {
|
||||
return strings.ToLower(strings.TrimSpace(port.Protocol)) + "/" + strings.TrimSpace(port.Port)
|
||||
}
|
||||
|
||||
func SortedSystemPortKeys(ports map[string]SystemPort) []string {
|
||||
keys := make([]string, 0, len(ports))
|
||||
for key := range ports {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package sync
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusReady Status = "ready"
|
||||
StatusExisting Status = "existing"
|
||||
StatusRemove Status = "remove"
|
||||
StatusBlocked Status = "blocked"
|
||||
)
|
||||
|
||||
type ReasonCode string
|
||||
|
||||
const (
|
||||
ReasonInvalidPolicy ReasonCode = "invalid_policy"
|
||||
ReasonAlreadyExists ReasonCode = "already_exists_in_target"
|
||||
ReasonOnlyExistsInTarget ReasonCode = "only_exists_in_target"
|
||||
ReasonManagedOnlyInTarget ReasonCode = "managed_only_exists_in_target"
|
||||
ReasonUnsafeRemoval ReasonCode = "unsafe_managed_rule_removal"
|
||||
ReasonReadOnlyRule ReasonCode = "read_only_rule"
|
||||
)
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
)
|
||||
|
||||
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+.*)?$`
|
||||
VLLMMetricLabelPattern = `([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("(?:[^"\\]|\\.)*")`
|
||||
NumberAlphaPattern = `(\d+)([A-Za-z]+)`
|
||||
@@ -63,6 +66,9 @@ var regexMap = make(map[string]*regexp.Regexp)
|
||||
|
||||
func Init() {
|
||||
patterns := []string{
|
||||
VMNameValidationPattern,
|
||||
VMCommonPattern,
|
||||
|
||||
VLLMMetricSamplePattern,
|
||||
VLLMMetricLabelPattern,
|
||||
NumberAlphaPattern,
|
||||
|
||||
@@ -309,16 +309,15 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) {
|
||||
if revalidate {
|
||||
s.revalidateCursor = cursor
|
||||
}
|
||||
shouldClose := clean || (!s.Persistent && !revalidate)
|
||||
if !shouldClose {
|
||||
if !clean {
|
||||
timeout := graceTimeout
|
||||
if revalidate {
|
||||
if !s.Persistent || revalidate {
|
||||
timeout = revalidateGrace
|
||||
}
|
||||
s.grace = time.AfterFunc(timeout, s.Close)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if shouldClose {
|
||||
if clean {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@ const (
|
||||
TaskScopeCluster = "Cluster"
|
||||
TaskScopeAppInstall = "AppInstallTask"
|
||||
TaskScopeAI = "AI"
|
||||
TaskScopeVm = "VirtualMachine"
|
||||
)
|
||||
|
||||
func GetTaskName(resourceName, operate, scope string) string {
|
||||
|
||||
+631
-225
File diff suppressed because it is too large
Load Diff
+631
-225
File diff suppressed because it is too large
Load Diff
+99
-111
@@ -265,8 +265,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
|
||||
"formatEN": "Application port update [key]-[name] =\u003e [port]"
|
||||
"formatZH": "应用端口修改 [key]-[name] => [port]",
|
||||
"formatEN": "Application port update [key]-[name] => [port]"
|
||||
},
|
||||
"/apps/installed/sort/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -644,13 +644,6 @@
|
||||
"formatZH": "创建容器网络 name",
|
||||
"formatEN": "create container network [name]"
|
||||
},
|
||||
"/containers/network/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理未使用的容器网络",
|
||||
"formatEN": "Clean unused container networks"
|
||||
},
|
||||
"/containers/network/del": {
|
||||
"bodyKeys": [
|
||||
"names"
|
||||
@@ -686,8 +679,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "容器重命名 [name] =\u003e [newName]",
|
||||
"formatEN": "rename container [name] =\u003e [newName]"
|
||||
"formatZH": "容器重命名 [name] => [newName]",
|
||||
"formatEN": "rename container [name] => [newName]"
|
||||
},
|
||||
"/containers/repo": {
|
||||
"bodyKeys": [
|
||||
@@ -838,8 +831,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
},
|
||||
"/core/auth/expired/reset": {
|
||||
"bodyKeys": [],
|
||||
@@ -1723,8 +1716,8 @@
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
},
|
||||
"/core/enterprise/users/del": {
|
||||
"bodyKeys": [
|
||||
@@ -1925,8 +1918,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||
},
|
||||
"/core/settings/memo": {
|
||||
"bodyKeys": [],
|
||||
@@ -1955,8 +1948,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统端口 =\u003e [serverPort]",
|
||||
"formatEN": "update system port =\u003e [serverPort]"
|
||||
"formatZH": "修改系统端口 => [serverPort]",
|
||||
"formatEN": "update system port => [serverPort]"
|
||||
},
|
||||
"/core/settings/proxy/update": {
|
||||
"bodyKeys": [
|
||||
@@ -1981,8 +1974,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统 ssl =\u003e [ssl]",
|
||||
"formatEN": "update system ssl =\u003e [ssl]"
|
||||
"formatZH": "修改系统 ssl => [ssl]",
|
||||
"formatEN": "update system ssl => [ssl]"
|
||||
},
|
||||
"/core/settings/terminal/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -1998,8 +1991,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||
"formatEN": "update system setting [key] =\u003e [value]"
|
||||
"formatZH": "修改系统配置 [key] => [value]",
|
||||
"formatEN": "update system setting [key] => [value]"
|
||||
},
|
||||
"/core/settings/upgrade": {
|
||||
"bodyKeys": [
|
||||
@@ -2007,8 +2000,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新系统 =\u003e [version]",
|
||||
"formatEN": "upgrade system =\u003e [version]"
|
||||
"formatZH": "更新系统 => [version]",
|
||||
"formatEN": "upgrade system => [version]"
|
||||
},
|
||||
"/core/xapp/appQRCode": {
|
||||
"bodyKeys": [
|
||||
@@ -2466,7 +2459,7 @@
|
||||
"formatZH": "同步 SSL 证书 [primaryDomain]",
|
||||
"formatEN": "sync SSL certificate [primaryDomain]"
|
||||
},
|
||||
"/core/xpack/vms": {
|
||||
"/xpack/vms": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
@@ -2475,7 +2468,7 @@
|
||||
"formatZH": "创建虚拟机 [name]",
|
||||
"formatEN": "create VM [name]"
|
||||
},
|
||||
"/core/xpack/vms/del": {
|
||||
"/xpack/vms/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2493,7 +2486,7 @@
|
||||
"formatZH": "删除虚拟机 [name]",
|
||||
"formatEN": "delete VM [name]"
|
||||
},
|
||||
"/core/xpack/vms/disks/resize": {
|
||||
"/xpack/vms/disks/resize": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"target",
|
||||
@@ -2504,14 +2497,14 @@
|
||||
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
|
||||
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
|
||||
},
|
||||
"/core/xpack/vms/environment/enable": {
|
||||
"/xpack/vms/environment/enable": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "启用虚拟机运行环境",
|
||||
"formatEN": "enable VM runtime environment"
|
||||
},
|
||||
"/core/xpack/vms/iso": {
|
||||
"/xpack/vms/iso": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2521,7 +2514,7 @@
|
||||
"formatZH": "创建虚拟机镜像 [name] [type]",
|
||||
"formatEN": "create VM ISO [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/iso/del": {
|
||||
"/xpack/vms/iso/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2539,7 +2532,7 @@
|
||||
"formatZH": "删除虚拟机镜像 [name]",
|
||||
"formatEN": "delete VM ISO [name]"
|
||||
},
|
||||
"/core/xpack/vms/iso/update": {
|
||||
"/xpack/vms/iso/update": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2549,7 +2542,7 @@
|
||||
"formatZH": "更新虚拟机镜像 [name] [type]",
|
||||
"formatEN": "update VM ISO [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/networks": {
|
||||
"/xpack/vms/networks": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2559,7 +2552,7 @@
|
||||
"formatZH": "创建虚拟机网络 [name] [type]",
|
||||
"formatEN": "create VM network [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/networks/del": {
|
||||
"/xpack/vms/networks/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2577,7 +2570,7 @@
|
||||
"formatZH": "删除虚拟机网络 [name]",
|
||||
"formatEN": "delete VM network [name]"
|
||||
},
|
||||
"/core/xpack/vms/networks/update": {
|
||||
"/xpack/vms/networks/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2595,7 +2588,7 @@
|
||||
"formatZH": "更新虚拟机网络 [name]",
|
||||
"formatEN": "update VM network [name]"
|
||||
},
|
||||
"/core/xpack/vms/operate": {
|
||||
"/xpack/vms/operate": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"operate"
|
||||
@@ -2605,14 +2598,14 @@
|
||||
"formatZH": "操作虚拟机 [name] [operate]",
|
||||
"formatEN": "operate VM [name] [operate]"
|
||||
},
|
||||
"/core/xpack/vms/orphans/clean": {
|
||||
"/xpack/vms/orphans/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理虚拟机孤立数据",
|
||||
"formatEN": "clean VM orphaned data"
|
||||
},
|
||||
"/core/xpack/vms/rename": {
|
||||
"/xpack/vms/rename": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"newName"
|
||||
@@ -2622,7 +2615,7 @@
|
||||
"formatZH": "重命名虚拟机 [name] 为 [newName]",
|
||||
"formatEN": "rename VM [name] to [newName]"
|
||||
},
|
||||
"/core/xpack/vms/snapshots": {
|
||||
"/xpack/vms/snapshots": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -2632,7 +2625,7 @@
|
||||
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "create VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/xpack/vms/snapshots/del": {
|
||||
"/xpack/vms/snapshots/del": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -2642,7 +2635,7 @@
|
||||
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "delete VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/xpack/vms/snapshots/recover": {
|
||||
"/xpack/vms/snapshots/recover": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
@@ -2652,7 +2645,7 @@
|
||||
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "recover VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/xpack/vms/storages": {
|
||||
"/xpack/vms/storages": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
@@ -2662,7 +2655,7 @@
|
||||
"formatZH": "创建虚拟机存储 [name] [type]",
|
||||
"formatEN": "create VM storage [name] [type]"
|
||||
},
|
||||
"/core/xpack/vms/storages/del": {
|
||||
"/xpack/vms/storages/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2680,7 +2673,7 @@
|
||||
"formatZH": "删除虚拟机存储 [name]",
|
||||
"formatEN": "delete VM storage [name]"
|
||||
},
|
||||
"/core/xpack/vms/storages/update": {
|
||||
"/xpack/vms/storages/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2698,14 +2691,14 @@
|
||||
"formatZH": "更新虚拟机存储 [name]",
|
||||
"formatEN": "update VM storage [name]"
|
||||
},
|
||||
"/core/xpack/vms/sync": {
|
||||
"/xpack/vms/sync": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步虚拟机",
|
||||
"formatEN": "sync virtual machines"
|
||||
},
|
||||
"/core/xpack/vms/templates": {
|
||||
"/xpack/vms/templates": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"templateName"
|
||||
@@ -2715,7 +2708,7 @@
|
||||
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
|
||||
"formatEN": "create VM [name] template [templateName]"
|
||||
},
|
||||
"/core/xpack/vms/templates/del": {
|
||||
"/xpack/vms/templates/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
@@ -2733,7 +2726,7 @@
|
||||
"formatZH": "删除虚拟机模板 [name]",
|
||||
"formatEN": "delete VM template [name]"
|
||||
},
|
||||
"/core/xpack/vms/update": {
|
||||
"/xpack/vms/update": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
@@ -2895,8 +2888,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "首页应用 [key] =\u003e 显示:[value]",
|
||||
"formatEN": "app launcher [key] =\u003e show: [value]"
|
||||
"formatZH": "首页应用 [key] => 显示:[value]",
|
||||
"formatEN": "app launcher [key] => show: [value]"
|
||||
},
|
||||
"/dashboard/quick/change": {
|
||||
"bodyKeys": [],
|
||||
@@ -3024,7 +3017,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the mysql database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/grants": {
|
||||
"bodyKeys": [
|
||||
@@ -3105,7 +3098,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the mongodb database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/mongodb/password": {
|
||||
"bodyKeys": [
|
||||
@@ -3190,7 +3183,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the postgresql database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/pg/password": {
|
||||
"bodyKeys": [
|
||||
@@ -3295,8 +3288,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
|
||||
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
|
||||
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
|
||||
},
|
||||
"/databases/variables/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -3332,8 +3325,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
|
||||
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
|
||||
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
|
||||
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
|
||||
},
|
||||
"/files/chunkdownload": {
|
||||
"bodyKeys": [
|
||||
@@ -3423,8 +3416,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改权限 [path] =\u003e [mode]",
|
||||
"formatEN": "Change mode [path] =\u003e [mode]"
|
||||
"formatZH": "修改权限 [path] => [mode]",
|
||||
"formatEN": "Change mode [path] => [mode]"
|
||||
},
|
||||
"/files/move": {
|
||||
"bodyKeys": [
|
||||
@@ -3433,8 +3426,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
|
||||
"formatEN": "Move [oldPaths] =\u003e [newPath]"
|
||||
"formatZH": "移动文件 [oldPaths] => [newPath]",
|
||||
"formatEN": "Move [oldPaths] => [newPath]"
|
||||
},
|
||||
"/files/owner": {
|
||||
"bodyKeys": [
|
||||
@@ -3444,8 +3437,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
|
||||
"formatEN": "Change owner [path] =\u003e [user]/[group]"
|
||||
"formatZH": "修改用户/组 [path] => [user]/[group]",
|
||||
"formatEN": "Change owner [path] => [user]/[group]"
|
||||
},
|
||||
"/files/preview": {
|
||||
"bodyKeys": [
|
||||
@@ -3479,8 +3472,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "重命名 [oldName] =\u003e [newName]",
|
||||
"formatEN": "Rename [oldName] =\u003e [newName]"
|
||||
"formatZH": "重命名 [oldName] => [newName]",
|
||||
"formatEN": "Rename [oldName] => [newName]"
|
||||
},
|
||||
"/files/save": {
|
||||
"bodyKeys": [
|
||||
@@ -3537,8 +3530,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载 url =\u003e [path]/[name]",
|
||||
"formatEN": "Download url =\u003e [path]/[name]"
|
||||
"formatZH": "下载 url => [path]/[name]",
|
||||
"formatEN": "Download url => [path]/[name]"
|
||||
},
|
||||
"/files/wget/process/remove": {
|
||||
"bodyKeys": [
|
||||
@@ -3688,12 +3681,16 @@
|
||||
"formatZH": "删除 Docker 端口防护策略 [uuids]",
|
||||
"formatEN": "delete Docker port guard policies [uuids]"
|
||||
},
|
||||
"/hosts/firewall/docker/sync": {
|
||||
"bodyKeys": [],
|
||||
"/hosts/firewall/family/operate": {
|
||||
"bodyKeys": [
|
||||
"subsystem",
|
||||
"family",
|
||||
"operation"
|
||||
],
|
||||
"formatEN": "[operation] [subsystem] [family] firewall chains",
|
||||
"formatZH": "[operation] [subsystem] [family] 防火墙链",
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步 Docker 端口防护规则",
|
||||
"formatEN": "sync Docker port guard rules"
|
||||
"beforeFunctions": []
|
||||
},
|
||||
"/hosts/firewall/filter/operate": {
|
||||
"bodyKeys": [
|
||||
@@ -3734,13 +3731,6 @@
|
||||
"formatZH": "添加防火墙规则",
|
||||
"formatEN": "create firewall rules"
|
||||
},
|
||||
"/hosts/firewall/rules/adopt": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "纳管防火墙规则",
|
||||
"formatEN": "adopt firewall rule"
|
||||
},
|
||||
"/hosts/firewall/rules/delete": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
@@ -3750,12 +3740,12 @@
|
||||
},
|
||||
"/hosts/firewall/rules/reorder": {
|
||||
"bodyKeys": [
|
||||
"uuid"
|
||||
"instanceKey"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "调整防火墙规则顺序 [uuid]",
|
||||
"formatEN": "reorder firewall rule [uuid]"
|
||||
"formatZH": "调整防火墙规则顺序 [instanceKey]",
|
||||
"formatEN": "reorder firewall rule [instanceKey]"
|
||||
},
|
||||
"/hosts/firewall/rules/reset": {
|
||||
"bodyKeys": [],
|
||||
@@ -3764,25 +3754,23 @@
|
||||
"formatZH": "重置防火墙规则",
|
||||
"formatEN": "reset firewall rules"
|
||||
},
|
||||
"/hosts/firewall/rules/sync": {
|
||||
"bodyKeys": [
|
||||
"subsystem",
|
||||
"sourceProvider",
|
||||
"targetProvider"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步 [subsystem] 防火墙规则到 [targetProvider]",
|
||||
"formatEN": "sync [subsystem] firewall rules to [targetProvider]"
|
||||
},
|
||||
"/hosts/firewall/rules/update": {
|
||||
"bodyKeys": [
|
||||
"uuid"
|
||||
"instanceKey"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新防火墙规则 [uuid]",
|
||||
"formatEN": "update firewall rule [uuid]"
|
||||
"formatZH": "更新防火墙规则 [instanceKey]",
|
||||
"formatEN": "update firewall rule [instanceKey]"
|
||||
},
|
||||
"/hosts/firewall/settings/ipv6": {
|
||||
"bodyKeys": [
|
||||
"status"
|
||||
],
|
||||
"formatEN": "Set firewall IPv6 support to [status]",
|
||||
"formatZH": "设置防火墙 IPv6 支持为 [status]",
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": []
|
||||
},
|
||||
"/hosts/firewall/settings/operate": {
|
||||
"bodyKeys": [
|
||||
@@ -3900,8 +3888,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
|
||||
"formatEN": "update SSH setting [key] =\u003e [newValue]"
|
||||
"formatZH": "修改 SSH 配置 [key] => [newValue]",
|
||||
"formatEN": "update SSH setting [key] => [newValue]"
|
||||
},
|
||||
"/hosts/tool/config/set": {
|
||||
"bodyKeys": [
|
||||
@@ -4162,7 +4150,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "快照 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the snapshot [name] is modified => [description]"
|
||||
},
|
||||
"/settings/snapshot/import": {
|
||||
"bodyKeys": [
|
||||
@@ -4254,8 +4242,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||
"formatEN": "update system setting [key] =\u003e [value]"
|
||||
"formatZH": "修改系统配置 [key] => [value]",
|
||||
"formatEN": "update system setting [key] => [value]"
|
||||
},
|
||||
"/toolbox/clam": {
|
||||
"bodyKeys": [
|
||||
@@ -4373,8 +4361,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改主机参数 [key] =\u003e [value]",
|
||||
"formatEN": "update device conf [key] =\u003e [value]"
|
||||
"formatZH": "修改主机参数 [key] => [value]",
|
||||
"formatEN": "update device conf [key] => [value]"
|
||||
},
|
||||
"/toolbox/device/update/host": {
|
||||
"bodyKeys": [
|
||||
@@ -4383,8 +4371,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改主机 Host [key] =\u003e [value]",
|
||||
"formatEN": "update device host [key] =\u003e [value]"
|
||||
"formatZH": "修改主机 Host [key] => [value]",
|
||||
"formatEN": "update device host [key] => [value]"
|
||||
},
|
||||
"/toolbox/device/update/swap": {
|
||||
"bodyKeys": [
|
||||
@@ -4412,8 +4400,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
|
||||
"formatEN": "update fail2ban conf [key] =\u003e [value]"
|
||||
"formatZH": "修改 Fail2ban 配置 [key] => [value]",
|
||||
"formatEN": "update fail2ban conf [key] => [value]"
|
||||
},
|
||||
"/toolbox/ftp": {
|
||||
"bodyKeys": [
|
||||
@@ -4672,8 +4660,8 @@
|
||||
"output_value": "domain"
|
||||
}
|
||||
],
|
||||
"formatZH": "修改默认 server =\u003e [domain]",
|
||||
"formatEN": "Change default server =\u003e [domain]"
|
||||
"formatZH": "修改默认 server => [domain]",
|
||||
"formatEN": "Change default server => [domain]"
|
||||
},
|
||||
"/websites/del": {
|
||||
"bodyKeys": [
|
||||
@@ -5436,8 +5424,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
|
||||
"formatEN": "update tamper info [website][path] =\u003e [status]"
|
||||
"formatZH": "更新防篡改信息 [website][path] => [status]",
|
||||
"formatEN": "update tamper info [website][path] => [status]"
|
||||
},
|
||||
"/xpack/vllm/command-template/create": {
|
||||
"bodyKeys": [
|
||||
|
||||
@@ -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."
|
||||
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
|
||||
CustomAppStoreFileValid: "Application store package requires .tar.gz format"
|
||||
AppBackup: 'Application backup'
|
||||
|
||||
@@ -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."
|
||||
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
|
||||
CustomAppStoreFileValid: "El paquete de la tienda de aplicaciones debe tener formato .tar.gz"
|
||||
AppBackup: 'Copia de seguridad de aplicación'
|
||||
|
||||
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "دریافت وضعیت برنامه ناموفق بود.
|
||||
MasterNodePortNotAvailable: "بررسی اتصال پورت {{ .port }} گره {{ .name }} ناموفق بود. تنظیمات دیواره آتش/گروه امنیتی و وضعیت گره اصلی را بررسی کنید."
|
||||
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 باشد"
|
||||
AppBackup: 'پشتیبانگیری از برنامه'
|
||||
|
||||
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "アプリケーションのステータス取得が異常
|
||||
MasterNodePortNotAvailable: "ノード {{ .name }} のポート {{ .port }} の接続性検証が失敗しました。ファイアウォール/セキュリティグループの設定とマスターノードのステータスを確認してください。"
|
||||
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 形式である必要があります"
|
||||
AppBackup: 'アプリケーションバックアップ'
|
||||
|
||||
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "애플리케이션 상태 획득이 비정상입니다. 노
|
||||
MasterNodePortNotAvailable: "노드 {{ .name }} 포트 {{ .port }} 연결성 검증에 실패했습니다. 방화벽/보안 그룹 설정 및 마스터 노드 상태를 확인하세요."
|
||||
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 형식이어야 합니다"
|
||||
AppBackup: '애플리케이션 백업'
|
||||
|
||||
@@ -133,48 +133,6 @@ AppStatusUnHealthy: "ດຶງສະຖານະແອັບບໍ່ສຳເ
|
||||
MasterNodePortNotAvailable: "ການກວດສອບການເຊື່ອມຕໍ່ຂອງໂນດ {{ .name }} ຜ່ານພອດ {{ .port }} ລົ້ມເຫຼວ. ກະລຸນາກວດສອບການຕັ້ງຄ່າໄຟຣ໌ວໍ/ກຸ່ມຄວາມປອດໄພ ແລະ ສະຖານະໂນດຫຼັກ."
|
||||
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
|
||||
CustomAppStoreFileValid: "ແພັກເກດຮ້ານຄ້າແອັບຕ້ອງເປັນຮູບແບບ .tar.gz"
|
||||
AppBackup: 'ສຳຮອງແອັບ'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user