Compare commits

..
39 Commits
Author SHA1 Message Date
ssongliu 65243c68c4 fix: allow licensed community restore progress route (#13913) 2026-09-24 10:18:01 +08:00
ssongliu 256e79ca81 fix: remove minimum upgrade backup copies limit (#13907) 2026-09-23 18:17:32 +08:00
ssongliu 4861eb69cb fix: separate firewalld batches by native option (#13896) 2026-09-22 18:20:18 +08:00
蘭 fb8cf15537 fix: Fix the automatic renewal certificate alert issue (#13895) 2026-09-22 15:06:48 +08:00
ssongliu c4a6791271 fix: allow updated SSH ports through firewall before restart (#13894) 2026-09-22 14:51:54 +08:00
蘭 f58e147636 fix: enhance file rename functionality (#13891) 2026-09-22 12:49:05 +08:00
ssongliu 387e9fbeed fix: correct firewall port allowances and whitelist sync reporting (#13892) 2026-09-22 12:48:40 +08:00
ssongliu 4eb627bc79 fix(vm): display additional virtual machine states (#13890) 2026-09-22 10:27:49 +08:00
ssongliu 850c86229c fix: log dashboard disk command failures (#13889) 2026-09-22 10:16:54 +08:00
CityFun f984917a66 fix(upgrade): resolve application upgrade failures (#13888) 2026-09-22 10:11:13 +08:00
A_Words 8588217fbf fix: prioritize HTTP-to-HTTPS redirects over website redirects (#13847) 2026-09-22 10:05:53 +08:00
ssongliu a2307c5f64 fix(firewall): improve batch operations and task log completion (#13886) 2026-09-22 09:46:46 +08:00
蘭 5923290de8 ref: add file task messages for copy, move, compress and decompress (#13885) 2026-09-21 17:32:14 +08:00
蘭 1c994fba4a ref: update documentation URL for the panel (#13884) 2026-09-21 17:28:23 +08:00
蘭 415ab96aab ref:improve the panel API key (#13883) 2026-09-21 17:18:15 +08:00
ssongliu 19bb823b05 fix(firewall): guard IPv6 forwarding against RA disruption (#13882) 2026-09-21 17:12:17 +08:00
ssongliu 3a5371652e refactor(firewall): simplify state reads and batch verification (#13881) 2026-09-21 15:09:13 +08:00
蘭 a267b4148a ref:Optimize and improve the panel API key (#13880) 2026-09-21 12:49:45 +08:00
蘭 36a01eb60d feat: The panel API key supports creating multiple tokens and enhance terminal capabilities (#13872) 2026-09-20 18:32:54 +08:00
王贺 65f6fdd045 fix(ssh): display SSH login logs in server timezone (#13869)
The SSH log page formatted dates with the browser timezone, so logs looked shifted when the client timezone differed from the server. Format the timestamp with the offset returned by the API so the page matches auth.log, the terminal and the CSV export. Refs #13860.
2026-09-20 18:02:46 +08:00
ssongliu 75b60b32e4 feat(vm): update multi-CD-ROM guidance translations (#13857) 2026-09-18 16:38:31 +08:00
CityFun 6cb65e2290 fix: Fixed with ollama page not work (#13855) 2026-09-18 11:35:46 +08:00
ssongliu 0bad1b471f feat(core): add runtime diagnostics and pprof capture (#13852) 2026-09-17 15:46:06 +08:00
ssongliu 3814525edd fix(core): avoid enumerating authorized IP subnets (#13851)
Use net.IPNet.Contains to check CIDR membership directly and remove the address increment loop. Large authorized subnets no longer cause per-request address enumeration and excessive CPU usage.
2026-09-17 15:45:57 +08:00
ssongliu 8162dd1856 fix(container): improve inspect panel theme and text layout (#13849) 2026-09-17 15:45:48 +08:00
ssongliu e833787020 fix(firewall): preserve whitelist priority and rule ordering (#13845) 2026-09-17 13:02:08 +08:00
ssongliu 673ffac516 refactor(firewall): simplify whitelist configuration and rule protection (#13838) 2026-09-16 22:09:49 +08:00
ssongliu e864610015 fix(firewall): validate whitelist ports and sources in form (#13835) 2026-09-16 21:31:06 +08:00
ssongliu 78402e1b7d refactor(firewall): consolidate utilities and flatten packages (#13833) 2026-09-16 16:48:48 +08:00
ssongliu 782bc1e67c fix(firewall): manage SSH access and queue stop operations (#13831)
* fix(firewall): manage SSH access and queue stop operations

* fix(firewall): reconcile whitelist rules and sync differences
2026-09-16 16:45:33 +08:00
ssongliu 86e4ed6f64 perf(firewall): optimize large rule sets and queue deletions (#13829) 2026-09-16 15:34:10 +08:00
CityFun ee8bac39af style: Optimize the website configuration UI (#13828) 2026-09-16 14:48:57 +08:00
ssongliu fe742b9f41 fix(firewall): improve whitelist management and rule lifecycle (#13826) 2026-09-15 23:55:01 +08:00
CityFun 9a5bd9bcba fix: Fix the issue where Brotli settings cannot be saved when enabled in OpenResty (#13822) 2026-09-15 18:36:51 +08:00
ssongliu b9c8e39560 fix: validate Docker IPv4 forwarding (#13820) 2026-09-15 18:36:36 +08:00
CityFun 6b20ff0b13 feat: OpenClaw supports configuring model Max Tokens (#13818) 2026-09-15 18:36:24 +08:00
ssongliu 89bd32b6d4 fix(terminal): isolate persistent shortcut sessions (#13810) 2026-09-15 10:05:16 +08:00
ssongliu 005f240fb7 fix: improve firewall lifecycle and sync (#13809) 2026-09-15 10:04:53 +08:00
蘭 75da53e374 feat: Remote download supports server file name options and improves error handling (#13808)
* feat: Remote download supports server file name options and improves error handling

* feat: Remote download supports server file name options and improves error handling
2026-09-15 10:04:44 +08:00
236 changed files with 16876 additions and 10746 deletions
+89 -34
View File
@@ -2,14 +2,16 @@ package v2
import ( import (
"errors" "errors"
"github.com/1Panel-dev/1Panel/agent/buserr"
"net/http" "net/http"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -60,7 +62,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
// @Summary Operate firewall // @Summary Operate firewall
// @Accept json // @Accept json
// @Param request body dto.FirewallLifecycleOperation true "request" // @Param request body dto.FirewallLifecycleOperation true "request"
// @Success 200 // @Success 200 {object} dto.FirewallLifecycleOperationResponse
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/operate [post] // @Router /hosts/firewall/operate [post]
@@ -71,12 +73,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
return return
} }
if err := firewallService.OperateFirewall(request); err != nil { result, err := firewallService.QueueFirewallOperation(request)
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.Success(c) helper.SuccessWithData(c, result)
} }
// @Tags Firewall // @Tags Firewall
@@ -367,7 +370,8 @@ func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
} }
// @Tags Firewall // @Tags Firewall
// @Summary Delete managed unified firewall v2 rules // @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.
// @Accept json // @Accept json
// @Param request body dto.FirewallRuleDelete true "request" // @Param request body dto.FirewallRuleDelete true "request"
// @Success 200 {object} dto.FirewallRuleDeleteResponse // @Success 200 {object} dto.FirewallRuleDeleteResponse
@@ -453,6 +457,8 @@ func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
} }
func handleFirewallRuleError(c *gin.Context, err error) { func handleFirewallRuleError(c *gin.Context, err error) {
var businessErr buserr.BusinessError
isBusinessError := errors.As(err, &businessErr)
switch { switch {
case errors.Is(err, filter.ErrProtectedRule): case errors.Is(err, filter.ErrProtectedRule):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err) helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
@@ -460,7 +466,7 @@ func handleFirewallRuleError(c *gin.Context, err error) {
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err) helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict): case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err) helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrManagedScopeChange): case isBusinessError && businessErr.Msg == "ErrFirewallRuleScopeChange":
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err) helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope), case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable): errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
@@ -468,6 +474,9 @@ func handleFirewallRuleError(c *gin.Context, err error) {
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict), case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
errors.Is(err, repo.ErrFirewallPersistenceInvalid): errors.Is(err, repo.ErrFirewallPersistenceInvalid):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err) helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
case isBusinessError && businessErr.Msg == "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): case errors.Is(err, filter.ErrVerificationFailed):
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err) helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
default: default:
@@ -491,26 +500,69 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
} }
// @Tags Firewall // @Tags Firewall
// @Summary Queue firewall port whitelist update // @Summary Create firewall port whitelist rules
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log. // @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json // @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request" // @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post] // @Router /hosts/firewall/settings/whitelist [post]
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙端口白名单 [value]","formatEN":"update firewall port whitelist [value]"} // @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistCreate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @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.
// @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/update [post]
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) { func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistUpdate var request dto.FirewallPortWhitelistUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
result, err := firewallSettingService.QueuePortWhitelist(request.Value) if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, result) helper.Success(c)
}
// @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.
// @Accept json
// @Param request body dto.FirewallPortWhitelistDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/delete [post]
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
@@ -528,14 +580,10 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
return return
} }
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil { if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) { var businessErr buserr.BusinessError
helper.ErrorWithBusinessCode( if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
c, c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
http.StatusConflict, c.Abort()
"FW_BACKEND_CLEANUP_REQUIRED",
"ErrInvalidParams",
err,
)
return return
} }
helper.InternalServer(c, err) helper.InternalServer(c, err)
@@ -664,19 +712,26 @@ func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
} }
func handleDockerPortGuardError(c *gin.Context, err error) { func handleDockerPortGuardError(c *gin.Context, err error) {
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) { var businessErr buserr.BusinessError
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err) if errors.As(err, &businessErr) {
return code, errorCode := http.StatusInternalServerError, ""
switch businessErr.Msg {
case "ErrDockerIptablesChainUnavailable":
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
case "ErrDockerNftablesChainUnavailable":
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
case "ErrInvalidParams":
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
case "ErrDockerFailed":
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
}
if errorCode != "" {
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
c.Abort()
return
}
} }
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) { if errors.Is(err, docker.ErrUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerGuardInvalid) {
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
return
}
if errors.Is(err, service.ErrDockerUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err) helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
return return
} }
+20 -7
View File
@@ -29,6 +29,7 @@ import (
// @Summary Ws local terminal // @Summary Ws local terminal
// @Param command query string false "command" // @Param command query string false "command"
// @Param session query string false "session id to reattach" // @Param session query string false "session id to reattach"
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -43,6 +44,7 @@ func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
// @Param command query string false "command" // @Param command query string false "command"
// @Param session query string false "session id to reattach" // @Param session query string false "session id to reattach"
// @Param title query string false "session title shown in the session list" // @Param title query string false "session title shown in the session list"
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -146,13 +148,14 @@ func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ss
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0")) hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
} }
opts := terminal.SessionOptions{ opts := terminal.SessionOptions{
Identity: identity, Identity: identity,
Kind: kind, Kind: kind,
Title: sanitizeTerminalTitle(c.Query("title")), Title: sanitizeTerminalTitle(c.Query("title")),
HostID: uint(max(hostID, 0)), Persistent: c.Query("terminalPersistent") == "true",
Cols: cols, HostID: uint(max(hostID, 0)),
Rows: rows, Cols: cols,
InitCmd: command, Rows: rows,
InitCmd: command,
} }
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) { err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
client, err := connect() client, err := connect()
@@ -240,6 +243,16 @@ func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)), UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)), AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
} }
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
millis, err := strconv.ParseInt(value, 10, 64)
if err != nil || millis <= 0 {
return terminal.Identity{}, false
}
identity.AuthLeaseUntil = time.UnixMilli(millis)
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
identity.AuthLeaseUntil = maximum
}
}
return identity, identity.Valid() return identity, identity.Valid()
} }
+10
View File
@@ -0,0 +1,10 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
}
+16 -7
View File
@@ -162,16 +162,25 @@ type AgentWebsiteBindReq struct {
} }
type AgentModelConfigUpdateReq struct { type AgentModelConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
Model string `json:"model" validate:"required"` Model string `json:"model" validate:"required"`
Fallbacks []string `json:"fallbacks"` Fallbacks []string `json:"fallbacks"`
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
} }
type AgentModelConfig struct { type AgentModelConfig struct {
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Model string `json:"model"` Model string `json:"model"`
Fallbacks []string `json:"fallbacks"` Fallbacks []string `json:"fallbacks"`
Metadata []AgentModelMetadata `json:"metadata"`
}
type AgentModelMetadata struct {
Model string `json:"model" validate:"required"`
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
ContextWindow int `json:"contextWindow" validate:"min=0"`
MaxTokens int `json:"maxTokens" validate:"min=0"`
} }
type AgentHermesChatSessionItem struct { type AgentHermesChatSessionItem struct {
+45 -18
View File
@@ -1,6 +1,7 @@
package dto package dto
import ( import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
) )
@@ -18,6 +19,7 @@ type FirewallSubsystemStatus struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"` SyncError string `json:"syncError,omitempty"`
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"` IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"` IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
} }
@@ -27,6 +29,11 @@ type FirewallLifecycleOperation struct {
WithDockerRestart bool `json:"withDockerRestart"` WithDockerRestart bool `json:"withDockerRestart"`
} }
type FirewallLifecycleOperationResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued"`
}
type FirewallBackendOption struct { type FirewallBackendOption struct {
Name string `json:"name"` Name string `json:"name"`
Installed bool `json:"installed"` Installed bool `json:"installed"`
@@ -42,10 +49,12 @@ type FirewallBackendOption struct {
} }
type FirewallBackendFamilyStatus struct { type FirewallBackendFamilyStatus struct {
Available bool `json:"available"` Available bool `json:"available"`
Initialized bool `json:"initialized"` Initialized bool `json:"initialized"`
Bound bool `json:"bound"` Bound bool `json:"bound"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
ForwardPolicy string `json:"forwardPolicy,omitempty"`
RAInterfaces []string `json:"raInterfaces,omitempty"`
} }
type FirewallBackendGroup struct { type FirewallBackendGroup struct {
@@ -55,15 +64,26 @@ type FirewallBackendGroup struct {
} }
type FirewallSettings struct { type FirewallSettings struct {
System FirewallBackendGroup `json:"system"` System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"` Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"` Docker FirewallBackendGroup `json:"docker"`
PingStatus string `json:"pingStatus"` PingStatus string `json:"pingStatus"`
PortWhitelist string `json:"portWhiteList"` PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
PanelPort string `json:"panelPort"`
SSHPort string `json:"sshPort"`
}
type FirewallPortWhitelistCreate struct {
Rule filter.PortWhitelist `json:"rule" validate:"required"`
} }
type FirewallPortWhitelistUpdate struct { type FirewallPortWhitelistUpdate struct {
Value string `json:"value" validate:"required"` OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
Rule filter.PortWhitelist `json:"rule" validate:"required"`
}
type FirewallPortWhitelistDelete struct {
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
} }
type FirewallBackendOperation struct { type FirewallBackendOperation struct {
@@ -87,11 +107,7 @@ type FirewallInitializationTask struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
} }
type FirewallSystemPort struct { type FirewallSystemPort = firewall.SystemPort
Family string
Port string
Protocol string
}
type FirewallRuleInventoryResponse struct { type FirewallRuleInventoryResponse struct {
IPv4Range filter.PositionRange `json:"ipv4Range"` IPv4Range filter.PositionRange `json:"ipv4Range"`
@@ -114,6 +130,7 @@ type FirewallRuleReset struct {
} }
type FirewallRuleInventory struct { type FirewallRuleInventory struct {
Refresh bool `json:"refresh,omitempty"`
PageInfo PageInfo
Scope filter.Scope `json:"scope,omitempty"` Scope filter.Scope `json:"scope,omitempty"`
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"` Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
@@ -225,8 +242,10 @@ type DockerPortGuardOperation struct {
} }
type FirewallRuleAdopt struct { type FirewallRuleAdopt struct {
Scope filter.Scope `json:"scope" validate:"required"` Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"` InstanceKey string `json:"instanceKey,omitempty" validate:"omitempty,max=128"`
Rule *filter.FirewallRule `json:"rule,omitempty"`
Marker string `json:"marker,omitempty" validate:"max=256"`
} }
type FirewallRuleCreateItem struct { type FirewallRuleCreateItem struct {
@@ -313,10 +332,18 @@ type FirewallRuleSyncFailure struct {
} }
type FirewallRuleDelete struct { type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"` UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
}
type FirewallRuleDeleteTarget struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
} }
type FirewallRuleDeleteResponse struct { type FirewallRuleDeleteResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
Succeeded int `json:"succeeded"` Succeeded int `json:"succeeded"`
Failed int `json:"failed"` Failed int `json:"failed"`
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"` Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
+1
View File
@@ -122,6 +122,7 @@ type FileWget struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"` IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"` UseProxy bool `json:"useProxy"`
UseServerFilename bool `json:"useServerFilename"`
} }
type FileMove struct { type FileMove struct {
+27 -198
View File
@@ -1,222 +1,51 @@
package model package model
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
const FirewallRuleSequenceStep int64 = 1 << 32
type DockerPortGuardPolicy struct { type DockerPortGuardPolicy struct {
BaseModel BaseModel
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"` UUID string `gorm:"uniqueIndex" json:"uuid"`
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"` ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"` Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"` HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"` HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"` Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
Mode string `gorm:"size:32;not null" json:"mode"` Mode string `json:"mode"`
Sources string `gorm:"type:text" json:"-"` Sources string `gorm:"type:text" json:"-"`
Description string `gorm:"type:text" json:"description"` Description string `gorm:"type:text" json:"description"`
NativeAction string `gorm:"size:32;not null;default:''" json:"-"` NativeAction string `gorm:"default:''" json:"-"`
NativeRules string `gorm:"type:text" json:"-"` NativeRules string `gorm:"type:text" json:"-"`
Sequence int64 `gorm:"not null;default:0" json:"-"` Sequence int64 `gorm:"default:0" json:"-"`
} }
type ForwardingRule struct { type ForwardingRule struct {
BaseModel BaseModel
Family string `gorm:"size:16;not null;uniqueIndex:idx_forwarding_rule_identity" json:"family"` Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_forwarding_rule_identity" json:"protocol"` Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
Port string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"port"` Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"`
TargetIP string `gorm:"size:64;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"` TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
TargetPort string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"` TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
Interface string `gorm:"size:32;not null;default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"` Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
} }
type FirewallRule struct { type FirewallRule struct {
UUID string `gorm:"size:64;primaryKey" json:"uuid"` UUID string `gorm:"primaryKey" json:"uuid"`
Family string `gorm:"size:16;not null" json:"family"` Family string `json:"family"`
Protocol string `gorm:"size:32;not null" json:"protocol"` Protocol string `json:"protocol"`
SourceAddress string `gorm:"size:255" json:"sourceAddress"` SourceAddress string `json:"sourceAddress"`
SourcePort string `gorm:"size:64" json:"sourcePort"` SourcePort string `json:"sourcePort"`
DestinationAddress string `gorm:"size:255" json:"destinationAddress"` DestinationAddress string `json:"destinationAddress"`
DestinationPort string `gorm:"size:64" json:"destinationPort"` DestinationPort string `json:"destinationPort"`
Interface string `gorm:"size:128" json:"interface"` Interface string `json:"interface"`
ConnectionStates string `gorm:"type:text" json:"connectionStates"` ConnectionStates string `gorm:"type:text" json:"connectionStates"`
Action string `gorm:"size:32;not null" json:"action"` Action string `json:"action"`
Description string `gorm:"type:text" json:"description"` Description string `gorm:"type:text" json:"description"`
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"` CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
Priority *int `json:"priority,omitempty"` Priority *int `json:"priority,omitempty"`
Sequence *int64 `gorm:"index" json:"sequence,omitempty"` Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
Origin string `gorm:"size:32;not null" json:"origin"` Origin string `json:"origin"`
Owner string `gorm:"size:320;not null" json:"owner"` Owner string `json:"owner"`
Revision uint `gorm:"not null;default:1" json:"revision"` Revision uint `gorm:"default:1" json:"revision"`
}
func FirewallRuleOwner(sourceKind, sourceID string) string {
sourceKind = strings.TrimSpace(sourceKind)
sourceID = strings.TrimSpace(sourceID)
if sourceID == "" {
return sourceKind
}
return sourceKind + ":" + sourceID
}
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
normalized, err := filter.NormalizeRule(rule)
if err != nil {
return FirewallRule{}, err
}
switch normalized.NativeKind {
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
default:
return FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
}
record := 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
}
return record, nil
}
func (rule FirewallRule) PolicyKey() 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 (rule FirewallRule) RulesForProvider(provider filter.Provider) ([]filter.FirewallRule, error) {
if rule.CompatibilityError != "" {
return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError)
}
connectionStates := make([]string, 0)
if rule.ConnectionStates != "" {
connectionStates = strings.Split(rule.ConnectionStates, ",")
}
base := filter.FirewallRule{
Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
Interface: rule.Interface, ConnectionStates: connectionStates,
Action: filter.Action(rule.Action), Description: rule.Description,
}
if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
base.Protocol = "tcp/udp"
}
if provider == filter.ProviderFirewalld {
base.Priority = rule.Priority
}
families := []filter.Family{filter.Family(rule.Family)}
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
hasIPv4, hasIPv6 := ruleAddressFamilies(base)
switch {
case hasIPv4 && hasIPv6:
return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope)
case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"):
families = []filter.Family{filter.FamilyIPv6}
case hasIPv4:
families = []filter.Family{filter.FamilyIPv4}
default:
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
}
}
result := make([]filter.FirewallRule, 0, len(families))
for _, family := range families {
compiled := base
compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
switch provider {
case filter.ProviderIptables, filter.ProviderNftables:
compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain
case filter.ProviderFirewalld:
compiled.Scope.Zone = filter.FirewalldInputZone
case filter.ProviderUFW:
compiled.Scope.Chain = filter.UFWInputChain
default:
return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider)
}
expanded, err := filter.ExpandAtomicRules(compiled)
if err != nil {
return nil, err
}
result = append(result, expanded...)
}
return result, nil
}
func SortFirewallRules(rules []FirewallRule, provider filter.Provider) {
sort.SliceStable(rules, func(i, j int) bool {
left, right := rules[i], rules[j]
if provider == filter.ProviderFirewalld {
switch {
case left.Priority == nil && right.Priority != nil:
return false
case left.Priority != nil && right.Priority == nil:
return true
case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority:
return *left.Priority < *right.Priority
}
} else {
switch {
case left.Sequence == nil && right.Sequence != nil:
return false
case left.Sequence != nil && right.Sequence == nil:
return true
case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence:
return *left.Sequence < *right.Sequence
}
}
return left.UUID < right.UUID
})
}
func ruleAddressFamilies(rule filter.FirewallRule) (bool, bool) {
hasIPv4, hasIPv6 := false, false
for _, address := range []string{rule.SourceAddress, rule.DestinationAddress} {
address = strings.TrimSpace(address)
if address == "" {
continue
}
if strings.Contains(address, ":") {
hasIPv6 = true
} else {
hasIPv4 = true
}
}
return hasIPv4, hasIPv6
} }
-27
View File
@@ -11,10 +11,8 @@ import (
type IDockerPortGuardRepo interface { type IDockerPortGuardRepo interface {
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error) ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
DeleteBatch(context.Context, []string) error DeleteBatch(context.Context, []string) error
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
} }
type DockerPortGuardRepo struct{} type DockerPortGuardRepo struct{}
@@ -30,15 +28,6 @@ func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPo
return policies, err return policies, err
} }
func (r *DockerPortGuardRepo) ListRuntimeReadOnly(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", true).
Order("family, sequence, host_ip, host_port, protocol").
Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error { func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
return global.DB.WithContext(ctx). return global.DB.WithContext(ctx).
Where("read_only = ? AND uuid IN ?", false, uuids). Where("read_only = ? AND uuid IN ?", false, uuids).
@@ -59,19 +48,3 @@ func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.
return nil return nil
}) })
} }
func (r *DockerPortGuardRepo) ReplaceRuntimeReadOnly(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("read_only = ?", true).
Delete(&model.DockerPortGuardPolicy{}).Error; err != nil {
return err
}
if len(policies) == 0 {
return nil
}
for i := range policies {
policies[i].ReadOnly = true
}
return tx.Create(&policies).Error
})
}
+52 -15
View File
@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
var ( var (
@@ -23,18 +24,14 @@ type IFirewallRuleRepo interface {
List(context.Context, ...DBOption) ([]model.FirewallRule, error) List(context.Context, ...DBOption) ([]model.FirewallRule, error)
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
DeleteWithRevision(context.Context, string, uint) error DeleteWithRevision(context.Context, string, uint) error
DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error
SaveResetOrder(context.Context, []model.FirewallRule) error
} }
type FirewallRuleRepo struct { type FirewallRuleRepo struct {
db *gorm.DB db *gorm.DB
} }
func WithFirewallRuleSource(kind, id string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
}
}
func NewIFirewallRuleRepo() IFirewallRuleRepo { func NewIFirewallRuleRepo() IFirewallRuleRepo {
return &FirewallRuleRepo{} return &FirewallRuleRepo{}
} }
@@ -93,6 +90,51 @@ func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID stri
return nil 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 { func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
return firewallDB(ctx, r.db) return firewallDB(ctx, r.db)
} }
@@ -133,18 +175,13 @@ func prepareFirewallRule(rule *model.FirewallRule) error {
} }
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} { func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
result := cloneUpdates(updates) result := make(map[string]interface{}, len(updates)+1)
for key, value := range updates {
result[key] = value
}
delete(result, "id") delete(result, "id")
delete(result, "uuid") delete(result, "uuid")
delete(result, "revision") delete(result, "revision")
delete(result, "created_at") delete(result, "created_at")
return result return result
} }
func cloneUpdates(updates map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{}, len(updates)+1)
for key, value := range updates {
result[key] = value
}
return result
}
+14 -12
View File
@@ -5,12 +5,12 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
) )
type IForwardingRuleRepo interface { type IForwardingRuleRepo interface {
List(context.Context) ([]model.ForwardingRule, error) List(context.Context) ([]model.ForwardingRule, error)
ReplaceAll(context.Context, []model.ForwardingRule) error CreateBatch(context.Context, []model.ForwardingRule) error
DeleteBatch(context.Context, []uint) error
} }
type ForwardingRuleRepo struct{} type ForwardingRuleRepo struct{}
@@ -23,14 +23,16 @@ func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule,
return rules, err return rules, err
} }
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error { func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if len(rules) == 0 {
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil { return nil
return err }
} return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error
if len(rules) == 0 { }
return nil
} func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error {
return tx.Create(&rules).Error if len(ids) == 0 {
}) return nil
}
return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error
} }
+3 -2
View File
@@ -936,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
AccountID: agent.AccountID, AccountID: agent.AccountID,
Model: model, Model: model,
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model), Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
Metadata: extractOpenclawModelMetadata(conf, account, models),
}, nil }, nil
} }
@@ -967,7 +968,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if agent.AgentType != constant.AppOpenclaw { if agent.AgentType != constant.AppOpenclaw {
return fmt.Errorf("%s does not support", agent.AgentType) return fmt.Errorf("%s does not support", agent.AgentType)
} }
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil { if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
return err return err
} }
} }
@@ -1684,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err return err
} }
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID) fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil { if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
return err return err
} }
case constant.AppHermesAgent: case constant.AppHermesAgent:
+4
View File
@@ -1354,6 +1354,10 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
} else if slices.Contains(options, "--dangerously-force-unsafe-install") { } else if slices.Contains(options, "--dangerously-force-unsafe-install") {
args = append(args, "--dangerously-force-unsafe-install") args = append(args, "--dangerously-force-unsafe-install")
} }
// Source confirmation does not grant the selected channel plugin's capabilities.
if slices.Contains(options, "--accept-capabilities") {
args = append(args, "--accept-capabilities")
}
return mgr.Run("docker", args...) return mgr.Run("docker", args...)
} }
+150 -5
View File
@@ -10,6 +10,7 @@ import (
"net/url" "net/url"
"path" "path"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -737,9 +738,11 @@ type modelProvider struct {
} }
type modelEntry struct { type modelEntry struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Input []string `json:"input,omitempty"` Input []string `json:"input,omitempty"`
ContextWindow int `json:"contextWindow,omitempty"`
MaxTokens int `json:"maxTokens,omitempty"`
} }
func requiresOpenclawProviderModels(provider string) bool { func requiresOpenclawProviderModels(provider string) bool {
@@ -767,7 +770,7 @@ type browserConfig struct {
DefaultProfile string `json:"defaultProfile"` DefaultProfile string `json:"defaultProfile"`
} }
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error { func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
if strings.TrimSpace(confDir) == "" { if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required") return fmt.Errorf("config dir is required")
} }
@@ -852,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
} }
conf = initial conf = initial
} else { } else {
preserveOpenclawModelMetadata(conf, cfg.Models)
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil { if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
return err return err
} }
@@ -906,6 +910,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if allowedOrigins != nil { if allowedOrigins != nil {
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins}) setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
} }
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
return err
}
if err := writeOpenclawConfigRaw(configPath, conf); err != nil { if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err return err
} }
@@ -920,6 +927,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order) return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
} }
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
raw, ok := conf["models"]
if !ok {
return nil
}
payload, err := json.Marshal(raw)
if err != nil {
return nil
}
var models modelsConfig
if err := json.Unmarshal(payload, &models); err != nil {
return nil
}
return &models
}
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
current := readOpenclawModelsConfig(conf)
if current == nil || next == nil {
return
}
for providerID, nextProvider := range next.Providers {
currentProvider, ok := current.Providers[providerID]
if !ok {
continue
}
byID := make(map[string]modelEntry, len(currentProvider.Models))
for _, entry := range currentProvider.Models {
byID[entry.ID] = entry
}
for index := range nextProvider.Models {
currentEntry, ok := byID[nextProvider.Models[index].ID]
if !ok {
continue
}
nextProvider.Models[index].Input = currentEntry.Input
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
}
next.Providers[providerID] = nextProvider
}
}
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
configured := readOpenclawModelsConfig(conf)
for _, item := range accountModels {
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
continue
}
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
if configured != nil {
for _, entry := range configured.Providers[providerID].Models {
if entry.ID != inferred.ID {
continue
}
metadata.ContextWindow = entry.ContextWindow
metadata.MaxTokens = entry.MaxTokens
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
if slices.Contains(entry.Input, "image") {
metadata.InputMode = "image"
} else {
metadata.InputMode = "text"
}
}
break
}
}
result = append(result, metadata)
}
return result
}
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
if len(requested) == 0 {
return nil
}
configured := readOpenclawModelsConfig(conf)
if configured == nil {
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
}
accountModels, err := loadAgentAccountModels(account)
if err != nil {
return err
}
available := make(map[string]dto.AgentAccountModel, len(accountModels))
for _, item := range accountModels {
available[item.ID] = item
}
seen := make(map[string]struct{}, len(requested))
for _, metadata := range requested {
item, ok := available[metadata.Model]
if !ok {
return buserr.New("ErrAgentModelNotInAccount")
}
if _, ok := seen[metadata.Model]; ok {
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
}
seen[metadata.Model] = struct{}{}
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
return err
}
provider := configured.Providers[providerID]
found := false
for index := range provider.Models {
if provider.Models[index].ID != inferred.ID {
continue
}
found = true
provider.Models[index].ContextWindow = metadata.ContextWindow
provider.Models[index].MaxTokens = metadata.MaxTokens
switch metadata.InputMode {
case "auto":
provider.Models[index].Input = inferred.Input
case "text":
provider.Models[index].Input = []string{"text"}
case "image":
provider.Models[index].Input = []string{"text", "image"}
default:
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
}
break
}
if !found {
return buserr.New("ErrAgentModelNotInAccount")
}
configured.Providers[providerID] = provider
}
modelsMap, err := structToMap(configured)
if err != nil {
return err
}
conf["models"] = modelsMap
return nil
}
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) { func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
accountModels, err := loadAgentAccountModels(account) accountModels, err := loadAgentAccountModels(account)
if err != nil { if err != nil {
@@ -1041,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
return fmt.Errorf("app install is required") return fmt.Errorf("app install is required")
} }
confDir := path.Join(appInstall.GetPath(), "data", "conf") confDir := path.Join(appInstall.GetPath(), "data", "conf")
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil { if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
return err return err
} }
dataDir := path.Join(appInstall.GetPath(), "data") dataDir := path.Join(appInstall.GetPath(), "data")
+10 -5
View File
@@ -30,10 +30,11 @@ import (
) )
const ( const (
ResourceAlertInterval = 30 ResourceAlertInterval = 30
CheckIntervalSec = 3 CheckIntervalSec = 3
LoadCheckIntervalMin = 5 LoadCheckIntervalMin = 5
sshIPLoginWindow = 30 * time.Minute sshIPLoginWindow = 30 * time.Minute
sslAutoRenewAlertSkipDays = 31
) )
type AlertTaskHelper struct { type AlertTaskHelper struct {
@@ -864,7 +865,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
daysDiff := int(math.Ceil( daysDiff := int(math.Ceil(
ssl.ExpireDate.Sub(currentDate).Hours() / 24, ssl.ExpireDate.Sub(currentDate).Hours() / 24,
)) ))
if daysDiff > 0 && int(cycle) >= daysDiff { if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain) daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate) projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
} }
@@ -872,6 +873,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
return daysDiffMap, projectMap return daysDiffMap, projectMap
} }
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
}
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) { func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
currentDate := time.Now() currentDate := time.Now()
daysDiffMap := make(map[int][]string) daysDiffMap := make(map[int][]string)
+24 -57
View File
@@ -40,14 +40,13 @@ const (
appUpgradeDown appUpgradeDown
appUpgradeMutated appUpgradeMutated
appUpgradeStarted appUpgradeStarted
appUpgradeReady
appUpgradeCommitted appUpgradeCommitted
) )
const composeServiceLabel = "com.docker.compose.service"
var appUpgradeLocks sync.Map var appUpgradeLocks sync.Map
const composeServiceLabel = "com.docker.compose.service"
type appUpgradeSnapshot interface { type appUpgradeSnapshot interface {
Restore() error Restore() error
Cleanup() Cleanup()
@@ -439,15 +438,14 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
t.LogSuccess(logStr) t.LogSuccess(logStr)
u.phase = appUpgradeStarted u.phase = appUpgradeStarted
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady")) containerNames, discoverErr := discoverUpgradeContainerNames(u.candidate, u.envContent)
containerNames, err := waitAppContainersReady(context.Background(), u.candidate) if discoverErr != nil {
if err != nil { t.Logf("WARNING: discover upgraded application containers failed: %v", discoverErr)
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err) } else if len(containerNames) > 0 {
return err u.candidate.ContainerName = strings.Join(containerNames, ",")
} else {
t.Log("WARNING: no containers found for the upgraded application")
} }
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
u.phase = appUpgradeReady
u.candidate.ContainerName = strings.Join(containerNames, ",")
u.candidate.Status = constant.StatusRunning u.candidate.Status = constant.StatusRunning
u.candidate.Message = "" u.candidate.Message = ""
@@ -472,6 +470,11 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil { } else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err return err
} }
if discoverErr == nil && len(containerNames) > 0 {
if syncErr := syncAppInstallStatus(&u.candidate, true); syncErr != nil {
t.Logf("WARNING: sync upgraded application status failed: %v", syncErr)
}
}
u.phase = appUpgradeCommitted u.phase = appUpgradeCommitted
u.deleteOldImages(t) u.deleteOldImages(t)
return nil return nil
@@ -591,9 +594,6 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
} }
func (u *appUpgradeContext) finishRollback() error { func (u *appUpgradeContext) finishRollback() error {
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
return err
}
restored := u.original restored := u.original
if err := appInstallRepo.Save(context.Background(), &restored); err != nil { if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
return err return err
@@ -881,28 +881,7 @@ func (s *upgradeFileSnapshot) Cleanup() {
} }
} }
type appContainerReadinessClient interface { func discoverUpgradeContainerNames(install model.AppInstall, envContent []byte) ([]string, error) {
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
ContainerInspect(context.Context, string) (container.InspectResponse, error)
}
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
return waitAppContainersReadyWithClient(ctx, client, install)
}
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
envContent, err := os.ReadFile(install.GetEnvPath())
if err != nil {
envContent, err = renderUpgradeEnv(&install, nil)
if err != nil {
return nil, err
}
}
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false) project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -916,36 +895,24 @@ func waitAppContainersReadyWithClient(ctx context.Context, client appContainerRe
if len(expectedServices) == 0 { if len(expectedServices) == 0 {
return strings.Split(install.ContainerName, ","), nil return strings.Split(install.ContainerName, ","), nil
} }
options := container.ListOptions{ client, err := docker.NewDockerClient()
All: true, if err != nil {
Filters: filters.NewArgs( return nil, err
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
),
} }
containers, err := client.ContainerList(ctx, options) defer client.Close()
containers, err := client.ContainerList(context.Background(), container.ListOptions{
All: true,
Filters: filters.NewArgs(filters.Arg("label", composeWorkdirLabel+"="+install.GetPath())),
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
foundServices := make(map[string]bool, len(expectedServices))
containerNames := make([]string, 0, len(containers)) containerNames := make([]string, 0, len(containers))
for _, item := range containers { for _, item := range containers {
serviceName := item.Labels[composeServiceLabel] if _, ok := expectedServices[item.Labels[composeServiceLabel]]; ok && len(item.Names) > 0 {
if _, ok := expectedServices[serviceName]; !ok {
continue
}
if err = waitContainerReady(ctx, client, item.ID); err != nil {
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
}
foundServices[serviceName] = true
if len(item.Names) > 0 {
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/")) containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
} }
} }
for serviceName := range expectedServices {
if !foundServices[serviceName] {
return nil, fmt.Errorf("container for service %s was not created", serviceName)
}
}
sort.Strings(containerNames) sort.Strings(containerNames)
return containerNames, nil return containerNames, nil
} }
+23 -10
View File
@@ -352,22 +352,34 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
if dir != nil { if dir != nil {
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App") logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
t.Log(logStr) t.Log(logStr)
cleanupFailed := false
if deleteReq.UseLifecycleScripts { if deleteReq.UseLifecycleScripts {
if err = runScript(t, &install, "uninstall"); err != nil { if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
return err cleanupFailed = true
if !deleteReq.ForceDelete {
return scriptErr
}
} }
} else { } else {
out, err := compose.Down(install.GetComposePath()) out, downErr := compose.Down(install.GetComposePath())
if err != nil && !deleteReq.ForceDelete { if downErr != nil {
return handleErr(install, err, out) cleanupFailed = true
if !deleteReq.ForceDelete {
return handleErr(install, downErr, out)
}
} }
if err = runScript(t, &install, "uninstall"); err != nil { if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
_, _ = compose.Up(install.GetComposePath()) cleanupFailed = true
return err if !deleteReq.ForceDelete {
_, _ = compose.Up(install.GetComposePath())
return scriptErr
}
} }
} }
t.LogSuccess(logStr) if !cleanupFailed {
t.LogSuccess(logStr)
}
if deleteReq.DeleteImage { if deleteReq.DeleteImage {
content, err := op.GetContent(install.GetEnvPath()) content, err := op.GetContent(install.GetEnvPath())
if err != nil { if err != nil {
@@ -466,8 +478,9 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
} }
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil) uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil)
go func() { go func() {
if err := uninstallTask.Execute(); err != nil && !deleteReq.ForceDelete { if err := uninstallTask.Execute(); err != nil {
install.Status = constant.StatusError install.Status = constant.StatusError
install.Message = err.Error()
_ = appInstallRepo.Save(context.Background(), &install) _ = appInstallRepo.Save(context.Background(), &install)
} }
}() }()
+2
View File
@@ -464,12 +464,14 @@ func loadDiskInfo() []dto.DiskInfo {
cmd.PipeCommand{Name: "awk", Args: []string{format}}, cmd.PipeCommand{Name: "awk", Args: []string{format}},
) )
if err != nil { 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)) cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunPipe( stdout, err = cmdMgr2.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}}, cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}}, cmd.PipeCommand{Name: "awk", Args: []string{format}},
) )
if err != nil { if err != nil {
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
return datas return datas
} }
} }
+9 -1
View File
@@ -18,6 +18,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
) )
const dockerNftablesMinVersion = "29.0.0" const dockerNftablesMinVersion = "29.0.0"
@@ -82,6 +84,11 @@ func (u *DockerService) UpdateFirewallBackend(backend string) error {
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) { if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion) return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
} }
if backend == constant.FirewallProviderNftables {
if err := dockerfirewall.CheckIPv4Forwarding(); err != nil {
return err
}
}
original, readErr := os.ReadFile(constant.DaemonJsonPath) original, readErr := os.ReadFile(constant.DaemonJsonPath)
existed := readErr == nil existed := readErr == nil
@@ -276,7 +283,8 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
delete(daemonMap, "ipv6") delete(daemonMap, "ipv6")
delete(daemonMap, "fixed-cidr-v6") delete(daemonMap, "fixed-cidr-v6")
delete(daemonMap, "ip6tables") delete(daemonMap, "ip6tables")
if configuredDockerFirewallBackend() != constant.FirewallProviderNftables { backend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if !strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
delete(daemonMap, "experimental") delete(daemonMap, "experimental")
} }
} }
+3 -2
View File
@@ -37,8 +37,9 @@ var (
clamRepo = repo.NewIClamRepo() clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo() monitorRepo = repo.NewIMonitorRepo()
settingRepo = repo.NewISettingRepo() settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo() forwardingRuleRepo = repo.NewIForwardingRuleRepo()
backupRepo = repo.NewIBackupRepo()
websiteRepo = repo.NewIWebsiteRepo() websiteRepo = repo.NewIWebsiteRepo()
websiteDomainRepo = repo.NewIWebsiteDomainRepo() websiteDomainRepo = repo.NewIWebsiteDomainRepo()
+29 -9
View File
@@ -439,13 +439,15 @@ func (f *FileService) Compress(c request.FileCompress) error {
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil { if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
return err return err
} }
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1) taskName := i18n.GetMsgWithMap("FileTaskCompress", map[string]interface{}{"dst": strconv.Quote(filepath.Join(c.Dst, c.Name))})
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
if err != nil { if err != nil {
return err return err
} }
go func() { go func() {
taskItem.AddSubTask(c.Name, func(t *task.Task) error { taskItem.AddSubTask(taskName, func(t *task.Task) error {
t.LogStart(c.Name) logFileTaskSources(t, c.Files)
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
compressType := files.CompressType(c.Type) compressType := files.CompressType(c.Type)
dstFile := filepath.Join(c.Dst, c.Name) dstFile := filepath.Join(c.Dst, c.Name)
success := false success := false
@@ -516,13 +518,15 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil { if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
return err return err
} }
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1) taskName := i18n.GetMsgWithMap("FileTaskDecompress", map[string]interface{}{"dst": strconv.Quote(c.Dst)})
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
if err != nil { if err != nil {
return err return err
} }
go func() { go func() {
taskItem.AddSubTask(c.Path, func(t *task.Task) error { taskItem.AddSubTask(taskName, func(t *task.Task) error {
t.LogStart(c.Path) logFileTaskSources(t, []string{c.Path})
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
dstExisted := fo.Stat(c.Dst) dstExisted := fo.Stat(c.Dst)
parentDir := filepath.Dir(c.Dst) parentDir := filepath.Dir(c.Dst)
if !fo.Stat(parentDir) { if !fo.Stat(parentDir) {
@@ -896,6 +900,7 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
key := "file-wget-" + common.GetUuid() key := "file-wget-" + common.GetUuid()
options := files.DownloadOptions{ options := files.DownloadOptions{
IgnoreCertificate: w.IgnoreCertificate, IgnoreCertificate: w.IgnoreCertificate,
UseServerFilename: w.UseServerFilename,
} }
if w.UseProxy { if w.UseProxy {
systemProxy, err := NewISettingService().GetSystemProxy() systemProxy, err := NewISettingService().GetSystemProxy()
@@ -913,6 +918,12 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options) return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
} }
func logFileTaskSources(t *task.Task, sources []string) {
for _, source := range sources {
t.Log(i18n.GetMsgWithMap("FileTaskSource", map[string]interface{}{"path": strconv.Quote(source)}))
}
}
func (f *FileService) MvFile(m request.FileMove) error { func (f *FileService) MvFile(m request.FileMove) error {
fo := files.NewFileOp() fo := files.NewFileOp()
if err := validateFileMove(fo, m); err != nil { if err := validateFileMove(fo, m); err != nil {
@@ -924,15 +935,24 @@ func (f *FileService) MvFile(m request.FileMove) error {
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) { if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
return buserr.New("TaskIsExecuting") return buserr.New("TaskIsExecuting")
} }
taskItem, err := task.NewTask(m.NewPath, task.TaskExec, task.TaskScopeTask, m.TaskID, 1) nameKey := "FileTaskCopy"
if m.Type == "cut" {
nameKey = "FileTaskMove"
}
taskName := i18n.GetMsgWithMap(nameKey, map[string]interface{}{"dst": strconv.Quote(m.NewPath)})
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
if err != nil { if err != nil {
fileTransferLocks.Release(m.TaskID) fileTransferLocks.Release(m.TaskID)
return err return err
} }
go func() { go func() {
defer fileTransferLocks.Release(m.TaskID) defer fileTransferLocks.Release(m.TaskID)
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error { taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error {
t.LogStart(m.NewPath) logFileTaskSources(t, m.OldPaths)
logFileTaskSources(t, m.CoverPaths)
if m.Name != "" {
t.Log(i18n.GetMsgWithMap("FileTaskRename", map[string]interface{}{"name": strconv.Quote(m.Name)}))
}
err := f.moveFileWithContext(t.TaskCtx, m) err := f.moveFileWithContext(t.TaskCtx, m)
if err != nil && t.TaskCtx.Err() != nil { if err != nil && t.TaskCtx.Err() != nil {
return t.TaskCtx.Err() return t.TaskCtx.Err()
+1061 -2288
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-144
View File
@@ -1,144 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"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"
)
type panelPortWhitelistKey struct{}
func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error {
if oldPort == 0 || oldPort > 65535 || port == 0 || port > 65535 {
return fmt.Errorf("invalid panel port transition %d -> %d", oldPort, port)
}
if LoadPanelPort() != strconv.Itoa(int(oldPort)) {
return fmt.Errorf("panel port changed before firewall update")
}
if oldPort == port {
return nil
}
client, err := s.baseClient()
if err != nil {
if configuredSystemFirewallBackend() == "" && len(lifecycle.InstalledProviders()) == 0 {
return nil
}
return err
}
provider := client.Name()
active, err := client.Status()
if err != nil {
return err
}
if !supportsManagedFilterChains(provider) && !active {
return nil
}
required, err := loadRequiredFirewallPorts(strconv.Itoa(int(port)))
if err != nil {
return err
}
managedChains := supportsManagedFilterChains(provider)
if managedChains {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
}
configured, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
return err
}
protected := firewall.NormalizePortWhitelist(append(configured, required...))
if managedChains {
prepared := append([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}}, required...)
if err := syncPanelRequiredPorts(provider, prepared); err != nil {
return err
}
if err := syncPanelRequiredPorts(provider, required); err != nil {
warnPanelPortCleanupFailure(oldPort, err)
return nil
}
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
return nil
}
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(port)), Protocol: "tcp"}})
for _, port := range ports {
if err := s.ensureSystemPort(ctx, port); err != nil {
return err
}
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
return nil
}
// cleanupPanelPortLocked removes the old system-owned policy as well as any
// remaining managed runtime rule. The caller must hold firewallRuleMutationMu.
func (s *FirewallService) cleanupPanelPortLocked(ctx context.Context, provider string, oldPort uint, protected []firewall.PortWhitelist) error {
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, protected)
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}})
if provider == constant.FirewallProviderUFW {
for _, port := range ports {
port.Protocol = "all"
ports = append(ports, port)
}
}
// Include family-neutral records left by firewalld or older versions, even
// when the selected backend has since changed.
ports = append(ports, dto.FirewallSystemPort{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"})
var cleanupErrors []error
for _, port := range ports {
if panelPortStillRequired(port, protected) {
continue
}
records, err := s.systemPortRecords(ctx, port)
if err != nil {
cleanupErrors = append(cleanupErrors, err)
continue
}
for _, record := range records {
if err := s.deleteRule(ctx, record.UUID, true); err != nil && !errors.Is(err, filter.ErrProtectedRule) {
cleanupErrors = append(cleanupErrors, err)
}
}
}
return errors.Join(cleanupErrors...)
}
func warnPanelPortCleanupFailure(port uint, err error) {
if err != nil && global.LOG != nil {
global.LOG.Warnf("clean up old panel firewall port %d failed: %v", port, err)
}
}
func panelPortStillRequired(port dto.FirewallSystemPort, protected []firewall.PortWhitelist) bool {
rule := systemPortRule(filter.ProviderUFW, port)
for _, required := range protected {
if port.Family != "" && required.Family != "" && port.Family != required.Family {
continue
}
other := rule
other.Protocol, other.DestinationPort = required.Protocol, required.Port
if filter.RulesOverlap(rule, other) {
return true
}
}
return false
}
func panelRuleStillRequired(rule filter.FirewallRule, protected []firewall.PortWhitelist) bool {
family := string(rule.Scope.Family)
if rule.Scope.Family == filter.FamilyInet {
family = ""
}
return panelPortStillRequired(dto.FirewallSystemPort{
Family: family, Port: rule.DestinationPort, Protocol: rule.Protocol,
}, protected)
}
-76
View File
@@ -1,76 +0,0 @@
package service
import (
"context"
"fmt"
"io"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
)
const (
firewallTaskHost = "FirewallTaskHost"
firewallTaskForwarding = "FirewallTaskForwarding"
firewallTaskDocker = "FirewallTaskDocker"
)
func firewallTaskName(operation, subsystem, backend string) string {
name := i18n.GetMsgByKey(subsystem)
if backend != "" {
name += " · " + backend
}
key := "FirewallRule" + operation
if operation == task.TaskExec {
key = "FirewallTaskInitialize"
}
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
}
func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
t.Logf("rules=%d", len(labels))
err := t.TaskCtx.Err()
if err == nil {
err = apply(t.TaskCtx)
}
succeeded, failed := 0, 0
for _, label := range labels {
if err != nil {
failed++
t.LogFailedWithErr(label, err)
} else {
succeeded++
t.LogSuccess(label)
}
}
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func closeUnstartedFirewallTask(t *task.Task) {
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(t.TaskID)
if closer, ok := t.Logger.Out.(io.Closer); ok {
_ = closer.Close()
}
}
-65
View File
@@ -1,65 +0,0 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
func selectedDockerFirewallBackend(fallback string) string {
selected := configuredDockerFirewallBackend()
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected
}
fallback = strings.ToLower(strings.TrimSpace(fallback))
if fallback == constant.FirewallProviderNftables {
return fallback
}
return constant.FirewallProviderIptables
}
func configuredDockerFirewallBackend() string {
if global.DB == nil {
return ""
}
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
selected = strings.ToLower(strings.TrimSpace(selected))
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected
}
return ""
}
func selectedSystemFirewallClient() (lifecycle.Client, error) {
if provider := configuredSystemFirewallBackend(); provider != "" {
return lifecycle.NewClientFor(provider)
}
client, err := lifecycle.NewClient()
if err != nil {
return nil, err
}
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
return client, nil
}
func configuredSystemFirewallBackend() string {
if global.DB == nil {
return ""
}
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
return strings.TrimSpace(provider)
}
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
return selectedSystemFirewallClient()
}
func selectedSystemFirewallProvider() (string, error) {
client, err := selectedSystemFirewallClient()
if err != nil {
return "", err
}
return client.Name(), nil
}
+200 -342
View File
@@ -5,240 +5,138 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"slices"
"strings"
"sync" "sync"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
"gorm.io/gorm" "gorm.io/gorm"
) )
type IFirewallSettingService interface { type IFirewallSettingService interface {
QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error) CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error) Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error Operate(context.Context, dto.FirewallBackendOperation) error
} }
type FirewallSettingService struct{} type FirewallSettingService struct{}
var firewallWhitelistTaskMu sync.Mutex var firewallWhitelistMu sync.Mutex
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required") func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
firewallWhitelistMu.Lock()
func firewallBackendCleanupRequired(current, target string) error { defer firewallWhitelistMu.Unlock()
return fmt.Errorf(
"%w: current backend %s still contains 1Panel runtime rules; clean it up before switching to %s",
ErrFirewallBackendCleanupRequired,
current,
target,
)
}
func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
func (s *FirewallSettingService) QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error) {
return s.queuePortWhitelist(value, newFirewallService())
}
func (s *FirewallSettingService) queuePortWhitelist(value string, firewallService *FirewallService) (dto.FilterChainOperationResponse, error) {
firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock()
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem, err := task.NewTask(i18n.GetMsgByKey("FirewallWhitelistTask"), task.TaskUpdate, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
succeeded, failed := 0, 0
err := s.applyPortWhitelist(t.TaskCtx, value, firewallService, func(status, label string, err error) {
switch status {
case "applied":
succeeded++
t.LogSuccess(label)
case "failed":
failed++
t.LogFailedWithErr(label, err)
default:
t.Log(i18n.GetWithName(status, label))
}
})
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall whitelist task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
type whitelistReporter func(status, label string, err error)
func (s *FirewallSettingService) applyPortWhitelist(ctx context.Context, value string, firewallService *FirewallService, report whitelistReporter) error {
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer firewallRuleMutationMu.Unlock()
if err := ctx.Err(); err != nil { return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return err current, err := loadPortWhitelistSetting(tx)
}
ports, err := firewall.ParsePortWhitelist(value)
if err != nil {
return err
}
required, err := firewallService.requiredPorts()
if err != nil {
return err
}
removed, err := s.savePortWhitelist(ctx, ports, required, firewallService)
if err != nil {
return err
}
report("FirewallWhitelistSaved", "", nil)
for _, port := range systemPorts(removed) {
report("FirewallWhitelistReleased", whitelistPortLabel(port), nil)
}
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, required)
provider, providerErr := firewallService.selectedProvider(ctx)
ready := s.portWhitelistReadiness(provider, providerErr, firewallService)
return syncPortWhitelist(ctx, ports, required, ready, firewallService.ensureSystemPortLocked, report)
}
func (s *FirewallSettingService) savePortWhitelist(ctx context.Context, ports, required []firewall.PortWhitelist, firewallService *FirewallService) ([]firewall.PortWhitelist, error) {
var removed []firewall.PortWhitelist
err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var setting model.Setting
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return err
}
previous, err := firewall.ParsePortWhitelist(setting.Value)
if err != nil { if err != nil {
return err return err
} }
removed = excludeFirewallPorts(excludeFirewallPorts(previous, ports), required) current = append(current, request.Rule)
txCtx := context.WithValue(ctx, constant.DB, tx) current, err = firewall.ValidatePortWhitelist(current)
if err := firewallService.releaseSystemPorts(txCtx, systemPorts(removed)); err != nil {
return err
}
value, err := json.Marshal(ports)
if err != nil { if err != nil {
return err return err
} }
return tx.Where("key = ?", constant.FirewallPortWhiteList). value, err := json.Marshal(current)
Assign(map[string]interface{}{"value": string(value)}). if err != nil {
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error 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
}) })
return removed, err
} }
func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider, providerErr error, firewallService *FirewallService) func(dto.FirewallSystemPort) (bool, error) { func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
type state struct { firewallWhitelistMu.Lock()
ready bool defer firewallWhitelistMu.Unlock()
err error firewallRuleMutationMu.Lock()
} defer firewallRuleMutationMu.Unlock()
states := make(map[string]state) return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return func(port dto.FirewallSystemPort) (bool, error) { current, err := loadPortWhitelistSetting(tx)
if providerErr != nil {
return false, providerErr
}
key := "service"
if isDirectFirewallProvider(provider) {
key = port.Family
}
if cached, ok := states[key]; ok {
return cached.ready, cached.err
}
var result state
if isDirectFirewallProvider(provider) {
initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), port.Family)
result = state{ready: initialized && bound, err: err}
} else {
client, err := firewallService.baseClient()
result.err = err
if err == nil {
result.ready, result.err = client.Status()
}
}
states[key] = result
return result.ready, result.err
}
}
func syncPortWhitelist(
ctx context.Context,
ports, required []firewall.PortWhitelist,
ready func(dto.FirewallSystemPort) (bool, error),
ensure func(context.Context, dto.FirewallSystemPort) error,
report whitelistReporter,
) error {
var failures []error
for _, port := range systemPorts(ports) {
if err := ctx.Err(); err != nil {
return errors.Join(append(failures, err)...)
}
label := whitelistPortLabel(port)
if containsFirewallPort(required, firewall.PortWhitelist{Family: port.Family, Port: port.Port, Protocol: port.Protocol}) {
report("FirewallWhitelistRequired", label, nil)
continue
}
active, err := ready(port)
if err == nil && !active {
report("FirewallWhitelistDeferred", label, nil)
continue
}
if err == nil {
err = ensure(ctx, port)
}
if err != nil { if err != nil {
report("failed", label, err) return err
failures = append(failures, fmt.Errorf("%s: %w", label, err))
continue
} }
report("applied", label, nil) index, err := findPortWhitelistRule(current, request.OldRule)
} if err != nil {
return errors.Join(failures...) 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
})
} }
func whitelistPortLabel(port dto.FirewallSystemPort) string { func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
return fmt.Sprintf("%s %s/%s", port.Family, port.Port, port.Protocol) 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
})
} }
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) { func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()} result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
result.PortWhitelist = ports
} else {
result.PortWhitelist = constant.FirewallPortWhiteListValue
}
installed := make(map[string]bool) installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() { for _, name := range lifecycle.InstalledProviders() {
installed[name] = true installed[name] = true
} }
result.System.Selected = configuredSystemFirewallBackend() systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
result.System.Selected = strings.TrimSpace(systemBackend)
if result.System.Selected == "" { if result.System.Selected == "" {
if client, err := lifecycle.NewClient(); err == nil { if client, err := lifecycle.NewClient(""); err == nil {
result.System.Selected = client.Name() result.System.Selected = client.Name()
} }
} }
@@ -251,16 +149,16 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
} { } {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true} option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.System.Selected { if option.Installed && name == result.System.Selected {
client, err := lifecycle.NewClientFor(name) client, err := lifecycle.NewClient(name)
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} else if supportsManagedFilterChains(name) { } else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base") overview, err := loadSystemFirewallOverview(name, "base")
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} }
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4) option.Initialized, option.Bound = overview.IsInit, overview.IsBind
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6) option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
} else if option.Active, err = client.Status(); err != nil { } else if option.Active, err = client.Status(); err != nil {
option.Message = err.Error() option.Message = err.Error()
} }
@@ -273,32 +171,29 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
result.System.Options = append(result.System.Options, option) result.System.Options = append(result.System.Options, option)
} }
result.Forwarding.Selected = configuredForwardingBackend() forwardingBackend, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
result.Forwarding.Selected = strings.TrimSpace(forwardingBackend)
if result.Forwarding.Selected == "" {
result.Forwarding.Selected = constant.FirewallProviderIptables
}
result.Forwarding.Current = result.Forwarding.Selected result.Forwarding.Current = result.Forwarding.Selected
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} { for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true} option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.Forwarding.Selected { if option.Installed && name == result.Forwarding.Selected {
manager, err := newForwardingManagerFor(name) manager, err := newForwardingAdapterFor(name)
if err != nil { if err != nil {
option.Message = err.Error() option.Message = err.Error()
} else if status, err := manager.Status(); err != nil {
option.Message = err.Error()
} else { } else {
option.Initialized, option.Bound = status.IsInit, status.IsBind status, statusErr := loadForwardingFirewallOverview(manager)
ipv4Init, ipv4Bound, ipv4Err := manager.FamilyStatus(constant.FirewallFamilyIPv4) option.IPv4, option.IPv6 = status.IPv4, status.IPv6
ipv6Init, ipv6Bound, ipv6Err := manager.FamilyStatus(constant.FirewallFamilyIPv6) if statusErr != nil {
option.IPv4 = dto.FirewallBackendFamilyStatus{ option.Message = statusErr.Error()
Available: ipv4Err == nil, Initialized: ipv4Init, Bound: ipv4Bound, } else {
option.Initialized, option.Bound = status.IsInit, status.IsBind
} }
option.IPv6 = dto.FirewallBackendFamilyStatus{ if name == constant.FirewallProviderIptables && !option.IPv6.Available {
Available: ipv6Err == nil, Initialized: ipv6Init, Bound: ipv6Bound, if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
} option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
if name == constant.FirewallProviderIptables {
if commands, commandErr := lifecycle.ResolveIptablesCommands(); commandErr == nil {
option.IPv6.Available = option.IPv6.Available && commands.IPv6Available()
if !commands.IPv6Available() {
option.IPv6.Reason = docker_guard.ReasonCommandMissing
}
} }
} }
} }
@@ -316,7 +211,11 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
if dockerInstalled { if dockerInstalled {
dockerVersion = loadDockerEngineVersion(ctx) dockerVersion = loadDockerEngineVersion(ctx)
} }
result.Docker.Selected = configuredDockerFirewallBackend() dockerBackend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
dockerBackend = strings.ToLower(strings.TrimSpace(dockerBackend))
if dockerBackend == constant.FirewallProviderIptables || dockerBackend == constant.FirewallProviderNftables {
result.Docker.Selected = dockerBackend
}
result.Docker.Current = result.Docker.Selected result.Docker.Current = result.Docker.Selected
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} { for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{ option := dto.FirewallBackendOption{
@@ -329,53 +228,42 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
option.Active = false option.Active = false
} }
if option.Active { if option.Active {
guard := docker_guard.NewRuntime(name) guard := newDockerFirewallRuntime(name)
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6) ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6)
option.Initialized = ipv4.Initialized || ipv6.Initialized option.Initialized = ipv4.Initialized || ipv6.Initialized
option.Bound = ipv4.Bound || ipv6.Bound option.Bound = ipv4.Bound || ipv6.Bound
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
} }
result.Docker.Options = append(result.Docker.Options, option) result.Docker.Options = append(result.Docker.Options, option)
} }
var err error
return result, nil result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
} if err != nil {
return result, err
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
switch provider {
case constant.FirewallProviderIptables:
return iptables_helper.LoadFamilyInitStatus(family, "base")
case constant.FirewallProviderNftables:
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
default:
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
} }
} result.PanelPort = LoadPanelPort()
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFamilyStatus { if sshErr != nil {
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 { global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
commands, err := lifecycle.ResolveIptablesCommands() } else {
if err != nil || !commands.IPv6Available() { result.SSHPort = sshPort
return dto.FirewallBackendFamilyStatus{Reason: docker_guard.ReasonCommandMissing}
}
}
initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family)
return dto.FirewallBackendFamilyStatus{
Available: err == nil,
Initialized: initialized,
Bound: bound,
} }
return result, err
} }
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error { func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables { if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem) return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
} }
if request.Subsystem == "system" && !supportsManagedFilterChains(request.Backend) && request.Operation != "select" { if request.Subsystem == "system" && (request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables) && request.Operation != "select" {
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend) return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
} }
switch request.Subsystem { switch request.Subsystem {
@@ -385,10 +273,9 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
} }
if request.Operation == "initialize" { if request.Operation == "initialize" {
service := newFirewallService() service := newFirewallService()
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend)); err != nil { rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
return err whitelistErr := service.SyncPortWhitelist(ctx)
} return errors.Join(rulesErr, whitelistErr)
return service.syncConfiguredFirewallPorts(ctx)
} }
return nil return nil
case "forwarding": case "forwarding":
@@ -400,64 +287,14 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
} }
} }
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error { func NewIFirewallSettingService() IFirewallSettingService {
guard := docker_guard.NewRuntime(request.Backend) return &FirewallSettingService{}
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
if current == "" {
current = alternateDirectBackend(request.Backend)
}
initialized, err := dockerGuardBackendInitialized(current)
if err != nil {
return err
}
if current != request.Backend && initialized {
return firewallBackendCleanupRequired(current, request.Backend)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
return err
}
if request.Operation == "select" {
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
if request.Operation == "initialize" {
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
return nil
}
func dockerGuardBackendInitialized(backend string) (bool, error) {
guard := docker_guard.NewRuntime(backend)
for _, family := range []string{docker_guard.FamilyIPv4, docker_guard.FamilyIPv6} {
initialized, err := guard.Initialized(family)
if err != nil {
return false, err
}
if initialized {
return true, nil
}
}
return false, nil
} }
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error { func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer firewallRuleMutationMu.Unlock()
if _, err := lifecycle.NewClientFor(request.Backend); err != nil { if _, err := lifecycle.NewClient(request.Backend); err != nil {
return err return err
} }
if request.Operation == "cleanup" { if request.Operation == "cleanup" {
@@ -465,7 +302,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
} }
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey) previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
if previous == "" { if previous == "" {
if client, err := lifecycle.NewClient(); err == nil { if client, err := lifecycle.NewClient(""); err == nil {
previous = client.Name() previous = client.Name()
} }
} }
@@ -475,7 +312,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
return err return err
} }
if initialized { if initialized {
return firewallBackendCleanupRequired(previous, request.Backend) return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": previous, "target": request.Backend}, nil)
} }
} }
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil { if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
@@ -501,21 +338,14 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
} }
func systemFirewallBackendInitialized(backend string) (bool, error) { func systemFirewallBackendInitialized(backend string) (bool, error) {
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor) client, err := lifecycle.NewClient(backend)
}
func systemFirewallBackendInitializedWithClientFactory(
backend string,
newClient func(string) (lifecycle.Client, error),
) (bool, error) {
client, err := newClient(backend)
if err != nil { if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) { if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil return false, nil
} }
return false, err return false, err
} }
if supportsManagedFilterChains(backend) { if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} { for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family) initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
@@ -533,30 +363,8 @@ func systemFirewallBackendInitializedWithClientFactory(
return client.Status() return client.Status()
} }
func cleanupSystemBackend(backend string) error {
switch backend {
case constant.FirewallProviderIptables:
return newIptablesHelperManager().Cleanup()
case constant.FirewallProviderNftables:
return newNftablesHelperManager().Cleanup()
default:
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
}
}
func cleanupInactiveSystemBackend(backend string) error {
switch backend {
case constant.FirewallProviderIptables:
return (&iptables_helper.Manager{}).Cleanup()
case constant.FirewallProviderNftables:
return (&nftables_helper.Manager{}).Cleanup()
default:
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
}
}
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error { func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
manager, err := newForwardingManagerFor(request.Backend) manager, err := newForwardingAdapterFor(request.Backend)
if err != nil { if err != nil {
return err return err
} }
@@ -574,7 +382,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
if request.Operation == "select" { if request.Operation == "select" {
current := previous current := previous
if current == "" { if current == "" {
detected, err := newForwardingManager() detected, err := newForwardingAdapter()
if err != nil { if err != nil {
return err return err
} }
@@ -585,7 +393,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
return err return err
} }
if current != request.Backend && initialized { if current != request.Backend && initialized {
return firewallBackendCleanupRequired(current, request.Backend) return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
} }
} }
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil { if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
@@ -599,7 +407,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
} }
func forwardingBackendInitialized(backend string) (bool, error) { func forwardingBackendInitialized(backend string) (bool, error) {
manager, err := newForwardingManagerFor(backend) manager, err := newForwardingAdapterFor(backend)
if err != nil { if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) { if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil return false, nil
@@ -618,9 +426,59 @@ func forwardingBackendInitialized(backend string) (bool, error) {
return false, nil return false, nil
} }
func alternateDirectBackend(backend string) string { func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
if backend == constant.FirewallProviderNftables { guard := newDockerFirewallRuntime(request.Backend)
return constant.FirewallProviderIptables if request.Operation == "cleanup" {
if err := guard.Cleanup(); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
} }
return constant.FirewallProviderNftables previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if request.Operation == "select" {
current := previous
if current == "" {
current = constant.FirewallProviderNftables
if request.Backend == constant.FirewallProviderNftables {
current = constant.FirewallProviderIptables
}
}
initialized, err := dockerGuardBackendInitialized(current)
if err != nil {
return err
}
if current != request.Backend && initialized {
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
return err
}
if request.Operation == "select" {
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
if request.Operation == "initialize" {
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
return nil
}
func dockerGuardBackendInitialized(backend string) (bool, error) {
guard := newDockerFirewallRuntime(backend)
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
initialized, err := guard.Initialized(family)
if err != nil {
return false, err
}
if initialized {
return true, nil
}
}
return false, nil
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+304 -320
View File
@@ -4,9 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
@@ -16,10 +18,17 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n" "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/forwarding"
forwardingproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding/providers"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" )
const (
forwardingSyncConverged = "converged"
forwardingSyncMissing = "missing"
forwardingSyncRuntimeOnly = "runtime_only"
) )
type IForwardingService interface { type IForwardingService interface {
@@ -32,7 +41,7 @@ type IForwardingService interface {
} }
type ForwardingService struct { type ForwardingService struct {
managerFactory func() (*forwarding.Manager, error) clientFactory func() (forwarding.Adapter, error)
rules repo.IForwardingRuleRepo rules repo.IForwardingRuleRepo
enabled func() (bool, error) enabled func() (bool, error)
persistBackend func(string) error persistBackend func(string) error
@@ -40,43 +49,27 @@ type ForwardingService struct {
} }
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected") var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
var forwardingMutationMu sync.Mutex
const ( var forwardingMutationMu sync.Mutex
forwardingSyncConverged = "converged"
forwardingSyncMissing = "missing"
forwardingSyncRuntimeOnly = "runtime_only"
)
var ( var (
forwardingSyncStateMu sync.RWMutex forwardingSyncStateMu sync.RWMutex
forwardingLastSyncErr error forwardingLastSyncErr error
) )
func NewIForwardingService() IForwardingService {
return newForwardingService()
}
func newForwardingService() *ForwardingService {
return &ForwardingService{
managerFactory: newForwardingManager,
rules: repo.NewIForwardingRuleRepo(),
enabled: forwardingPersistedEnabled,
markEnabled: func() error {
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
},
persistBackend: func(backend string) error {
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
},
}
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) { func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
selected := configuredForwardingBackend() selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
baseInfo := dto.FirewallSubsystemStatus{ selected = strings.TrimSpace(selected)
Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(), if selected == "" {
selected = constant.FirewallProviderIptables
} }
manager, err := s.manager() baseInfo := dto.FirewallSubsystemStatus{
Version: "-", Name: selected, Backend: selected, SyncError: lastForwardingSyncError(),
}
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
baseInfo.Name += "-forward"
}
manager, err := s.clientFactory()
if err != nil { if err != nil {
if errors.Is(err, errForwardingBackendUnavailable) { if errors.Is(err, errForwardingBackendUnavailable) {
baseInfo.Reason = constant.FirewallBackendNotInstalled baseInfo.Reason = constant.FirewallBackendNotInstalled
@@ -84,39 +77,41 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
} }
return baseInfo, err return baseInfo, err
} }
status, err := manager.Status() client, err := lifecycle.NewClient(manager.Name())
if err != nil { if err != nil {
return baseInfo, err return baseInfo, err
} }
version, versionErr := client.Version()
status, statusErr := loadForwardingFirewallOverview(manager)
if err := errors.Join(versionErr, statusErr); err != nil {
return baseInfo, err
}
baseInfo.IsExist = true baseInfo.IsExist = true
baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name baseInfo.Name, baseInfo.Backend = manager.Name(), manager.Name()
baseInfo.Version = status.Version if baseInfo.Backend == constant.FirewallProviderIptables || baseInfo.Backend == constant.FirewallProviderNftables {
baseInfo.PingStatus = ping.LoadStatus() baseInfo.Name += "-forward"
}
baseInfo.Version = version
baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
baseInfo.IPv4 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4) baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6) for _, family := range []struct {
command string
status *dto.FirewallBackendFamilyStatus
}{
{"iptables", &baseInfo.IPv4},
{"ip6tables", &baseInfo.IPv6},
} {
policy, err := loadForwardPolicy(family.command)
if err != nil {
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
continue
}
family.status.ForwardPolicy = policy
}
return baseInfo, nil return baseInfo, nil
} }
func loadForwardingFamilyInfo(manager *forwarding.Manager, backend, family string) dto.FirewallBackendFamilyStatus {
initialized, bound, err := manager.FamilyStatus(family)
available := err == nil
if backend == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
commands, commandErr := lifecycle.ResolveIptablesCommands()
available = available && commandErr == nil && commands.IPv6Available()
}
return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound}
}
func forwardingDisplayName(backend string) string {
switch backend {
case constant.FirewallProviderIptables, constant.FirewallProviderNftables:
return backend + "-forward"
default:
return backend
}
}
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) { func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
if request.Strategy != "" { if request.Strategy != "" {
return 0, nil, nil return 0, nil, nil
@@ -125,11 +120,11 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
manager, err := s.manager() manager, err := s.clientFactory()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
runtime, err := manager.List("", "") runtime, err := manager.List()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
@@ -179,24 +174,18 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
return int64(total), items, nil return int64(total), items, nil
} }
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool { func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
values := []string{ count := 0
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP, for _, rule := range request.Rules {
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(), if rule.Operation == "add" {
} count += strings.Count(rule.Protocol, "/") + 1
for _, value := range values { }
if strings.Contains(strings.ToLower(value), keyword) { if count > filter.MaxAtomicExpansion {
return true return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
} }
} }
return false
}
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
labels := make([]string, len(request.Rules))
operation := task.TaskCreate operation := task.TaskCreate
for i, rule := range request.Rules { for _, rule := range request.Rules {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s %s -> %s:%s", i+1, len(request.Rules), rule.Operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
if rule.Operation != "add" { if rule.Operation != "add" {
operation = task.TaskUpdate operation = task.TaskUpdate
} }
@@ -204,48 +193,26 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
if forwardingOperationsOnlyRemove(request.Rules) { if forwardingOperationsOnlyRemove(request.Rules) {
operation = task.TaskDelete operation = task.TaskDelete
} }
return queueFirewallRuleTask(firewallTaskForwarding, operation, labels, func(ctx context.Context) error { taskItem, err := task.NewTask(firewallTaskName(operation, firewallTaskForwarding, ""), operation, task.TaskScopeFirewall, "", 0)
return s.operateRules(ctx, request)
})
}
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
stored, err := s.rules.List(ctx)
if err != nil { if err != nil {
return err return dto.FilterChainOperationResponse{}, err
} }
desired, err := applyForwardingOperations(forwardingRulesFromModels(stored), request.Rules) taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
if errors.Is(err, forwarding.ErrRuleExists) { return s.operateRules(t.TaskCtx, request, t)
return buserr.New("ErrRecordExist") }, nil, 0, 0)
} else if err != nil { if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
return err taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, err
} }
if err := s.rules.ReplaceAll(ctx, forwardingRuleModels(desired)); err != nil { go func() { _ = taskItem.Execute() }()
return err return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
if err := s.reconcile(desired); err != nil {
recordForwardingSyncError(err)
if request.ForceDelete && forwardingOperationsOnlyRemove(request.Rules) {
if global.LOG != nil {
global.LOG.Error(err)
}
return nil
}
return err
}
recordForwardingSyncError(nil)
return nil
} }
func (s *ForwardingService) Enable() error { func (s *ForwardingService) Enable() error {
forwardingMutationMu.Lock() forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock() defer forwardingMutationMu.Unlock()
manager, err := s.manager() manager, err := s.clientFactory()
if err != nil { if err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
@@ -254,7 +221,7 @@ func (s *ForwardingService) Enable() error {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
if err := s.activateManager(manager); err != nil { if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
@@ -263,14 +230,12 @@ func (s *ForwardingService) Enable() error {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
err = manager.Reconcile(forwardingRulesFromModels(rules)) err = manager.ReplaceRules(forwardingRulesFromModels(rules))
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
func (s *ForwardingService) QueueInitialization( func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
request dto.FirewallInitializationTask,
) (dto.FilterChainOperationResponse, error) {
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err return dto.FilterChainOperationResponse{}, err
} }
@@ -278,13 +243,13 @@ func (s *ForwardingService) QueueInitialization(
if err != nil { if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err) return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
} }
var manager *forwarding.Manager var manager forwarding.Adapter
var backend string var backend string
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
forwardingMutationMu.Lock() forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock() defer forwardingMutationMu.Unlock()
var err error var err error
manager, err = s.manager() manager, err = s.clientFactory()
if err != nil { if err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
@@ -295,7 +260,7 @@ func (s *ForwardingService) QueueInitialization(
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
if err := s.activateManager(manager); err != nil { if err := s.initializeForwarding(manager); err != nil {
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
@@ -309,7 +274,7 @@ func (s *ForwardingService) QueueInitialization(
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
} }
err = manager.Reconcile(forwardingRulesFromModels(rules)) err = manager.ReplaceRules(forwardingRulesFromModels(rules))
recordForwardingSyncError(err) recordForwardingSyncError(err)
return err return err
}, nil) }, nil)
@@ -320,131 +285,43 @@ func (s *ForwardingService) QueueInitialization(
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
} }
func (s *ForwardingService) Restore(ctx context.Context) error { func NewIForwardingService() IForwardingService {
forwardingMutationMu.Lock() return newForwardingService()
defer forwardingMutationMu.Unlock() }
enabled, err := s.forwardingEnabled()
if err != nil || !enabled { func loadForwardPolicy(command string) (string, error) {
if err != nil { if !cmd.Which(command) {
recordForwardingSyncError(err) command += "-nft"
if !cmd.Which(command) {
return "", nil
} }
return err
} }
manager, err := s.manager() output, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
if err != nil { if err != nil {
recordForwardingSyncError(err) return "", err
return err
} }
stored, err := s.rules.List(ctx) for _, line := range strings.Split(output, "\n") {
if err != nil { fields := strings.Fields(line)
recordForwardingSyncError(err) if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
return err if fields[2] != "ACCEPT" && fields[2] != "DROP" {
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
}
return fields[2], nil
}
} }
if err := s.activateManager(manager); err != nil { return "", errors.New("FORWARD default policy was not found")
recordForwardingSyncError(err)
return err
}
err = manager.Reconcile(forwardingRulesFromModels(stored))
recordForwardingSyncError(err)
return err
} }
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error { func lastForwardingSyncError() string {
manager, err := s.manager() forwardingSyncStateMu.RLock()
if err != nil { defer forwardingSyncStateMu.RUnlock()
return err if forwardingLastSyncErr == nil {
return ""
} }
return s.reconcileWithManager(manager, rules) return forwardingLastSyncErr.Error()
} }
func (s *ForwardingService) reconcileWithManager(manager *forwarding.Manager, rules []forwarding.Rule) error { func mergeForwardingInventory(stored []model.ForwardingRule, runtime []forwarding.Rule) ([]forwardingInventoryItem, error) {
enabled, err := s.forwardingEnabled()
if err != nil || !enabled {
return err
}
if err := s.activateManager(manager); err != nil {
return err
}
return manager.Reconcile(rules)
}
func (s *ForwardingService) activateManager(manager *forwarding.Manager) error {
if err := s.saveForwardingBackend(manager.Name()); err != nil {
return err
}
return manager.Enable()
}
func (s *ForwardingService) forwardingEnabled() (bool, error) {
if s.enabled != nil {
return s.enabled()
}
return forwardingPersistedEnabled()
}
func (s *ForwardingService) saveForwardingBackend(backend string) error {
if s.persistBackend != nil {
return s.persistBackend(backend)
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
}
func (s *ForwardingService) persistForwardingEnabled() error {
if s.markEnabled != nil {
return s.markEnabled()
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
}
func forwardingPersistedEnabled() (bool, error) {
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
return status == constant.StatusEnable, err
}
func forwardingRulesFromModels(stored []model.ForwardingRule) []forwarding.Rule {
rules := make([]forwarding.Rule, 0, len(stored))
for _, rule := range stored {
rules = append(rules, forwarding.Rule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
})
}
return rules
}
func forwardingRuleModels(rules []forwarding.Rule) []model.ForwardingRule {
stored := make([]model.ForwardingRule, 0, len(rules))
for _, rule := range rules {
stored = append(stored, model.ForwardingRule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
})
}
return stored
}
type forwardingInventoryItem struct {
ID uint
Rule forwarding.Rule
IsDesired bool
IsRuntime bool
}
func (i forwardingInventoryItem) SyncStatus() string {
switch {
case i.IsDesired && i.IsRuntime:
return forwardingSyncConverged
case i.IsDesired:
return forwardingSyncMissing
default:
return forwardingSyncRuntimeOnly
}
}
func mergeForwardingInventory(
stored []model.ForwardingRule,
runtime []forwarding.Rule,
) ([]forwardingInventoryItem, error) {
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime)) items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
byIdentity := make(map[string]int, len(stored)+len(runtime)) byIdentity := make(map[string]int, len(stored)+len(runtime))
for _, record := range stored { for _, record := range stored {
@@ -475,108 +352,215 @@ func mergeForwardingInventory(
return items, nil return items, nil
} }
func recordForwardingSyncError(err error) { func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
forwardingSyncStateMu.Lock() values := []string{
forwardingLastSyncErr = err item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
forwardingSyncStateMu.Unlock() item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
}
func lastForwardingSyncError() string {
forwardingSyncStateMu.RLock()
defer forwardingSyncStateMu.RUnlock()
if forwardingLastSyncErr == nil {
return ""
} }
return forwardingLastSyncErr.Error() for _, value := range values {
} if strings.Contains(strings.ToLower(value), keyword) {
return true
func applyForwardingOperations(current []forwarding.Rule, requested []dto.ForwardRuleOperation) ([]forwarding.Rule, error) {
desired := make([]forwarding.Rule, 0, len(current)+len(requested))
for _, rule := range current {
normalized, err := forwarding.NormalizeRule(rule)
if err != nil {
return nil, fmt.Errorf("normalize persisted forwarding rule: %w", err)
} }
desired = append(desired, normalized)
} }
for _, operation := range requested { 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, "/") { for _, protocol := range strings.Split(operation.Protocol, "/") {
rule, err := forwarding.NormalizeRule(forwarding.Rule{ rule, err := forwarding.NormalizeRule(forwarding.Rule{
Family: operation.Family, Protocol: protocol, Port: operation.Port, TargetIP: operation.TargetIP, Family: operation.Family, Protocol: protocol, Port: operation.Port,
TargetPort: operation.TargetPort, Interface: operation.Interface, TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface,
}) })
if err != nil { if err != nil {
return nil, err return err
} }
index := forwardingRuleIndex(desired, rule) groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule)
switch forwarding.OperationType(operation.Operation) { }
case forwarding.OperationAdd: }
if index >= 0 { stored, err := s.rules.List(ctx)
return nil, forwarding.ErrRuleExists if err != nil {
} return err
desired = append(desired, rule) }
case forwarding.OperationRemove: byIdentity := make(map[string]model.ForwardingRule, len(stored))
if index >= 0 { for index, rule := range forwardingRulesFromModels(stored) {
desired = append(desired[:index], desired[index+1:]...) normalized, err := forwarding.NormalizeRule(rule)
} if err != nil {
default: return err
return nil, fmt.Errorf("unsupported forwarding operation %q", operation.Operation) }
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)
} }
} }
} }
return desired, nil 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 {
func forwardingRuleIndex(rules []forwarding.Rule, wanted forwarding.Rule) int { old[rule.Identity()] = true
wantedIdentity := wanted.Identity() }
for index, rule := range rules { unchanged := len(old) == len(groups[1].rules)
if rule.Identity() == wantedIdentity { duplicate := false
return index 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
} }
} }
return -1 var client forwarding.Adapter
} var failures []error
for _, group := range groups {
func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool { byFamily := make(map[string][]forwarding.Rule, 2)
if len(operations) == 0 { seen := make(map[string]bool, len(group.rules))
return false for _, rule := range group.rules {
} key := rule.Identity()
for _, operation := range operations { _, exists := byIdentity[key]
if operation.Operation != string(forwarding.OperationRemove) { if seen[key] || (group.operation == forwarding.OperationAdd && exists) {
return false 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 true return errors.Join(failures...)
}
func (s *ForwardingService) manager() (*forwarding.Manager, error) {
return s.managerFactory()
}
func newForwardingManager() (*forwarding.Manager, error) {
return newForwardingManagerFor(configuredForwardingBackend())
}
func configuredForwardingBackend() string {
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
selected = strings.TrimSpace(selected)
if selected == "" {
return constant.FirewallProviderIptables
}
return selected
}
func newForwardingManagerFor(backend string) (*forwarding.Manager, error) {
client, err := lifecycle.NewClientFor(backend)
if err != nil {
return nil, fmt.Errorf(
"%w: selected forwarding backend %s: %w",
errForwardingBackendUnavailable, backend, err,
)
}
adapter, err := forwardingproviders.New(client.Name())
if err != nil {
return nil, err
}
return forwarding.NewManager(adapter, client), nil
} }
+31 -24
View File
@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -22,6 +23,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/copier"
csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export" csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/geo" "github.com/1Panel-dev/1Panel/agent/utils/geo"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -217,10 +219,21 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
return err return err
} }
oldPortValue := strings.Join(loadSSHPortValues(directives), ",") oldPortValue := strings.Join(loadSSHPortValues(directives), ",")
if req.Key == "Port" {
if err := checkSSHPortAvailability(splitSSHPorts(oldPortValue), splitSSHPorts(req.NewValue)); err != nil {
return err
}
}
if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil { if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil {
return err return err
} }
if req.Key == "Port" { if req.Key == "Port" {
if err := newFirewallService().updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
if restoreErr := rewriteSSHManagedDirectives(sshPath, "Port", buildSSHDirectiveLines("Port", oldPortValue)); restoreErr != nil {
return fmt.Errorf("save SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr)
}
return err
}
handleSSHPortUpdate(oldPortValue, req.NewValue) handleSSHPortUpdate(oldPortValue, req.NewValue)
} }
@@ -320,18 +333,6 @@ func handleSSHPortUpdate(oldValue, newValue string) {
} }
} }
removedPorts, err := parseSSHPortsToInts(diffSSHPorts(oldPorts, newPorts))
if err != nil {
global.LOG.Errorf("parse removed ssh ports failed, err: %v", err)
} else {
addedPorts, err := parseSSHPortsToInts(diffSSHPorts(newPorts, oldPorts))
if err != nil {
global.LOG.Errorf("parse added ssh ports failed, err: %v", err)
} else if err := OperateFirewallPort(removedPorts, addedPorts); err != nil {
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", oldValue, newValue, err)
}
}
primaryPort, err := loadPrimarySSHPort(newValue) primaryPort, err := loadPrimarySSHPort(newValue)
if err != nil { if err != nil {
global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err) global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err)
@@ -371,6 +372,24 @@ func diffSSHPorts(left, right []string) []string {
return diff return diff
} }
func checkSSHPortAvailability(oldPorts, newPorts []string) error {
for _, port := range diffSSHPorts(newPorts, oldPorts) {
value, err := strconv.Atoi(port)
if err != nil || value < 1 || value > 65535 {
return fmt.Errorf("invalid SSH port %q", port)
}
if common.ScanPort(value) {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
listener, err := net.Listen("tcp4", ":"+strconv.Itoa(value))
if err != nil {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
_ = listener.Close()
}
return nil
}
func loadPrimarySSHPort(value string) (int, error) { func loadPrimarySSHPort(value string) (int, error) {
ports := splitSSHPorts(value) ports := splitSSHPorts(value)
if len(ports) == 0 { if len(ports) == 0 {
@@ -379,18 +398,6 @@ func loadPrimarySSHPort(value string) (int, error) {
return strconv.Atoi(ports[0]) return strconv.Atoi(ports[0])
} }
func parseSSHPortsToInts(ports []string) ([]int, error) {
var values []int
for _, port := range ports {
value, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
values = append(values, value)
}
return values, nil
}
func runWithOptionalSudo(sudo, name string, args ...string) (string, error) { func runWithOptionalSudo(sudo, name string, args ...string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)) cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
if sudo != "" { if sudo != "" {
+4 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"path" "path"
"strconv" "strconv"
@@ -32,7 +33,9 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
return nil, err return nil, err
} }
go func() { go func() {
_ = OperateFirewallPort(nil, addPorts) if err := ensureFirewallPorts(addPorts); err != nil {
global.LOG.Errorf("allow website firewall ports failed: %v", err)
}
}() }()
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
+33 -5
View File
@@ -2470,10 +2470,19 @@
}, },
"/core/xpack/vms/del": { "/core/xpack/vms/del": {
"bodyKeys": [ "bodyKeys": [
"name" "id"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]", "formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]" "formatEN": "delete VM [name]"
}, },
@@ -3770,12 +3779,31 @@
}, },
"/hosts/firewall/settings/whitelist": { "/hosts/firewall/settings/whitelist": {
"bodyKeys": [ "bodyKeys": [
"value" "rule"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防火墙端口白名单 [value]", "formatZH": "创建防火墙端口白名单",
"formatEN": "update firewall port whitelist [value]" "formatEN": "create firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/delete": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除防火墙端口白名单",
"formatEN": "delete firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/update": {
"bodyKeys": [
"oldRule",
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "编辑防火墙端口白名单",
"formatEN": "update firewall port whitelist"
}, },
"/hosts/monitor/clean": { "/hosts/monitor/clean": {
"bodyKeys": [], "bodyKeys": [],
+1 -1
View File
@@ -27,7 +27,7 @@ const (
FirewallPingStatusKey = "BanPing" FirewallPingStatusKey = "BanPing"
FirewallPortWhiteList = "FirewallPortWhiteList" FirewallPortWhiteList = "FirewallPortWhiteList"
FirewallPortWhiteListValue = "80/tcp,443/tcp,443/udp" FirewallPortWhiteListValue = `[{"port":"80","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"udp","sources":["0.0.0.0/0","::/0"]}]`
) )
const ( const (
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back' 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' 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' 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' 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 }}' FirewallSyncStep: 'Synchronize rules to {{ .name }}'
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize' FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}' FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains' FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
FirewallPersistDockerGuardStep: 'Persist Docker port guard status' FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule." ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
FirewallWhitelistTask: "Update firewall port whitelist"
FirewallWhitelistSaved: "Whitelist configuration saved"
FirewallWhitelistDeferred: "{{ .name }}: configuration saved; pending firewall activation"
FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list" 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" FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules"
FileTaskCopy: 'Copy files to {{ .dst }}'
FileTaskMove: 'Move files to {{ .dst }}'
FileTaskCompress: 'Compress files to {{ .dst }}'
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 }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados' FirewallCreateRuleExecutionFailed: 'Error al crear la regla. 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' 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' 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' 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 }}' FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall' FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}' FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}' FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker' FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva." ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva."
FirewallWhitelistTask: "Actualizar la lista de puertos permitidos del cortafuegos"
FirewallWhitelistSaved: "Configuración de la lista de permitidos guardada"
FirewallWhitelistDeferred: "{{ .name }}: configuración guardada; pendiente de activar el cortafuegos"
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" 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" 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 }}'
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 }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند' FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود' FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد' FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده' FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}' FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود' FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}' FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker
FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}' FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}'
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker' FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید." ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید."
FirewallWhitelistTask: "به‌روزرسانی فهرست پورت‌های مجاز فایروال"
FirewallWhitelistSaved: "پیکربندی فهرست مجاز ذخیره شد"
FirewallWhitelistDeferred: "{{ .name }}: پیکربندی ذخیره شد؛ در انتظار فعال‌سازی فایروال"
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید" FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید"
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود" FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود"
FileTaskCopy: 'کپی فایل‌ها به {{ .dst }}'
FileTaskMove: 'انتقال فایل‌ها به {{ .dst }}'
FileTaskCompress: 'فشرده‌سازی فایل‌ها در {{ .dst }}'
FileTaskDecompress: 'استخراج فایل‌ها در {{ .dst }}'
FileTaskSource: 'مسیر مبدأ: {{ .path }}'
FileTaskFormat: 'قالب بایگانی: {{ .format }}'
FileTaskRename: 'نام فایل مقصد: {{ .name }}'
ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید."
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません' FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました' FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました' FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件' FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
FirewallSyncRuleUnchanged: '一致済み、変更不要'
FirewallSyncStep: '{{ .name }} にルールを同期' FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした' FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化' FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエン
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド' FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存' FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。" ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
FirewallWhitelistTask: "ファイアウォールのポート許可リストを更新"
FirewallWhitelistSaved: "許可リストの設定を保存しました"
FirewallWhitelistDeferred: "{{ .name }}:設定を保存しました。ファイアウォールの有効化後に適用します"
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください" FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています" FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"
FileTaskCopy: 'ファイルを {{ .dst }} にコピー'
FileTaskMove: 'ファイルを {{ .dst }} に移動'
FileTaskCompress: 'ファイルを {{ .dst }} に圧縮'
FileTaskDecompress: 'ファイルを {{ .dst }} に展開'
FileTaskSource: '元のパス:{{ .path }}'
FileTaskFormat: '圧縮形式:{{ .format }}'
FileTaskRename: '保存先ファイル名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다' FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다' FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다' FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개' FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
FirewallSyncStep: '{{ .name }}에 규칙 동기화' FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다' FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화' FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩' FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장' FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요." ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
FirewallWhitelistTask: "방화벽 포트 허용 목록 업데이트"
FirewallWhitelistSaved: "허용 목록 설정이 저장되었습니다"
FirewallWhitelistDeferred: "{{ .name }}: 설정이 저장되었으며 방화벽 활성화 후 적용됩니다"
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요" FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다" FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"
FileTaskCopy: '{{ .dst }}에 파일 복사'
FileTaskMove: '{{ .dst }}로 파일 이동'
FileTaskCompress: '{{ .dst }}에 파일 압축'
FileTaskDecompress: '{{ .dst }}에 압축 해제'
FileTaskSource: '원본 경로: {{ .path }}'
FileTaskFormat: '압축 형식: {{ .format }}'
FileTaskRename: '대상 파일 이름: {{ .name }}'
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
+13 -4
View File
@@ -693,7 +693,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ' FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ' FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້' FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}' FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}' FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ' FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}' FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
@@ -707,8 +708,16 @@ FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແ
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}' FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker' FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່." ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
FirewallWhitelistTask: "ອັບເດດລາຍຊື່ພອດທີ່ອະນຸຍາດຂອງໄຟວໍ"
FirewallWhitelistSaved: "ບັນທຶກການຕັ້ງຄ່າລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ"
FirewallWhitelistDeferred: "{{ .name }}: ບັນທຶກການຕັ້ງຄ່າແລ້ວ; ລໍຖ້າເປີດໃຊ້ໄຟວໍ"
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ" FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ" FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"
FileTaskCopy: 'ສຳເນົາໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskMove: 'ຍ້າຍໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskCompress: 'ບີບອັດໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskDecompress: 'ແຕກໄຟລ໌ໄປທີ່ {{ .dst }}'
FileTaskSource: 'ເສັ້ນທາງຕົ້ນທາງ: {{ .path }}'
FileTaskFormat: 'ຮູບແບບໄຟລ໌ບີບອັດ: {{ .format }}'
FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan' 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' FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, 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' 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 }}' FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan' FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}' FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}' FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker' FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu." ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
FirewallWhitelistTask: "Kemas kini senarai port dibenarkan tembok api"
FirewallWhitelistSaved: "Konfigurasi senarai dibenarkan telah disimpan"
FirewallWhitelistDeferred: "{{ .name }}: konfigurasi disimpan; menunggu pengaktifan tembok api"
FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan" 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" FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"
FileTaskCopy: 'Salin fail ke {{ .dst }}'
FileTaskMove: 'Pindahkan fail ke {{ .dst }}'
FileTaskCompress: 'Mampatkan fail ke {{ .dst }}'
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 }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos' FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. 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' 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' 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' 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 }}' FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização' FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}' FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as p
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}' FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker' FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker'
ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra." 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."
FirewallWhitelistTask: "Atualizar a lista de portas permitidas do firewall"
FirewallWhitelistSaved: "Configuração da lista de permissões salva"
FirewallWhitelistDeferred: "{{ .name }}: configuração salva; aguardando a ativação do firewall"
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" 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" 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 }}'
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 }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены' FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления' FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось' FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}' FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}' FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}' FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}' FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Проверить бэкенд межсете
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}' FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker' FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило." ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
FirewallWhitelistTask: "Обновить список разрешённых портов межсетевого экрана"
FirewallWhitelistSaved: "Настройки списка разрешённых портов сохранены"
FirewallWhitelistDeferred: "{{ .name }}: настройки сохранены; ожидается включение межсетевого экрана"
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил" FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов" FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"
FileTaskCopy: 'Копирование файлов в {{ .dst }}'
FileTaskMove: 'Перемещение файлов в {{ .dst }}'
FileTaskCompress: 'Сжатие файлов в {{ .dst }}'
FileTaskDecompress: 'Распаковка файлов в {{ .dst }}'
FileTaskSource: 'Исходный путь: {{ .path }}'
FileTaskFormat: 'Формат архива: {{ .format }}'
FileTaskRename: 'Имя целевого файла: {{ .name }}'
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı' FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı'
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi' FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, 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' 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' FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi' FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak' FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkeleri
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla' FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet' FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet'
ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun." 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."
FirewallWhitelistTask: "Güvenlik duvarı izin verilen port listesini güncelle"
FirewallWhitelistSaved: "İzin verilenler listesi yapılandırması kaydedildi"
FirewallWhitelistDeferred: "{{ .name }}: yapılandırma kaydedildi; güvenlik duvarının etkinleştirilmesi bekleniyor"
FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin" 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" 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'
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 }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復' FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗' FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗' FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條' FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
FirewallSyncRuleUnchanged: '已一致,無需變更'
FirewallSyncStep: '同步規則到 {{ .name }}' FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗' FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}' FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈' FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態' FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。" ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
FirewallWhitelistTask: "更新防火牆連接埠白名單"
FirewallWhitelistSaved: "白名單設定已儲存"
FirewallWhitelistDeferred: "{{ .name }}:設定已儲存,待防火牆啟用後套用"
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除" FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護" FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"
FileTaskCopy: '複製檔案至 {{ .dst }}'
FileTaskMove: '移動檔案至 {{ .dst }}'
FileTaskCompress: '壓縮檔案至 {{ .dst }}'
FileTaskDecompress: '解壓檔案至 {{ .dst }}'
FileTaskSource: '來源路徑:{{ .path }}'
FileTaskFormat: '壓縮格式:{{ .format }}'
FileTaskRename: '目標檔名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
+13 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚' FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚'
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败' FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败' FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条' FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
FirewallSyncRuleUnchanged: '已一致,无需变更'
FirewallSyncStep: "同步规则到 {{ .name }}" FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败" FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}" FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链" FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态" FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。" ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
FirewallWhitelistTask: "更新防火墙端口白名单"
FirewallWhitelistSaved: "白名单配置已保存"
FirewallWhitelistDeferred: "{{ .name }}:配置已保存,待防火墙启用后下发"
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除" FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护" FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"
FileTaskCopy: '复制文件到 {{ .dst }}'
FileTaskMove: '移动文件到 {{ .dst }}'
FileTaskCompress: '压缩文件到 {{ .dst }}'
FileTaskDecompress: '解压文件到 {{ .dst }}'
FileTaskSource: '源路径:{{ .path }}'
FileTaskFormat: '压缩格式:{{ .format }}'
FileTaskRename: '目标文件名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
+20 -15
View File
@@ -11,11 +11,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations"
migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils" migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils"
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
) )
func Init() { func Init() {
@@ -26,6 +26,15 @@ func Init() {
return return
} }
clientName := client.Name() clientName := client.Name()
initialize := false
defer func() {
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 { if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil {
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err) global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
return return
@@ -40,11 +49,11 @@ func Init() {
if err := initForwardingRules(ctx); err != nil { if err := initForwardingRules(ctx); err != nil {
global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err) global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err)
} }
if !needInit() { initialize = needInit()
if !initialize {
repairIptablesBaseChains(clientName) repairIptablesBaseChains(clientName)
return return
} }
defer initDockerPortGuard(ctx)
InitPingStatus() InitPingStatus()
global.LOG.Info("initializing firewall settings...") global.LOG.Info("initializing firewall settings...")
if clientName == "nftables" { if clientName == "nftables" {
@@ -64,17 +73,12 @@ func Init() {
return return
} }
settingRepo := repo.NewISettingRepo() settingRepo := repo.NewISettingRepo()
panelPort := service.LoadPanelPort()
if len(panelPort) == 0 {
global.LOG.Errorf("find 1panel service port failed")
return
}
requiredPorts, err := service.LoadRequiredFirewallPortWhiteList() requiredPorts, err := service.LoadRequiredFirewallPortWhiteList()
if err != nil { if err != nil {
global.LOG.Errorf("load required firewall ports failed, err: %v", err) global.LOG.Errorf("load required firewall ports failed, err: %v", err)
return return
} }
if err := iptables_helper.RestoreBaseChains(panelPort, requiredPorts); err != nil { if err := iptables_helper.RestoreBaseChains(requiredPorts); err != nil {
global.LOG.Errorf("restore iptables base chains failed, err: %v", err) global.LOG.Errorf("restore iptables base chains failed, err: %v", err)
return return
} }
@@ -99,11 +103,12 @@ func repairIptablesBaseChains(clientName string) {
if status != constant.StatusEnable { if status != constant.StatusEnable {
return return
} }
manager := iptables_helper.Manager{ ports, err := service.LoadRequiredFirewallPortWhiteList()
PanelPort: service.LoadPanelPort, if err != nil {
LoadRequiredPorts: service.LoadRequiredFirewallPortWhiteList, global.LOG.Warnf("load required firewall ports for base chain repair failed, err: %v", err)
return
} }
if err := manager.RepairBaseChains(); err != nil { if err := iptables_helper.RepairBaseChains(ports); err != nil {
global.LOG.Warnf("repair iptables base chains failed, err: %v", err) global.LOG.Warnf("repair iptables base chains failed, err: %v", err)
} }
} }
@@ -150,7 +155,7 @@ func needInit() bool {
func InitPingStatus() { func InitPingStatus() {
global.LOG.Info("initializing ban ping status from settings...") global.LOG.Info("initializing ban ping status from settings...")
status := ping.LoadStatus() status := firewall.LoadPingStatus()
statusInDB, _ := repo.NewISettingRepo().GetValueByKey("BanPing") statusInDB, _ := repo.NewISettingRepo().GetValueByKey("BanPing")
if statusInDB == status { if statusInDB == status {
return return
@@ -160,7 +165,7 @@ func InitPingStatus() {
if statusInDB == constant.StatusDisable { if statusInDB == constant.StatusDisable {
enable = "0" enable = "0"
} }
if err := ping.UpdateStatus(enable); err != nil { if err := firewall.UpdatePingStatus(enable); err != nil {
global.LOG.Errorf("initialize ping status failed: %v", err) global.LOG.Errorf("initialize ping status failed: %v", err)
} }
} }
+1
View File
@@ -110,6 +110,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.NormalizeFirewallBackendSelections, migrations.NormalizeFirewallBackendSelections,
migrations.SimplifyFirewallRulePolicy, migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly, migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
} }
} }
@@ -0,0 +1,218 @@
package migrations
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"unicode"
"github.com/1Panel-dev/1Panel/agent/app/model"
"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/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const firewalldSSHServiceMigrationID = "20260916-remove-firewalld-ssh-service"
func TransferFirewalldSSHService(ctx context.Context, client lifecycle.Client, syncWhitelist func(context.Context) error) error {
return transferFirewalldSSHService(ctx, global.DB, client, syncWhitelist)
}
func transferFirewalldSSHService(ctx context.Context, db *gorm.DB, client lifecycle.Client, syncWhitelist func(context.Context) error) error {
if err := syncWhitelist(ctx); err != nil {
return err
}
if client.Name() != lifecycle.ProviderFirewalld {
return nil
}
var count int64
if err := db.WithContext(ctx).Table("migrations").Where("id = ?", firewalldSSHServiceMigrationID).Count(&count).Error; err != nil {
return fmt.Errorf("check firewalld SSH service migration: %w", err)
}
if count > 0 {
return nil
}
active, err := client.Status()
if err != nil || !active {
return err
}
if err := ctx.Err(); err != nil {
return err
}
if err := providers.RemoveFirewalldSSHService(); err != nil {
return fmt.Errorf("transfer firewalld SSH access to whitelist: %w", err)
}
if err := db.WithContext(ctx).Table("migrations").Clauses(clause.OnConflict{DoNothing: true}).
Create(map[string]interface{}{"id": firewalldSSHServiceMigrationID}).Error; err != nil {
return fmt.Errorf("record firewalld SSH service migration: %w", err)
}
return nil
}
var MigrateFirewallPortWhitelistSources = &gormigrate.Migration{
ID: "20260915-migrate-firewall-port-whitelist-sources",
Migrate: func(tx *gorm.DB) error {
var setting model.Setting
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
rules, err := migrateFirewallPortWhitelist(setting.Value)
if err != nil {
return fmt.Errorf("migrate firewall port whitelist: %w", err)
}
value, err := json.Marshal(rules)
if err != nil {
return err
}
if setting.ID == 0 {
err = tx.Create(&model.Setting{Key: constant.FirewallPortWhiteList, Value: string(value)}).Error
} else {
err = tx.Model(&setting).Update("value", string(value)).Error
}
if err != nil {
return err
}
return tx.Where("key = ?", "FirewallPortWhiteListPending").Delete(&model.Setting{}).Error
},
}
type legacyPortWhitelist struct {
Ports []string `json:"ports"`
Family string `json:"family"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Sources []string `json:"sources"`
}
func (entry legacyPortWhitelist) singlePortRule() firewall.PortWhitelist {
rule := firewall.PortWhitelist{Port: entry.Port, Protocol: entry.Protocol, Type: entry.Type, Sources: entry.Sources}
if strings.TrimSpace(rule.Type) != "" && rule.Port == "" && len(entry.Ports) > 0 {
rule.Port = entry.Ports[0]
}
return rule
}
func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error) {
legacy, err := parseLegacyPortWhitelist(value)
if err != nil {
return nil, err
}
rules := make([]firewall.PortWhitelist, 0, len(legacy)+5)
indexes := make(map[string]int)
key := func(rule firewall.PortWhitelist) string {
if rule.Type != "" {
return rule.Type + "/" + rule.Protocol
}
return rule.Type + "/" + rule.Protocol + "/" + rule.Port
}
for index, entry := range legacy {
family := strings.ToLower(strings.TrimSpace(entry.Family))
if family != "" && family != constant.FirewallFamilyIPv4 && family != constant.FirewallFamilyIPv6 {
return nil, fmt.Errorf("entry #%d: invalid address family %q", index+1, entry.Family)
}
rule := entry.singlePortRule()
if strings.TrimSpace(rule.Protocol) == "" {
rule.Protocol = "tcp"
}
if len(rule.Sources) == 0 {
rule.Sources = []string{"0.0.0.0/0"}
if family == constant.FirewallFamilyIPv6 {
rule.Sources = []string{"::/0"}
} else if family == "" && strings.TrimSpace(rule.Type) != "" {
rule.Sources = append(rule.Sources, "::/0")
}
}
rule.Sources, err = firewall.NormalizeWhitelistSources(family, rule.Sources)
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
normalized, err := service.InitializeFirewallWhitelistPorts([]firewall.PortWhitelist{rule})
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
rule = normalized[0]
if existing, found := indexes[key(rule)]; found {
rules[existing].Sources, err = firewall.NormalizeWhitelistSources("", append(rules[existing].Sources, rule.Sources...))
if err != nil {
return nil, err
}
continue
}
indexes[key(rule)] = len(rules)
rules = append(rules, rule)
}
defaults := []firewall.PortWhitelist{
{Type: firewall.PortWhitelistTypePanel, Protocol: "tcp"},
{Type: firewall.PortWhitelistTypeSSH, Protocol: "tcp"},
{Port: "443", Protocol: "tcp"},
{Port: "443", Protocol: "udp"},
{Port: "80", Protocol: "tcp"},
}
for _, rule := range defaults {
index, found := indexes[key(rule)]
if !found {
if rule.Type == "" {
continue
}
index = len(rules)
indexes[key(rule)] = index
rules = append(rules, rule)
}
var ipv4, ipv6 bool
for _, source := range rules[index].Sources {
if strings.Contains(source, ":") {
ipv6 = true
} else {
ipv4 = true
}
}
if !ipv4 {
rules[index].Sources = append(rules[index].Sources, "0.0.0.0/0")
}
if !ipv6 {
rules[index].Sources = append(rules[index].Sources, "::/0")
}
}
return service.InitializeFirewallWhitelistPorts(rules)
}
func parseLegacyPortWhitelist(value string) ([]legacyPortWhitelist, error) {
value = strings.TrimSpace(value)
if value == "" || value == "null" {
return nil, nil
}
if strings.HasPrefix(value, "[") {
var rules []legacyPortWhitelist
err := json.Unmarshal([]byte(value), &rules)
return rules, err
}
items := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' || unicode.IsSpace(r) })
rules := make([]legacyPortWhitelist, 0, len(items))
for _, item := range items {
parts := strings.Split(item, "/")
rule := legacyPortWhitelist{}
switch len(parts) {
case 1:
rule.Port = parts[0]
case 2:
rule.Port, rule.Protocol = parts[0], parts[1]
case 3:
rule.Family, rule.Port, rule.Protocol = parts[0], parts[1], parts[2]
default:
return nil, fmt.Errorf("invalid legacy whitelist entry %q", item)
}
rules = append(rules, rule)
}
return rules, nil
}
+2 -2
View File
@@ -23,7 +23,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/ssh" "github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/utils/xpack"
@@ -1131,7 +1131,7 @@ var AddisIPtoWebsiteSSL = &gormigrate.Migration{
var InitPingStatus = &gormigrate.Migration{ var InitPingStatus = &gormigrate.Migration{
ID: "20251201-init-ping-status", ID: "20251201-init-ping-status",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
status := ping.LoadStatus() status := firewall.LoadPingStatus()
if err := tx.Create(&model.Setting{Key: "BanPing", Value: status}).Error; err != nil { if err := tx.Create(&model.Setting{Key: "BanPing", Value: status}).Error; err != nil {
return err return err
} }
@@ -2,6 +2,9 @@ package utils
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
@@ -142,7 +145,7 @@ func convertLegacyHostFirewallRecords(records []legacyHostFirewallRecord, provid
} }
continue continue
} }
identity := item.PolicyKey() identity := hostFirewallPolicyKey(item)
if index, exists := byIdentity[identity]; exists { if index, exists := byIdentity[identity]; exists {
if item.Description != "" { if item.Description != "" {
converted[index].Description = item.Description converted[index].Description = item.Description
@@ -336,10 +339,30 @@ func legacyIPOrPrefix(value string) bool {
} }
func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) { func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) {
record, err := model.FirewallRuleFromDomain(rule) normalized, err := filter.NormalizeRule(rule)
if err != nil { if err != nil {
return model.FirewallRule{}, err 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.UUID = uuid.NewString()
record.Origin = constant.FirewallRuleOriginAdopted record.Origin = constant.FirewallRuleOriginAdopted
record.Owner = constant.FirewallRuleSourceUser record.Owner = constant.FirewallRuleSourceUser
@@ -347,6 +370,27 @@ func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error)
return record, nil 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 { func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) error {
var existing []model.FirewallRule var existing []model.FirewallRule
if err := tx.Find(&existing).Error; err != nil { if err := tx.Find(&existing).Error; err != nil {
@@ -354,10 +398,10 @@ func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) erro
} }
byIdentity := make(map[string]model.FirewallRule, len(existing)) byIdentity := make(map[string]model.FirewallRule, len(existing))
for _, item := range existing { for _, item := range existing {
byIdentity[item.PolicyKey()] = item byIdentity[hostFirewallPolicyKey(item)] = item
} }
for _, item := range rules { for _, item := range rules {
identity := item.PolicyKey() identity := hostFirewallPolicyKey(item)
if current, exists := byIdentity[identity]; exists { if current, exists := byIdentity[identity]; exists {
if current.Description == "" && item.Description != "" { if current.Description == "" && item.Description != "" {
if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID). if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID).
+7
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"path" "path"
"strconv"
"github.com/1Panel-dev/1Panel/agent/cmd/server/conf" "github.com/1Panel-dev/1Panel/agent/cmd/server/conf"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -61,4 +62,10 @@ func initBaseInfo() {
panic(err) panic(err)
} }
global.CONF.Base.InstallDir = nodeInfo.BaseDir global.CONF.Base.InstallDir = nodeInfo.BaseDir
if !global.IsMaster {
global.CONF.Base.Port = strconv.FormatUint(uint64(nodeInfo.NodePort), 10)
if nodeInfo.NodePort == 0 {
global.CONF.Base.Port = "9999"
}
}
} }
+4 -1
View File
@@ -11,6 +11,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter := Router.Group("hosts") hostRouter := Router.Group("hosts")
baseApi := v2.ApiGroupApp.BaseApi baseApi := v2.ApiGroupApp.BaseApi
Router.POST("/internal/terminal/sessions/revoke", baseApi.RevokeTerminalSessions) Router.POST("/internal/terminal/sessions/revoke", baseApi.RevokeTerminalSessions)
Router.GET("/internal/terminal/capabilities", baseApi.TerminalCapabilities)
{ {
hostRouter.POST("", baseApi.CreateHost) hostRouter.POST("", baseApi.CreateHost)
hostRouter.POST("/info", baseApi.GetHostByID) hostRouter.POST("/info", baseApi.GetHostByID)
@@ -27,7 +28,9 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort) hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings) hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend) hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend)
hostRouter.POST("/firewall/settings/whitelist", baseApi.UpdateFirewallPortWhitelist) hostRouter.POST("/firewall/settings/whitelist", baseApi.CreateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist)
hostRouter.POST("/firewall/forward/base", baseApi.LoadForwardingBaseInfo) hostRouter.POST("/firewall/forward/base", baseApi.LoadForwardingBaseInfo)
hostRouter.POST("/firewall/forward/search", baseApi.SearchForwardingRules) hostRouter.POST("/firewall/forward/search", baseApi.SearchForwardingRules)
hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules) hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules)
+9
View File
@@ -27,6 +27,7 @@ type CommandHelper struct {
outputFile string outputFile string
scriptPath string scriptPath string
stdin io.Reader stdin io.Reader
stderr io.Writer
env []string env []string
timeout time.Duration timeout time.Duration
taskItem *task.Task taskItem *task.Task
@@ -360,6 +361,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
cmd.Stdout = &stdout cmd.Stdout = &stdout
cmd.Stderr = &stderr cmd.Stderr = &stderr
} }
if c.stderr != nil {
cmd.Stderr = io.MultiWriter(cmd.Stderr, c.stderr)
}
env := os.Environ() env := os.Environ()
env = append(env, c.env...) env = append(env, c.env...)
cmd.Env = env cmd.Env = env
@@ -481,6 +485,11 @@ func WithStdin(stdin io.Reader) Option {
s.stdin = stdin s.stdin = stdin
} }
} }
func WithStderr(stderr io.Writer) Option {
return func(s *CommandHelper) {
s.stderr = stderr
}
}
func WithEnv(env ...string) Option { func WithEnv(env ...string) Option {
return func(s *CommandHelper) { return func(s *CommandHelper) {
s.env = append(s.env, env...) s.env = append(s.env, env...)
+16 -1
View File
@@ -2,6 +2,7 @@ package manager
import ( import (
"errors" "errors"
"strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -18,5 +19,19 @@ func handlerErr(out string, err error) error {
} }
func run(name string, args ...string) (string, error) { func run(name string, args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout(name, args...) return runWithTimeout(10*time.Second, name, args...)
}
func runWithTimeout(timeout time.Duration, name string, args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(timeout), cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout(name, args...)
}
func serviceOperationTimeout(operation, serviceName string) time.Duration {
if operation == "restart" {
switch strings.TrimSuffix(serviceName, ".service") {
case "docker", "dockerd", "docker.dockerd", "snap.docker.dockerd":
return 2 * time.Minute
}
}
return 10 * time.Second
} }
+1 -1
View File
@@ -46,7 +46,7 @@ func (s *Openrc) Operate(operate, serviceName string) error {
case "disable": case "disable":
return handlerErr(run("rc-update", "del", serviceName, "default")) return handlerErr(run("rc-update", "del", serviceName, "default"))
default: default:
return handlerErr(run(s.toolCmd, serviceName, operate)) return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, serviceName, operate))
} }
} }
+3 -2
View File
@@ -1,6 +1,7 @@
package manager package manager
import ( import (
"fmt"
"strings" "strings"
) )
@@ -48,7 +49,7 @@ func (s *Snap) IsEnable(serviceName string) bool {
func (s *Snap) Operate(operate, serviceName string) error { func (s *Snap) Operate(operate, serviceName string) error {
if s.IsExist(serviceName) { if s.IsExist(serviceName) {
return handlerErr(run(s.toolCmd, operate, serviceName)) return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, operate, serviceName))
} }
return nil return fmt.Errorf("snap service %q does not exist", serviceName)
} }
+2 -2
View File
@@ -57,7 +57,7 @@ func (s *Systemd) Status(serviceName string) (string, error) {
return run(s.toolCmd, "status", serviceName) return run(s.toolCmd, "status", serviceName)
} }
func (s *Systemd) Operate(operate, serviceName string) error { func (s *Systemd) Operate(operate, serviceName string) error {
out, err := run(s.toolCmd, operate, serviceName) out, err := runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, operate, serviceName)
if err != nil { if err != nil {
if fallbackName := systemdAliasFallbackName(serviceName); fallbackName != "" && strings.Contains(out, "alias name or linked unit file") { if fallbackName := systemdAliasFallbackName(serviceName); fallbackName != "" && strings.Contains(out, "alias name or linked unit file") {
return s.Operate(operate, fallbackName) return s.Operate(operate, fallbackName)
@@ -65,7 +65,7 @@ func (s *Systemd) Operate(operate, serviceName string) error {
if err := NewSnap().Operate(operate, serviceName); err == nil { if err := NewSnap().Operate(operate, serviceName); err == nil {
return nil return nil
} }
return handlerErr(run(s.toolCmd, operate, serviceName)) return handlerErr(out, err)
} }
return nil return nil
} }
+1 -1
View File
@@ -39,7 +39,7 @@ func (s *Sysvinit) Status(serviceName string) (string, error) {
} }
func (s *Sysvinit) Operate(operate, serviceName string) error { func (s *Sysvinit) Operate(operate, serviceName string) error {
return handlerErr(run(s.toolCmd, serviceName, operate)) return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, serviceName, operate))
} }
func (s *Sysvinit) Reload() error { func (s *Sysvinit) Reload() error {
+71 -20
View File
@@ -16,6 +16,7 @@ import (
"io" "io"
"io/fs" "io/fs"
"math" "math"
"mime"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@@ -28,6 +29,8 @@ import (
"sync" "sync"
"syscall" "syscall"
"time" "time"
"unicode"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
@@ -386,9 +389,23 @@ type DownloadProxyConfig struct {
type DownloadOptions struct { type DownloadOptions struct {
IgnoreCertificate bool IgnoreCertificate bool
UseServerFilename bool
Proxy *DownloadProxyConfig Proxy *DownloadProxyConfig
} }
func downloadResponseFilename(header string) string {
_, params, err := mime.ParseMediaType(header)
if err != nil {
return ""
}
name := strings.TrimSpace(params["filename"])
if name == "" || name == "." || name == ".." || len(name) > 255 || !utf8.ValidString(name) ||
strings.ContainsAny(name, "/\\:") || strings.IndexFunc(name, unicode.IsControl) >= 0 {
return ""
}
return name
}
func buildDownloadProxyURL(proxy DownloadProxyConfig) (*url.URL, error) { func buildDownloadProxyURL(proxy DownloadProxyConfig) (*url.URL, error) {
proxyType := strings.TrimSpace(proxy.Type) proxyType := strings.TrimSpace(proxy.Type)
proxyHost := strings.TrimSpace(proxy.URL) proxyHost := strings.TrimSpace(proxy.URL)
@@ -455,7 +472,7 @@ type downloadPolicy struct {
idleTimeout time.Duration idleTimeout time.Duration
} }
var remoteDownloadPolicy = downloadPolicy{retries: 3, retryDelay: 2 * time.Second, idleTimeout: 90 * time.Second} var remoteDownloadPolicy = downloadPolicy{retries: 3, retryDelay: 5 * time.Second, idleTimeout: 90 * time.Second}
func saveDownloadProcess(process Process) { func saveDownloadProcess(process Process) {
if process.Total > 0 { if process.Total > 0 {
@@ -492,20 +509,27 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
client.CloseIdleConnections() client.CloseIdleConnections()
return err return err
} }
original, err := os.Lstat(dst) parent = filepath.Dir(dst)
if err != nil && !os.IsNotExist(err) { var original os.FileInfo
client.CloseIdleConnections() if !options.UseServerFilename {
return err original, err = os.Lstat(dst)
} if err != nil && !os.IsNotExist(err) {
if original != nil && !original.Mode().IsRegular() { client.CloseIdleConnections()
client.CloseIdleConnections() return err
return fmt.Errorf("download target must be a regular file") }
if original != nil && !original.Mode().IsRegular() {
client.CloseIdleConnections()
return fmt.Errorf("download target must be a regular file")
}
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
task := &downloadTask{cancel: cancel, done: make(chan struct{}), dst: dst} task := &downloadTask{cancel: cancel, done: make(chan struct{}), dst: dst}
if options.UseServerFilename {
task.dst = ""
}
downloadMu.Lock() downloadMu.Lock()
for _, active := range downloadTasks { for _, active := range downloadTasks {
if active.dst == dst { if task.dst != "" && active.dst == task.dst {
downloadMu.Unlock() downloadMu.Unlock()
cancel() cancel()
client.CloseIdleConnections() client.CloseIdleConnections()
@@ -532,6 +556,34 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
close(task.done) close(task.done)
}() }()
process := Process{Key: key, Name: filepath.Base(dst), Status: "Downloading"} process := Process{Key: key, Name: filepath.Base(dst), Status: "Downloading"}
nameResolved := !options.UseServerFilename
resolveName := func(resp *http.Response) (string, error) {
if nameResolved {
return dst, nil
}
name := downloadResponseFilename(resp.Header.Get("Content-Disposition"))
if name == "" {
name = filepath.Base(dst)
}
resolved := filepath.Join(parent, name)
process.Name = name
downloadMu.Lock()
defer downloadMu.Unlock()
for otherKey, active := range downloadTasks {
if otherKey != key && active.dst == resolved {
return "", buserr.New("TaskIsExecuting")
}
}
if _, statErr := os.Lstat(resolved); statErr == nil {
return "", fmt.Errorf("download target already exists: %s", name)
} else if !os.IsNotExist(statErr) {
return "", statErr
}
task.dst = resolved
dst = resolved
nameResolved = true
return dst, nil
}
update := func(state downloadState, status string, attempt int) { update := func(state downloadState, status string, attempt int) {
process.Written = uint64(state.written) process.Written = uint64(state.written)
process.Total = uint64(max(0, state.total)) process.Total = uint64(max(0, state.total))
@@ -553,7 +605,7 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
record, runErr = recordDownloadPart(out.Name(), partInfo) record, runErr = recordDownloadPart(out.Name(), partInfo)
} }
if runErr == nil { if runErr == nil {
runErr = runRemoteDownload(ctx, client, rawURL, dst, out, remoteDownloadPolicy, update) runErr = runRemoteDownload(ctx, client, rawURL, dst, out, remoteDownloadPolicy, update, resolveName)
} }
task.mu.Lock() task.mu.Lock()
if ctx.Err() != nil { if ctx.Err() != nil {
@@ -685,7 +737,7 @@ func retryDownloadError(err error) bool {
} }
func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File, func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File,
policy downloadPolicy, update func(downloadState, string, int)) error { policy downloadPolicy, update func(downloadState, string, int), resolveName ...func(*http.Response) (string, error)) error {
state := downloadState{total: -1} state := downloadState{total: -1}
for attempt := 0; ; attempt++ { for attempt := 0; ; attempt++ {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
@@ -693,7 +745,7 @@ func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst str
} }
update(state, "Downloading", attempt) update(state, "Downloading", attempt)
retry, retryAfter, err := downloadAttempt(ctx, client, rawURL, dst, out, &state, policy.idleTimeout, retry, retryAfter, err := downloadAttempt(ctx, client, rawURL, dst, out, &state, policy.idleTimeout,
func() { update(state, "Downloading", attempt) }) func() { update(state, "Downloading", attempt) }, resolveName...)
if err == nil { if err == nil {
return nil return nil
} }
@@ -719,7 +771,7 @@ func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst str
} }
func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File, func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File,
state *downloadState, idleTimeout time.Duration, progress func()) (bool, time.Duration, error) { state *downloadState, idleTimeout time.Duration, progress func(), resolveName ...func(*http.Response) (string, error)) (bool, time.Duration, error) {
attemptCtx, cancel := context.WithCancel(ctx) attemptCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
request, err := http.NewRequestWithContext(attemptCtx, http.MethodGet, rawURL, nil) request, err := http.NewRequestWithContext(attemptCtx, http.MethodGet, rawURL, nil)
@@ -755,12 +807,6 @@ func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst strin
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return false, 0, fmt.Errorf("remote download returned HTTP %d", resp.StatusCode) return false, 0, fmt.Errorf("remote download returned HTTP %d", resp.StatusCode)
} }
ct := strings.ToLower(resp.Header.Get("Content-Type"))
ext := strings.ToLower(filepath.Ext(dst))
if (strings.Contains(ct, "text/html") || strings.Contains(ct, "text/xml")) &&
ext != ".html" && ext != ".htm" && ext != ".xml" && ext != ".svg" {
return false, 0, fmt.Errorf("unexpected download Content-Type: %s", ct)
}
if encoding := resp.Header.Get("Content-Encoding"); encoding != "" && !strings.EqualFold(encoding, "identity") { if encoding := resp.Header.Get("Content-Encoding"); encoding != "" && !strings.EqualFold(encoding, "identity") {
return false, 0, fmt.Errorf("unexpected download Content-Encoding: %s", encoding) return false, 0, fmt.Errorf("unexpected download Content-Encoding: %s", encoding)
} }
@@ -793,6 +839,11 @@ func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst strin
state.etag = etag state.etag = etag
} }
} }
if len(resolveName) > 0 {
if _, err := resolveName[0](resp); err != nil {
return false, 0, err
}
}
progress() progress()
timer := time.AfterFunc(idleTimeout, cancel) timer := time.AfterFunc(idleTimeout, cancel)
defer timer.Stop() defer timer.Stop()
@@ -1,42 +0,0 @@
package docker_guard
import (
"errors"
"fmt"
"strings"
)
var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP")
func (m *NftablesManager) checkForwardPolicy() error {
for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6},
} {
if !m.runner.Exists(family.command) {
continue
}
output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD")
if err != nil {
return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)}
}
found := false
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" {
continue
}
found = true
if fields[2] == "DROP" {
return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop}
}
if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
}
}
if !found {
return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")}
}
}
return nil
}
@@ -0,0 +1,247 @@
package docker_guard
import (
"net/netip"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"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"))
if backend == constant.FirewallProviderNftables {
tableFamily := "ip"
if family == constant.FirewallFamilyIPv6 {
tableFamily = "ip6"
}
tables, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "tables")
if err != nil {
return DNATRules{}
}
if !strings.Contains(tables, "table "+tableFamily+" docker-bridges") {
return DNATRules{Inspected: true}
}
output, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "table", tableFamily, "docker-bridges")
return DNATRules{Output: output, Inspected: err == nil}
}
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return DNATRules{}
}
executable := commands.IPv4
if family == constant.FirewallFamilyIPv6 {
executable = commands.IPv6
}
if executable == "" {
return DNATRules{}
}
output, err := manager.RunWithOptionalSudoAndStdout(executable, "-w", "-t", "nat", "-S")
return DNATRules{Output: output, Inspected: err == nil}
}
func ReadProxyEndpoints() ProxyEndpoints {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
if err != nil {
return ProxyEndpoints{}
}
return ProxyEndpoints{Items: parseDockerProxyEndpoints(output), Inspected: true}
}
func parseDockerProxyEndpoints(output string) []ProxyEndpoint {
result := make([]ProxyEndpoint, 0)
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
isProxy := slices.ContainsFunc(fields, func(field string) bool { return filepath.Base(field) == "docker-proxy" })
if !isProxy {
continue
}
protocol := commandFlagValue(fields, "-proto")
hostIP := commandFlagValue(fields, "-host-ip")
hostPortValue := commandFlagValue(fields, "-host-port")
hostPort, err := strconv.ParseUint(hostPortValue, 10, 16)
if err != nil || (protocol != "tcp" && protocol != "udp") || hostIP == "" {
continue
}
hostIP = strings.TrimSpace(hostIP)
if address, err := netip.ParseAddr(hostIP); err == nil {
hostIP = address.String()
}
result = append(result, ProxyEndpoint{Protocol: protocol, HostIP: hostIP, HostPort: uint16(hostPort)})
}
return result
}
func commandFlagValue(fields []string, name string) string {
for i := 0; i < len(fields); i++ {
if fields[i] == name && i+1 < len(fields) {
return fields[i+1]
}
if strings.HasPrefix(fields[i], name+"=") {
return strings.TrimPrefix(fields[i], name+"=")
}
}
return ""
}
func ProxyEndpointMatches(proxies []ProxyEndpoint, family, hostIP string, hostPort uint16, protocol string) bool {
for _, proxy := range proxies {
if proxy.Protocol == protocol && proxy.HostPort == hostPort && hostAddressMatches(proxy.HostIP, hostIP, family) {
return true
}
}
return false
}
func DNATRuleMatches(backend, output string, family, hostIP string, hostPort uint16, protocol string) bool {
return InspectEndpoints(backend, family, DNATRules{Output: output}, ProxyEndpoints{}).DNATMatches(hostIP, hostPort, protocol)
}
func DNATIngressReachable(backend, output string) bool {
if backend == constant.FirewallProviderNftables {
return strings.Contains(output, "hook prerouting")
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[0] == "-A" && fields[1] == "PREROUTING" && commandFlagValue(fields, "-j") == "DOCKER" {
return true
}
}
return false
}
type EndpointInspection struct {
DNATInspected, ProxyInspected, IngressReachable bool
family string
dnat, proxies map[ProxyEndpoint]bool
}
func InspectEndpoints(backend, family string, rules DNATRules, proxies ProxyEndpoints) EndpointInspection {
inspection := EndpointInspection{
DNATInspected: rules.Inspected, ProxyInspected: proxies.Inspected,
IngressReachable: DNATIngressReachable(backend, rules.Output), family: family,
dnat: make(map[ProxyEndpoint]bool), proxies: make(map[ProxyEndpoint]bool),
}
for _, proxy := range proxies.Items {
if isWildcardHostAddress(proxy.HostIP, family) {
wildcard := proxy
wildcard.HostIP = ""
inspection.proxies[wildcard] = true
}
proxy.HostIP = normalizedEndpointAddress(proxy.HostIP)
inspection.proxies[proxy] = true
}
replacer := strings.NewReplacer("{", " ", "}", " ", ",", " ", ";", " ")
addressToken := "ip"
if family == constant.FirewallFamilyIPv6 {
addressToken = "ip6"
}
for line := range strings.SplitSeq(rules.Output, "\n") {
if backend != constant.FirewallProviderNftables {
fields := strings.Fields(line)
if commandFlagValue(fields, "-j") != "DNAT" {
continue
}
port, err := strconv.ParseUint(commandFlagValue(fields, "--dport"), 10, 16)
if err != nil || strconv.FormatUint(port, 10) != commandFlagValue(fields, "--dport") {
continue
}
address, _, _ := strings.Cut(commandFlagValue(fields, "-d"), "/")
inspection.dnat[ProxyEndpoint{Protocol: commandFlagValue(fields, "-p"), HostIP: normalizedEndpointAddress(address), HostPort: uint16(port)}] = true
continue
}
fields := strings.Fields(replacer.Replace(line))
if !slices.Contains(fields, "dnat") {
continue
}
destination := ""
protocols := make([]string, 0, 1)
for i := 0; i+2 < len(fields); i++ {
if fields[i] == "meta" && fields[i+1] == "l4proto" {
protocols = append(protocols, fields[i+2])
}
if destination == "" && fields[i] == addressToken && fields[i+1] == "daddr" {
destination = fields[i+2]
}
}
destination, _, _ = strings.Cut(destination, "/")
destination = normalizedEndpointAddress(destination)
for i := 0; i+2 < len(fields); i++ {
if fields[i+1] != "dport" {
continue
}
port, err := strconv.ParseUint(fields[i+2], 10, 16)
if err != nil || strconv.FormatUint(port, 10) != fields[i+2] {
continue
}
matches := []string{fields[i]}
if fields[i] == "th" {
matches = protocols
}
for _, protocol := range matches {
inspection.dnat[ProxyEndpoint{Protocol: protocol, HostIP: destination, HostPort: uint16(port)}] = true
}
}
}
return inspection
}
func normalizedEndpointAddress(address string) string {
address = strings.TrimSpace(address)
if parsed, err := netip.ParseAddr(address); err == nil {
return parsed.String()
}
return address
}
func (inspection EndpointInspection) DNATMatches(hostIP string, hostPort uint16, protocol string) bool {
key := ProxyEndpoint{Protocol: protocol, HostPort: hostPort}
if inspection.dnat[key] {
return true
}
if isWildcardHostAddress(hostIP, inspection.family) {
return false
}
key.HostIP = normalizedEndpointAddress(hostIP)
return inspection.dnat[key]
}
func (inspection EndpointInspection) ProxyMatches(hostIP string, hostPort uint16, protocol string) bool {
key := ProxyEndpoint{Protocol: protocol, HostPort: hostPort, HostIP: normalizedEndpointAddress(hostIP)}
if inspection.proxies[key] {
return true
}
if !isWildcardHostAddress(hostIP, inspection.family) {
return false
}
key.HostIP = ""
return inspection.proxies[key]
}
func hostAddressMatches(left, right, family string) bool {
if isWildcardHostAddress(left, family) && isWildcardHostAddress(right, family) {
return true
}
left, right = strings.TrimSpace(left), strings.TrimSpace(right)
if address, err := netip.ParseAddr(left); err == nil {
left = address.String()
}
if address, err := netip.ParseAddr(right); err == nil {
right = address.String()
}
return left == right
}
func isWildcardHostAddress(value, family string) bool {
value = strings.TrimSpace(value)
if family == constant.FirewallFamilyIPv6 {
return value == "" || value == "::"
}
return value == "" || value == "0.0.0.0"
}
+24 -88
View File
@@ -3,73 +3,19 @@ package docker_guard
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/buserr"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall" firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
) )
const (
Chain = "1PANEL_DOCKER"
DockerChain = "DOCKER-USER"
FamilyIPv4 = constant.FirewallFamilyIPv4
FamilyIPv6 = constant.FirewallFamilyIPv6
ModeSources = "deny_sources"
ModeAllow = "allow_sources"
ModeAll = "deny_all"
StatusEffective = "effective"
StatusDisabled = "disabled"
StatusNotEffective = "not_effective"
ReasonCommandMissing = "command_missing"
ReasonDockerChainMissing = "docker_chain_missing"
ReasonGuardChainMissing = "guard_chain_missing"
ReasonJumpMissing = "jump_missing"
ReasonJumpNotFirst = "jump_not_first"
ReasonJumpDuplicate = "jump_duplicate"
ReasonInspectFailed = "inspect_failed"
)
var (
ErrDockerChainUnavailable = errors.New("Docker DOCKER-USER chain is unavailable")
ErrDockerIptablesChainUnavailable = fmt.Errorf("%w for iptables", ErrDockerChainUnavailable)
ErrDockerNftablesChainUnavailable = fmt.Errorf("%w for nftables", ErrDockerChainUnavailable)
)
type FamilyError struct {
Family string
Err error
}
func (e *FamilyError) Error() string { return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) }
func (e *FamilyError) Unwrap() error { return e.Err }
type Policy struct {
UUID string
Family string
HostIP string
HostPort uint16
Protocol string
Mode string
Sources []string
}
type FamilyStatus struct {
State string
Reason string
Initialized bool
Bound bool
Effective bool
}
type Runner interface { type Runner interface {
Run(executable string, args ...string) (string, error) Run(executable string, args ...string) (string, error)
RunInput(executable, input string, args ...string) (string, error) RunInput(executable, input string, args ...string) (string, error)
@@ -122,19 +68,18 @@ func dockerGuardExecutable(logical string) string {
} }
} }
type Manager struct { type Iptables struct {
runner Runner runner Runner
} }
var mutationMu sync.Mutex var mutationMu sync.Mutex
func NewManager() *Manager { return &Manager{runner: commandRunner{}} } func NewIptables() *Iptables { return &Iptables{runner: commandRunner{}} }
func (m *Manager) Initialize(policies []Policy) error { func (m *Iptables) Initialize(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
inventory, err := m.ListPolicies() if err := CheckIPv4Forwarding(); err != nil {
if err != nil {
return err return err
} }
if !m.runner.Exists("iptables-restore") { if !m.runner.Exists("iptables-restore") {
@@ -160,7 +105,7 @@ func (m *Manager) Initialize(policies []Policy) error {
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *Manager) Bind() error { func (m *Iptables) Bind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.bindExistingFamily("iptables", true); err != nil { if err := m.bindExistingFamily("iptables", true); err != nil {
@@ -174,17 +119,13 @@ func (m *Manager) Bind() error {
return nil return nil
} }
func (m *Manager) Reconcile(policies []Policy) error { func (m *Iptables) ReplacePolicies(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
inventory, err := m.ListPolicies()
if err != nil {
return err
}
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *Manager) ListPolicies() (PolicyInventory, error) { func (m *Iptables) ListPolicies() (PolicyInventory, error) {
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)} inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family) executable := executableForFamily(family)
@@ -215,7 +156,7 @@ func (m *Manager) ListPolicies() (PolicyInventory, error) {
return inventory, nil return inventory, nil
} }
func (m *Manager) Unbind() error { func (m *Iptables) Unbind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.unbindFamily("iptables"); err != nil { if err := m.unbindFamily("iptables"); err != nil {
@@ -229,7 +170,7 @@ func (m *Manager) Unbind() error {
return nil return nil
} }
func (m *Manager) Cleanup() error { func (m *Iptables) Cleanup() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
for _, executable := range []string{"iptables", "ip6tables"} { for _, executable := range []string{"iptables", "ip6tables"} {
@@ -251,7 +192,7 @@ func (m *Manager) Cleanup() error {
return nil return nil
} }
func (m *Manager) Initialized(family string) (bool, error) { func (m *Iptables) Initialized(family string) (bool, error) {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
return false, nil return false, nil
@@ -259,7 +200,7 @@ func (m *Manager) Initialized(family string) (bool, error) {
return m.chainExists(executable, Chain) return m.chainExists(executable, Chain)
} }
func (m *Manager) Status(family string) FamilyStatus { func (m *Iptables) Status(family string) FamilyStatus {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing}
@@ -275,12 +216,7 @@ func (m *Manager) Status(family string) FamilyStatus {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
} }
status := FamilyStatus{State: StatusNotEffective, Initialized: true} status := FamilyStatus{State: StatusNotEffective, Initialized: true}
rules, err := m.run(executable, "-S", DockerChain) jumps := countJumps(chains)
if err != nil {
status.Reason = ReasonInspectFailed
return status
}
jumps := countJumps(rules)
if jumps == 0 { if jumps == 0 {
status.Reason = ReasonJumpMissing status.Reason = ReasonJumpMissing
return status return status
@@ -289,7 +225,7 @@ func (m *Manager) Status(family string) FamilyStatus {
status.Reason = ReasonJumpDuplicate status.Reason = ReasonJumpDuplicate
return status return status
} }
if !hasFirstUniqueJump(rules) { if !hasFirstUniqueJump(chains) {
status.Reason = ReasonJumpNotFirst status.Reason = ReasonJumpNotFirst
return status return status
} }
@@ -299,7 +235,7 @@ func (m *Manager) Status(family string) FamilyStatus {
return status return status
} }
func (m *Manager) bindExistingFamily(executable string, required bool) error { func (m *Iptables) bindExistingFamily(executable string, required bool) error {
if !m.runner.Exists(executable) { if !m.runner.Exists(executable) {
if required { if required {
return fmt.Errorf("%s is not installed", executable) return fmt.Errorf("%s is not installed", executable)
@@ -312,7 +248,7 @@ func (m *Manager) bindExistingFamily(executable string, required bool) error {
} }
if !chainDeclared(output, DockerChain) { if !chainDeclared(output, DockerChain) {
if required { if required {
return ErrDockerIptablesChainUnavailable return buserr.New("ErrDockerIptablesChainUnavailable")
} }
return nil return nil
} }
@@ -325,7 +261,7 @@ func (m *Manager) bindExistingFamily(executable string, required bool) error {
return m.restoreLifecycle(executable, dockerGuardLifecycleRules(output, true, false)) return m.restoreLifecycle(executable, dockerGuardLifecycleRules(output, true, false))
} }
func (m *Manager) ensureFamily(executable string, required bool) error { func (m *Iptables) ensureFamily(executable string, required bool) error {
if !m.runner.Exists(executable) { if !m.runner.Exists(executable) {
if required { if required {
return fmt.Errorf("%s is not installed", executable) return fmt.Errorf("%s is not installed", executable)
@@ -338,7 +274,7 @@ func (m *Manager) ensureFamily(executable string, required bool) error {
} }
if !chainDeclared(output, DockerChain) { if !chainDeclared(output, DockerChain) {
if required { if required {
return ErrDockerIptablesChainUnavailable return buserr.New("ErrDockerIptablesChainUnavailable")
} }
return nil return nil
} }
@@ -361,7 +297,7 @@ func dockerGuardLifecycleRules(output string, bind, createOwned bool) [][]string
return rules return rules
} }
func (m *Manager) restoreLifecycle(executable string, rules [][]string) error { func (m *Iptables) restoreLifecycle(executable string, rules [][]string) error {
if len(rules) == 0 { if len(rules) == 0 {
return nil return nil
} }
@@ -379,7 +315,7 @@ func (m *Manager) restoreLifecycle(executable string, rules [][]string) error {
return nil return nil
} }
func (m *Manager) rebuildLocked(policies []Policy, inventory PolicyInventory) error { func (m *Iptables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family) executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) { if executable == "" || !m.runner.Exists(executable) {
@@ -444,7 +380,7 @@ func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInve
continue continue
} }
compiled := compilePolicy(policy) compiled := compilePolicy(policy)
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)] orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled { for ruleIndex, rule := range compiled {
order := int64(0) order := int64(0)
if ruleIndex < len(orders) { if ruleIndex < len(orders) {
@@ -521,7 +457,7 @@ func compilePolicy(policy Policy) [][]string {
return rules return rules
} }
func (m *Manager) unbindFamily(executable string) error { func (m *Iptables) unbindFamily(executable string) error {
if !m.runner.Exists(executable) { if !m.runner.Exists(executable) {
return nil return nil
} }
@@ -535,7 +471,7 @@ func (m *Manager) unbindFamily(executable string) error {
return nil return nil
} }
func (m *Manager) chainExists(executable, chain string) (bool, error) { func (m *Iptables) chainExists(executable, chain string) (bool, error) {
output, err := m.run(executable, "-S") output, err := m.run(executable, "-S")
if err != nil { if err != nil {
return false, err return false, err
@@ -553,7 +489,7 @@ func chainDeclared(output, chain string) bool {
return false return false
} }
func (m *Manager) run(executable string, args ...string) (string, error) { func (m *Iptables) run(executable string, args ...string) (string, error) {
commandArgs := append([]string{"-w", "-t", "filter"}, args...) commandArgs := append([]string{"-w", "-t", "filter"}, args...)
return m.runner.Run(executable, commandArgs...) return m.runner.Run(executable, commandArgs...)
} }
+84 -35
View File
@@ -3,6 +3,7 @@ package docker_guard
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/buserr"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -18,23 +19,22 @@ const (
dockerNftTable = "docker-bridges" dockerNftTable = "docker-bridges"
) )
type NftablesManager struct { type Nftables struct {
runner Runner runner Runner
} }
func NewNftablesManager() *NftablesManager { return &NftablesManager{runner: commandRunner{}} } func NewNftables() *Nftables { return &Nftables{runner: commandRunner{}} }
func (m *NftablesManager) Initialize(policies []Policy) error { func (m *Nftables) Initialize(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return errors.New("nft is not installed") return errors.New("nft is not installed")
} }
if err := m.checkForwardPolicy(); err != nil { if err := CheckIPv4Forwarding(); err != nil {
return err return err
} }
inventory, err := m.ListPolicies() if err := m.checkForwardPolicy(); err != nil {
if err != nil {
return err return err
} }
if err := m.ensureFamily(FamilyIPv4, true); err != nil { if err := m.ensureFamily(FamilyIPv4, true); err != nil {
@@ -46,7 +46,7 @@ func (m *NftablesManager) Initialize(policies []Policy) error {
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *NftablesManager) Bind() error { func (m *Nftables) Bind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if err := m.bindExistingFamily(FamilyIPv4, true); err != nil { if err := m.bindExistingFamily(FamilyIPv4, true); err != nil {
@@ -58,17 +58,13 @@ func (m *NftablesManager) Bind() error {
return nil return nil
} }
func (m *NftablesManager) Reconcile(policies []Policy) error { func (m *Nftables) ReplacePolicies(policies []Policy, inventory PolicyInventory) error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
inventory, err := m.ListPolicies()
if err != nil {
return err
}
return m.rebuildLocked(policies, inventory) return m.rebuildLocked(policies, inventory)
} }
func (m *NftablesManager) ListPolicies() (PolicyInventory, error) { func (m *Nftables) ListPolicies() (PolicyInventory, error) {
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return PolicyInventory{}, nil return PolicyInventory{}, nil
} }
@@ -95,7 +91,7 @@ func (m *NftablesManager) ListPolicies() (PolicyInventory, error) {
return inventory, nil return inventory, nil
} }
func (m *NftablesManager) Unbind() error { func (m *Nftables) Unbind() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
for _, family := range []string{FamilyIPv4, FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
@@ -106,7 +102,7 @@ func (m *NftablesManager) Unbind() error {
return nil return nil
} }
func (m *NftablesManager) Cleanup() error { func (m *Nftables) Cleanup() error {
mutationMu.Lock() mutationMu.Lock()
defer mutationMu.Unlock() defer mutationMu.Unlock()
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
@@ -123,7 +119,7 @@ func (m *NftablesManager) Cleanup() error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) Initialized(family string) (bool, error) { func (m *Nftables) Initialized(family string) (bool, error) {
if nftTableFamily(family) == "" || !m.runner.Exists("nft") { if nftTableFamily(family) == "" || !m.runner.Exists("nft") {
return false, nil return false, nil
} }
@@ -134,7 +130,7 @@ func (m *NftablesManager) Initialized(family string) (bool, error) {
return m.objectExists("chain", tableFamily, NftTable, NftChain), nil return m.objectExists("chain", tableFamily, NftTable, NftChain), nil
} }
func (m *NftablesManager) Status(family string) FamilyStatus { func (m *Nftables) Status(family string) FamilyStatus {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
if tableFamily == "" || !m.runner.Exists("nft") { if tableFamily == "" || !m.runner.Exists("nft") {
return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing}
@@ -142,16 +138,32 @@ func (m *NftablesManager) Status(family string) FamilyStatus {
if !m.objectExists("table", tableFamily, dockerNftTable) { if !m.objectExists("table", tableFamily, dockerNftTable) {
return FamilyStatus{State: StatusDisabled, Reason: ReasonDockerChainMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonDockerChainMissing}
} }
if !m.objectExists("chain", tableFamily, NftTable, NftBaseChain) || output, err := m.run("-a", "list", "table", tableFamily, NftTable)
!m.objectExists("chain", tableFamily, NftTable, NftChain) { if err != nil {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
}
baseExists, guardExists := false, false
currentChain := ""
var baseRules strings.Builder
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) >= 3 && fields[0] == "chain" && fields[2] == "{" {
currentChain = fields[1]
baseExists = baseExists || currentChain == NftBaseChain
guardExists = guardExists || currentChain == NftChain
} else if strings.TrimSpace(line) == "}" {
currentChain = ""
}
if currentChain == NftBaseChain {
baseRules.WriteString(line)
baseRules.WriteByte('\n')
}
}
if !baseExists || !guardExists {
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing} return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
} }
status := FamilyStatus{State: StatusNotEffective, Initialized: true} status := FamilyStatus{State: StatusNotEffective, Initialized: true}
rules, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain) rules := baseRules.String()
if err != nil {
status.Reason = ReasonInspectFailed
return status
}
jumps := nftJumpHandles(rules) jumps := nftJumpHandles(rules)
if len(jumps) == 0 { if len(jumps) == 0 {
status.Reason = ReasonJumpMissing status.Reason = ReasonJumpMissing
@@ -171,14 +183,14 @@ func (m *NftablesManager) Status(family string) FamilyStatus {
return status return status
} }
func (m *NftablesManager) ensureFamily(family string, required bool) error { func (m *Nftables) ensureFamily(family string, required bool) error {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
if tableFamily == "" { if tableFamily == "" {
return fmt.Errorf("unsupported address family %q", family) return fmt.Errorf("unsupported address family %q", family)
} }
if !m.objectExists("table", tableFamily, dockerNftTable) { if !m.objectExists("table", tableFamily, dockerNftTable) {
if required { if required {
return fmt.Errorf("%w %s", ErrDockerNftablesChainUnavailable, family) return fmt.Errorf("%w %s", buserr.New("ErrDockerNftablesChainUnavailable"), family)
} }
return nil return nil
} }
@@ -210,7 +222,7 @@ func (m *NftablesManager) ensureFamily(family string, required bool) error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) bindExistingFamily(family string, required bool) error { func (m *Nftables) bindExistingFamily(family string, required bool) error {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
if required { if required {
@@ -220,7 +232,7 @@ func (m *NftablesManager) bindExistingFamily(family string, required bool) error
} }
if !m.objectExists("table", tableFamily, dockerNftTable) { if !m.objectExists("table", tableFamily, dockerNftTable) {
if required { if required {
return fmt.Errorf("%w %s", ErrDockerNftablesChainUnavailable, family) return fmt.Errorf("%w %s", buserr.New("ErrDockerNftablesChainUnavailable"), family)
} }
return nil return nil
} }
@@ -234,7 +246,7 @@ func (m *NftablesManager) bindExistingFamily(family string, required bool) error
return m.ensureJump(family) return m.ensureJump(family)
} }
func (m *NftablesManager) ensureJump(family string) error { func (m *Nftables) ensureJump(family string) error {
tableFamily := nftTableFamily(family) tableFamily := nftTableFamily(family)
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain) output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
if err != nil { if err != nil {
@@ -248,7 +260,7 @@ func (m *NftablesManager) ensureJump(family string) error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) rebuildLocked(policies []Policy, inventory PolicyInventory) error { func (m *Nftables) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return nil return nil
} }
@@ -309,7 +321,7 @@ func orderedNftRules(family string, policies []Policy, inventory PolicyInventory
continue continue
} }
compiled := compileNftPolicy(policy) compiled := compileNftPolicy(policy)
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)] orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
for ruleIndex, rule := range compiled { for ruleIndex, rule := range compiled {
order := int64(0) order := int64(0)
if ruleIndex < len(orders) { if ruleIndex < len(orders) {
@@ -405,7 +417,7 @@ func validNftToken(token string) bool {
return !strings.ContainsAny(token, " \t\\\"'") return !strings.ContainsAny(token, " \t\\\"'")
} }
func (m *NftablesManager) unbindFamily(family string) error { func (m *Nftables) unbindFamily(family string) error {
if !m.runner.Exists("nft") { if !m.runner.Exists("nft") {
return nil return nil
} }
@@ -424,7 +436,7 @@ func (m *NftablesManager) unbindFamily(family string) error {
return m.runBatch(commands) return m.runBatch(commands)
} }
func (m *NftablesManager) runBatch(commands [][]string) error { func (m *Nftables) runBatch(commands [][]string) error {
if len(commands) == 0 { if len(commands) == 0 {
return nil return nil
} }
@@ -438,13 +450,13 @@ func (m *NftablesManager) runBatch(commands [][]string) error {
return nil return nil
} }
func (m *NftablesManager) objectExists(kind string, args ...string) bool { func (m *Nftables) objectExists(kind string, args ...string) bool {
command := append([]string{"list", kind}, args...) command := append([]string{"list", kind}, args...)
_, err := m.run(command...) _, err := m.run(command...)
return err == nil return err == nil
} }
func (m *NftablesManager) run(args ...string) (string, error) { func (m *Nftables) run(args ...string) (string, error) {
return m.runner.Run("nft", args...) return m.runner.Run("nft", args...)
} }
@@ -493,3 +505,40 @@ func nftHasFirstUniqueJump(output string) bool {
} }
return false return false
} }
func (m *Nftables) checkForwardPolicy() error {
for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6},
} {
if !m.runner.Exists(family.command) {
continue
}
output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD")
if err != nil {
return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)}
}
found := false
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" {
continue
}
found = true
if fields[2] == "DROP" {
label := "IPv4"
if family.name == FamilyIPv6 {
label = "IPv6"
}
return &FamilyError{Family: family.name, Err: buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": label}, nil)}
}
if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
}
}
if !found {
return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")}
}
}
return nil
}
+216 -119
View File
@@ -1,146 +1,243 @@
package docker_guard package docker_guard
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"net/netip" "net/netip"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"github.com/mattn/go-shellwords"
) )
var ErrInvalidPolicy = errors.New("invalid Docker port guard request") type observedPolicy struct {
policy Policy
sequence int64
nativeRules []NativeRule
managedOrders []int64
dropAll bool
droppedSource []string
allowedSource []string
acceptedSource []string
acceptAll bool
}
func NormalizePolicy(policy Policy) (Policy, error) { func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
policy.Family = strings.ToLower(strings.TrimSpace(policy.Family)) groups := make(map[string]*observedPolicy)
policy.HostIP = strings.TrimSpace(policy.HostIP) order := make([]string, 0)
policy.Protocol = strings.ToLower(strings.TrimSpace(policy.Protocol)) sequence := int64(0)
policy.Mode = strings.ToLower(strings.TrimSpace(policy.Mode)) for _, line := range strings.Split(output, "\n") {
if policy.HostPort == 0 || line = strings.TrimSpace(line)
(policy.Protocol != "tcp" && policy.Protocol != "udp") || if line == "" {
(policy.Family != FamilyIPv4 && policy.Family != FamilyIPv6) ||
(policy.Mode != ModeAll && policy.Mode != ModeSources && policy.Mode != ModeAllow) {
return Policy{}, fmt.Errorf("%w: invalid policy fields", ErrInvalidPolicy)
}
address, err := netip.ParseAddr(policy.HostIP)
if err != nil || (policy.Family == FamilyIPv4) != address.Is4() {
return Policy{}, fmt.Errorf("%w: host IP does not match address family", ErrInvalidPolicy)
}
normalizedSources := make([]string, 0, len(policy.Sources))
seen := make(map[string]struct{}, len(policy.Sources))
for _, source := range policy.Sources {
source = strings.TrimSpace(source)
if source == "" {
continue continue
} }
prefix, err := netip.ParsePrefix(source) tokens, err := shellwords.Parse(line)
if err != nil { if err != nil {
if sourceAddress, addressErr := netip.ParseAddr(source); addressErr == nil { return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
bits := 128
if sourceAddress.Is4() {
bits = 32
}
prefix = netip.PrefixFrom(sourceAddress, bits)
} else {
return Policy{}, fmt.Errorf("%w: invalid source address %q", ErrInvalidPolicy, source)
}
} }
if (policy.Family == FamilyIPv4) != prefix.Addr().Is4() { managed := strings.Contains(line, "1panel-docker:")
return Policy{}, fmt.Errorf("%w: source %q does not match address family", ErrInvalidPolicy, source) if !managed && !hasAcceptAction(tokens) {
}
canonical := prefix.Masked().String()
if _, exists := seen[canonical]; !exists {
seen[canonical] = struct{}{}
normalizedSources = append(normalizedSources, canonical)
}
}
if policy.Mode != ModeAll && len(normalizedSources) == 0 {
return Policy{}, fmt.Errorf("%w: source-based modes require at least one source", ErrInvalidPolicy)
}
if policy.Mode == ModeAll {
normalizedSources = []string{}
}
sort.Strings(normalizedSources)
policy.Sources = normalizedSources
return policy, nil
}
func NormalizePolicyUUIDs(values []string) ([]string, error) {
uuids := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, policyUUID := range values {
policyUUID = strings.TrimSpace(policyUUID)
if policyUUID == "" {
return nil, fmt.Errorf("%w: policy UUID cannot be empty", ErrInvalidPolicy)
}
if _, exists := seen[policyUUID]; exists {
continue continue
} }
seen[policyUUID] = struct{}{} sequence++
uuids = append(uuids, policyUUID) fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
} if err != nil {
if len(uuids) == 0 { return PolicyInventory{}, err
return nil, fmt.Errorf("%w: policy UUIDs cannot be empty", ErrInvalidPolicy) }
} identity := fragment.UUID
return uuids, nil if action == "accept" {
} identity = action
}
func PolicySyncKey(policy Policy) string { key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
mode := policy.Mode group, exists := groups[key]
if mode == ModeAllow && len(policy.Sources) == 0 { if !exists {
mode = ModeAll group = &observedPolicy{policy: fragment, sequence: sequence}
} groups[key] = group
sources := make([]string, 0, len(policy.Sources)) order = append(order, key)
for _, source := range policy.Sources { }
sources = append(sources, canonicalPolicySource(source)) switch {
} case action == "accept" && source != "":
sort.Strings(sources) group.acceptedSource = append(group.acceptedSource, source)
return strings.Join([]string{ group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
policy.UUID, policy.Family, CanonicalHost(policy.HostIP), strconv.Itoa(int(policy.HostPort)), case action == "accept":
policy.Protocol, mode, strings.Join(sources, ","), group.acceptAll = true
}, "\x00") group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
} case action == "return" && source != "":
group.allowedSource = append(group.allowedSource, source)
func canonicalPolicySource(value string) string { case action == "drop" && source != "":
value = strings.TrimSpace(value) group.droppedSource = append(group.droppedSource, source)
if prefix, err := netip.ParsePrefix(value); err == nil { case action == "drop":
return prefix.Masked().String() group.dropAll = true
} default:
if address, err := netip.ParseAddr(value); err == nil { return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
address = address.Unmap() }
return netip.PrefixFrom(address, address.BitLen()).String() if action != "accept" {
} group.managedOrders = append(group.managedOrders, sequence)
return value
}
func PolicyStatesEqual(left, right []Policy) bool {
if len(left) != len(right) {
return false
}
counts := make(map[string]int, len(left))
for _, policy := range left {
counts[PolicySyncKey(policy)]++
}
for _, policy := range right {
key := PolicySyncKey(policy)
if counts[key] == 0 {
return false
} }
counts[key]--
} }
return true inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: 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 len(group.allowedSource) > 0:
group.policy.Mode = ModeAllow
group.policy.Sources = uniqueSortedStrings(group.allowedSource)
case len(group.droppedSource) > 0:
group.policy.Mode = ModeSources
group.policy.Sources = uniqueSortedStrings(group.droppedSource)
case group.dropAll:
group.policy.Mode = ModeAll
default:
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
}
inventory.Policies = append(inventory.Policies, group.policy)
inventory.ManagedRuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.managedOrders...)
}
return inventory, nil
} }
func CanonicalHost(value string) string { func nativeRuleTokens(tokens []string) []string {
if address, err := netip.ParseAddr(value); err == nil { result := make([]string, 0, len(tokens))
return address.String() for index, token := range tokens {
if token == "#" {
tokens = tokens[:index]
break
}
}
if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" {
tokens = tokens[:len(tokens)-2]
}
for index := 0; index < len(tokens); index++ {
result = append(result, tokens[index])
if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" {
index += 4
}
} }
return result
}
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
policy := Policy{Family: family, HostIP: "0.0.0.0"}
if family == FamilyIPv6 {
policy.HostIP = "::"
}
source, action := "", ""
for index := 0; index < len(tokens); index++ {
switch tokens[index] {
case "-p":
policy.Protocol = nextPolicyToken(tokens, index)
case "--ctorigdst":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "-d":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "--ctorigdstport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "--dport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "-s":
source = nextPolicyToken(tokens, index)
case "--comment", "comment":
marker := nextPolicyToken(tokens, index)
if strings.HasPrefix(marker, "1panel-docker:") {
policy.UUID = strings.TrimPrefix(marker, "1panel-docker:")
}
case "-j":
action = strings.ToLower(nextPolicyToken(tokens, index))
case "meta":
if nextPolicyToken(tokens, index) == "l4proto" {
policy.Protocol = nextPolicyToken(tokens, index+1)
}
case "ct":
if nextPolicyToken(tokens, index) != "original" {
continue
}
switch nextPolicyToken(tokens, index+1) {
case "proto-dst":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+2))
case "ip", "ip6":
if nextPolicyToken(tokens, index+2) == "daddr" {
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+3))
}
}
case "ip", "ip6":
switch nextPolicyToken(tokens, index) {
case "saddr":
source = nextPolicyToken(tokens, index+1)
case "daddr":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1))
}
case "tcp", "udp":
if nextPolicyToken(tokens, index) == "dport" {
policy.Protocol = tokens[index]
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
}
case "accept", "drop", "return":
if index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment") {
continue
}
action = tokens[index]
}
}
if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) {
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
}
if action == "accept" && policy.Protocol == "" {
policy.Protocol = "all"
}
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()
}
return value return value
}
func nextPolicyToken(tokens []string, index int) string {
if index+1 >= len(tokens) {
return ""
}
return tokens[index+1]
}
func parsePolicyPort(value string) uint16 {
port, err := strconv.ParseUint(value, 10, 16)
if err != nil || port == 0 {
return 0
}
return uint16(port)
} }
func DecodeSources(value string) []string { func uniqueSortedStrings(values []string) []string {
result := []string{} seen := make(map[string]struct{}, len(values))
_ = json.Unmarshal([]byte(value), &result) result := make([]string, 0, len(values))
for _, value := range values {
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
sort.Strings(result)
return result return result
} }
@@ -1,258 +0,0 @@
package docker_guard
import (
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
"github.com/mattn/go-shellwords"
)
type observedPolicy struct {
policy Policy
sequence int64
nativeRules []NativeRule
managedOrders []int64
dropAll bool
droppedSource []string
allowedSource []string
acceptedSource []string
acceptAll bool
}
func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
groups := make(map[string]*observedPolicy)
order := make([]string, 0)
sequence := int64(0)
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
tokens, err := shellwords.Parse(line)
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
}
key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
group, exists := groups[key]
if !exists {
group = &observedPolicy{policy: fragment, sequence: sequence}
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 != "":
group.droppedSource = append(group.droppedSource, source)
case action == "drop":
group.dropAll = true
default:
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
}
if action != "accept" {
group.managedOrders = append(group.managedOrders, sequence)
}
}
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: 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 len(group.allowedSource) > 0:
group.policy.Mode = ModeAllow
group.policy.Sources = uniqueSortedStrings(group.allowedSource)
case len(group.droppedSource) > 0:
group.policy.Mode = ModeSources
group.policy.Sources = uniqueSortedStrings(group.droppedSource)
case group.dropAll:
group.policy.Mode = ModeAll
default:
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
}
inventory.Policies = append(inventory.Policies, group.policy)
inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...)
}
return inventory, nil
}
func nativeRuleTokens(tokens []string) []string {
result := make([]string, 0, len(tokens))
for index, token := range tokens {
if token == "#" {
tokens = tokens[:index]
break
}
}
if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" {
tokens = tokens[:len(tokens)-2]
}
for index := 0; index < len(tokens); index++ {
result = append(result, tokens[index])
if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" {
index += 4
}
}
return result
}
func managedOrderKey(family, policyUUID string) string {
return family + "\x00" + policyUUID
}
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
policy := Policy{Family: family, HostIP: wildcardHost(family)}
source, action := "", ""
for index := 0; index < len(tokens); index++ {
switch tokens[index] {
case "-p":
policy.Protocol = nextPolicyToken(tokens, index)
case "--ctorigdst":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "-d":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "--ctorigdstport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "--dport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "-s":
source = nextPolicyToken(tokens, index)
case "--comment", "comment":
marker := nextPolicyToken(tokens, index)
if strings.HasPrefix(marker, "1panel-docker:") {
policy.UUID = strings.TrimPrefix(marker, "1panel-docker:")
}
case "-j":
action = strings.ToLower(nextPolicyToken(tokens, index))
case "meta":
if nextPolicyToken(tokens, index) == "l4proto" {
policy.Protocol = nextPolicyToken(tokens, index+1)
}
case "ct":
if nextPolicyToken(tokens, index) != "original" {
continue
}
switch nextPolicyToken(tokens, index+1) {
case "proto-dst":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+2))
case "ip", "ip6":
if nextPolicyToken(tokens, index+2) == "daddr" {
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+3))
}
}
case "ip", "ip6":
switch nextPolicyToken(tokens, index) {
case "saddr":
source = nextPolicyToken(tokens, index+1)
case "daddr":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1))
}
case "tcp", "udp":
if nextPolicyToken(tokens, index) == "dport" {
policy.Protocol = tokens[index]
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
}
case "accept", "drop", "return":
if isCommentValue(tokens, index) {
continue
}
action = tokens[index]
}
}
if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) {
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
}
if action == "accept" && policy.Protocol == "" {
policy.Protocol = "all"
}
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") && !isCommentValue(tokens, index) {
return true
}
}
return false
}
func isCommentValue(tokens []string, index int) bool {
if index == 0 {
return false
}
return tokens[index-1] == "comment" || tokens[index-1] == "--comment"
}
func normalizeObservedHost(value string) string {
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
return prefix.Addr().String()
}
return value
}
func nextPolicyToken(tokens []string, index int) string {
if index+1 >= len(tokens) {
return ""
}
return tokens[index+1]
}
func parsePolicyPort(value string) uint16 {
port, err := strconv.ParseUint(value, 10, 16)
if err != nil || port == 0 {
return 0
}
return uint16(port)
}
func wildcardHost(family string) string {
if family == FamilyIPv6 {
return "::"
}
return "0.0.0.0"
}
func uniqueSortedStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
sort.Strings(result)
return result
}
+77 -103
View File
@@ -1,12 +1,77 @@
package docker_guard package docker_guard
import ( import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"fmt" "fmt"
"slices" "os"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
) )
const (
Chain = "1PANEL_DOCKER"
DockerChain = "DOCKER-USER"
FamilyIPv4 = constant.FirewallFamilyIPv4
FamilyIPv6 = constant.FirewallFamilyIPv6
ModeSources = "deny_sources"
ModeAllow = "allow_sources"
ModeAll = "deny_all"
StatusEffective = "effective"
StatusDisabled = "disabled"
StatusNotEffective = "not_effective"
ReasonCommandMissing = "command_missing"
ReasonDockerChainMissing = "docker_chain_missing"
ReasonGuardChainMissing = "guard_chain_missing"
ReasonJumpMissing = "jump_missing"
ReasonJumpNotFirst = "jump_not_first"
ReasonJumpDuplicate = "jump_duplicate"
ReasonInspectFailed = "inspect_failed"
)
type FamilyError struct {
Family string
Err error
}
func (e *FamilyError) Error() string { return fmt.Sprintf("%s Docker port guard: %v", e.Family, e.Err) }
func (e *FamilyError) Unwrap() error { return e.Err }
type ProxyEndpoint struct {
Protocol string
HostIP string
HostPort uint16
}
type ProxyEndpoints struct {
Items []ProxyEndpoint
Inspected bool
}
type DNATRules struct {
Output string
Inspected bool
}
type Policy struct {
UUID string
Family string
HostIP string
HostPort uint16
Protocol string
Mode string
Sources []string
}
type FamilyStatus struct {
State string
Reason string
Initialized bool
Bound bool
Effective bool
}
type NativeRule struct { type NativeRule struct {
Family string `json:"family"` Family string `json:"family"`
Order int64 `json:"order"` Order int64 `json:"order"`
@@ -27,9 +92,9 @@ type PolicyInventory struct {
} }
type Runtime interface { type Runtime interface {
Initialize([]Policy) error Initialize([]Policy, PolicyInventory) error
Bind() error Bind() error
Reconcile([]Policy) error ReplacePolicies([]Policy, PolicyInventory) error
Unbind() error Unbind() error
Cleanup() error Cleanup() error
Initialized(string) (bool, error) Initialized(string) (bool, error)
@@ -37,110 +102,19 @@ type Runtime interface {
ListPolicies() (PolicyInventory, error) ListPolicies() (PolicyInventory, error)
} }
func NewRuntime(provider string) Runtime { const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
if provider == constant.FirewallProviderNftables {
return NewNftablesManager() func CheckIPv4Forwarding() error {
} return checkIPv4Forwarding(os.ReadFile)
return NewManager()
} }
func Verify(runtime Runtime, desired []Policy, preserved []ReadOnlyPolicy) error { func checkIPv4Forwarding(readFile func(string) ([]byte, error)) error {
inventory, err := runtime.ListPolicies() value, err := readFile(ipv4ForwardingPath)
if err != nil { if err != nil {
return fmt.Errorf("verify synchronized Docker firewall policies: %w", err) return fmt.Errorf("inspect IPv4 forwarding: %w", err)
} }
if !PolicyStatesEqual(inventory.Policies, desired) { if strings.TrimSpace(string(value)) != "1" {
return fmt.Errorf("verify synchronized Docker firewall policies: target policies do not match the database") return buserr.New("ErrDockerIPv4ForwardingDisabled")
}
if !readOnlyStatesEqual(inventory.ReadOnly, preserved) {
return fmt.Errorf("verify synchronized Docker firewall policies: read-only runtime rules changed")
}
return nil
}
func readOnlyStatesEqual(left, right []ReadOnlyPolicy) bool {
if len(left) != len(right) {
return false
}
leftRules := flattenNativeRules(left)
rightRules := flattenNativeRules(right)
if len(leftRules) != len(rightRules) {
return false
}
for index := range leftRules {
if leftRules[index].Family != rightRules[index].Family || !slices.Equal(leftRules[index].Tokens, rightRules[index].Tokens) {
return false
}
}
return true
}
func flattenNativeRules(policies []ReadOnlyPolicy) []NativeRule {
rules := make([]NativeRule, 0)
for _, policy := range policies {
rules = append(rules, policy.NativeRules...)
}
slices.SortStableFunc(rules, func(left, right NativeRule) int {
if left.Family < right.Family {
return -1
}
if left.Family > right.Family {
return 1
}
if left.Order < right.Order {
return -1
}
if left.Order > right.Order {
return 1
}
return 0
})
return rules
}
func ReconcileTarget(backend string, policies []Policy, runtime Runtime) 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{FamilyIPv4, FamilyIPv6} {
status := runtime.Status(family)
if status.Reason == ReasonInspectFailed {
return fmt.Errorf("inspect Docker firewall target %s for %s failed", backend, family)
}
initialized = initialized || status.Initialized
}
if initialized {
return runtime.Reconcile(nil)
}
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)
} else {
if needsBind {
err = runtime.Bind()
}
if err == nil {
err = runtime.Reconcile(policies)
}
}
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 return nil
} }
+18 -28
View File
@@ -21,7 +21,7 @@ const (
ChangeReorder ChangeOperation = "reorder" ChangeReorder ChangeOperation = "reorder"
) )
type DesiredChange struct { type RuleChange struct {
CommandOnly bool `json:"-"` CommandOnly bool `json:"-"`
UnmarkedAdopted bool `json:"-"` UnmarkedAdopted bool `json:"-"`
Operation ChangeOperation `json:"operation"` Operation ChangeOperation `json:"operation"`
@@ -39,7 +39,7 @@ type NativeCommand struct {
Stdin string `json:"stdin,omitempty"` Stdin string `json:"stdin,omitempty"`
} }
type NativeRulePlan struct { type RuleCommands struct {
RuleUUID string `json:"ruleUUID"` RuleUUID string `json:"ruleUUID"`
Operation ChangeOperation `json:"operation"` Operation ChangeOperation `json:"operation"`
Commands []NativeCommand `json:"commands"` Commands []NativeCommand `json:"commands"`
@@ -48,15 +48,14 @@ type NativeRulePlan struct {
Expected ObservedRule `json:"expected"` Expected ObservedRule `json:"expected"`
} }
type BackendPlan struct { type CommandBatch struct {
CommandOnly bool `json:"-"` CommandOnly bool `json:"-"`
Provider Provider `json:"provider"` Provider Provider `json:"provider"`
Scope Scope `json:"scope"` Scope Scope `json:"scope"`
SnapshotRevision string `json:"snapshotRevision"` Rules []RuleCommands `json:"rules"`
Rules []NativeRulePlan `json:"rules"`
} }
func (p BackendPlan) CreatesOnly() bool { func (p CommandBatch) CreatesOnly() bool {
if len(p.Rules) == 0 { if len(p.Rules) == 0 {
return false return false
} }
@@ -68,27 +67,17 @@ func (p BackendPlan) CreatesOnly() bool {
return true return true
} }
type ApplyResult struct {
Applied []ObservedRule `json:"applied"`
Verification *VerifyResult `json:"verification,omitempty"`
}
type VerifyResult struct {
Snapshot Snapshot `json:"snapshot"`
Matched bool `json:"matched"`
}
type Adapter interface { type Adapter interface {
Provider() Provider Provider() Provider
Capabilities(context.Context) (Capabilities, error) Capabilities(context.Context) (Capabilities, error)
Observe(context.Context, Scope) (Snapshot, error) ListRules(context.Context, Scope) (RuleSet, error)
Compile(Snapshot, []DesiredChange) (BackendPlan, error) BuildCommands(RuleSet, []RuleChange) (CommandBatch, error)
Apply(context.Context, BackendPlan) (ApplyResult, error) RunCommands(context.Context, CommandBatch) error
Verify(context.Context, BackendPlan) (VerifyResult, error) Rollback(context.Context, CommandBatch) error
} }
type MultiScopeObserver interface { type MultiScopeReader interface {
ObserveScopes(context.Context, []Scope) ([]Snapshot, error) ListRuleScopes(context.Context, []Scope) ([]RuleSet, error)
} }
type RulePreparer interface { type RulePreparer interface {
@@ -99,7 +88,8 @@ type RuleChecker interface {
CheckRule(context.Context, FirewallRule) error CheckRule(context.Context, FirewallRule) error
} }
type UnverifiedRuleAppender interface { type ExternalRuleAdapter interface {
ListRulesByComment(context.Context, []Scope, string) ([]ObservedRule, error)
AppendUnverified(context.Context, FirewallRule, string) error AppendUnverified(context.Context, FirewallRule, string) error
} }
@@ -107,6 +97,6 @@ type NativeDetailReader interface {
NativeDetail(context.Context, string, bool) (string, error) NativeDetail(context.Context, string, bool) (string, error)
} }
type PlanRollbacker interface { type RuleSaver interface {
Rollback(context.Context, BackendPlan) error SaveRules(context.Context, Scope) error
} }
-25
View File
@@ -1,25 +0,0 @@
package filter
import "fmt"
var ErrDuplicateAdoption = fmt.Errorf("%w: duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", ErrRuleOperation)
func CheckAdoptDuplicates(snapshot Snapshot, requested FirewallRule) error {
count := 0
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported {
continue
}
same, err := SameRuleContent(observed.Rule, requested)
if err != nil {
return err
}
if same {
count++
if count > 1 {
return ErrDuplicateAdoption
}
}
}
return nil
}
+20
View File
@@ -0,0 +1,20 @@
package filter
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type CommentRuleReader interface {
ReadRulesByComment(context.Context, Scope, string) (string, error)
}
func ReadRulesByComment(ctx context.Context, executable string, args []string, comment string) (string, error) {
name, args := cmd.WrapWithOptionalSudo(executable, args...)
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LC_ALL=C", "LANGUAGE=en_US:en")).RunPipe(
cmd.PipeCommand{Name: name, Args: args},
cmd.PipeCommand{Name: "sh", Args: []string{"-c", `grep -F -- "$1"; result=$?; if [ "$result" -eq 1 ]; then exit 0; fi; exit "$result"`, "sh", comment}},
)
}
+33 -108
View File
@@ -5,7 +5,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort"
"strings" "strings"
) )
@@ -76,80 +75,6 @@ func SameRuleContent(before, after FirewallRule) (bool, error) {
return err == nil && previous == requested && before.Action == after.Action, err return err == nil && previous == requested && before.Action == after.Action, err
} }
type RuleCollisionIndex map[string][]Action
func (index RuleCollisionIndex) Add(rule FirewallRule) error {
key, err := RuleMatchKey(rule)
if err != nil {
return err
}
index[key] = append(index[key], rule.Action)
return nil
}
func (index RuleCollisionIndex) CheckDuplicate(rule FirewallRule) error {
key, err := RuleMatchKey(rule)
if err != nil {
return err
}
for _, action := range index[key] {
if action == rule.Action {
return checkCollisionActions(rule.Action, action)
}
}
return nil
}
func (index RuleCollisionIndex) Check(rule FirewallRule) error {
key, err := RuleMatchKey(rule)
if err != nil {
return err
}
for _, action := range index[key] {
if err := checkCollisionActions(rule.Action, action); err != nil {
return err
}
}
return nil
}
func CheckRuleCollision(requested, existing FirewallRule) error {
wanted, err := RuleMatchKey(requested)
if err != nil {
return err
}
actual, err := RuleMatchKey(existing)
if err != nil {
return err
}
if wanted != actual {
return nil
}
return checkCollisionActions(requested.Action, existing.Action)
}
func checkCollisionActions(requested, existing Action) error {
if requested == existing {
return fmt.Errorf("%w: equivalent rule already exists", ErrRuleOperation)
}
if OppositeActions(requested, existing) {
return ErrRuleConflict
}
return nil
}
func CheckObservedRuleCollisions(snapshot Snapshot, requested FirewallRule, excluded *Locator) error {
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported || excluded != nil && SameLocator(observed.Locator, *excluded) {
continue
}
if err := CheckRuleCollision(requested, observed.Rule); err != nil {
return err
}
}
return nil
}
func normalizedRuleKey(normalized FirewallRule) (string, error) { func normalizedRuleKey(normalized FirewallRule) (string, error) {
identity := ruleIdentity{ identity := ruleIdentity{
Scope: normalized.Scope.Key(), Scope: normalized.Scope.Key(),
@@ -209,49 +134,25 @@ func opaqueInstanceKey(rule ObservedRule, locator Locator) (string, error) {
}{Raw: strings.TrimSpace(rule.Raw), Locator: locator, Persistence: rule.Persistence}) }{Raw: strings.TrimSpace(rule.Raw), Locator: locator, Persistence: rule.Persistence})
} }
func SnapshotRevision(scope Scope, rules []ObservedRule) (string, error) { func NewRuleSet(scope Scope, rules []ObservedRule) (RuleSet, error) {
scope = scope.Normalize() scope = scope.Normalize()
if err := scope.ValidateMVP(); err != nil { if err := scope.ValidateMVP(); err != nil {
return "", err return RuleSet{}, err
} }
identities := make([]string, 0, len(rules))
for _, rule := range rules { for _, rule := range rules {
if rule.Rule.Scope.Normalize().Key() != scope.Key() { if rule.Rule.Scope.Normalize().Key() != scope.Key() {
return "", fmt.Errorf("%w: observed rule scope %q does not match snapshot scope %q", ErrInvalidRule, rule.Rule.Scope.Key(), scope.Key()) return RuleSet{}, fmt.Errorf("%w: observed rule scope does not match read scope", ErrInvalidRule)
} }
if rule.ParseStatus != ParseStatusSupported { if rule.ParseStatus == ParseStatusSupported {
locator, err := validatedLocator(rule.Locator, scope) if _, err := NormalizeRule(rule.Rule); err != nil {
if err != nil { return RuleSet{}, err
return "", err
} }
identity, err := opaqueInstanceKey(rule, locator)
if err != nil {
return "", err
}
identities = append(identities, identity)
continue
} }
if _, err := validatedLocator(rule.Locator, scope); err != nil {
identity, err := InstanceKey(rule) return RuleSet{}, err
if err != nil {
return "", err
} }
identities = append(identities, identity)
} }
sort.Strings(identities) return RuleSet{Scope: scope, Rules: rules}, nil
return hashJSON(struct {
Scope string `json:"scope"`
Rules []string `json:"rules"`
}{Scope: scope.Key(), Rules: identities})
}
func NewSnapshot(scope Scope, rules []ObservedRule) (Snapshot, error) {
revision, err := SnapshotRevision(scope, rules)
if err != nil {
return Snapshot{}, err
}
return Snapshot{Scope: scope.Normalize(), Revision: revision, Rules: rules}, nil
} }
func normalizeLocator(locator Locator, scope Scope) Locator { func normalizeLocator(locator Locator, scope Scope) Locator {
@@ -306,3 +207,27 @@ func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, er
} }
return matched[0], nil return matched[0], nil
} }
const ObservedFieldProtocol = "protocol"
// ObservedRuleMatchesExpected compares the semantic fields that the backend
// could actually observe. Partial rules name fields omitted by their native
// listing; those fields are supplied from the expected rule before comparing
// normalized identities.
func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) bool {
if observed.ParseStatus == ParseStatusOpaque {
return false
}
hydrated := observed.Rule
for _, field := range observed.UncertainFields {
switch field {
case ObservedFieldProtocol:
hydrated.Protocol = expected.Protocol
default:
return false
}
}
gotKey, gotErr := RuleKey(hydrated)
wantKey, wantErr := RuleKey(expected)
return gotErr == nil && wantErr == nil && gotKey == wantKey
}
-236
View File
@@ -1,9 +1,6 @@
package filter package filter
import ( import (
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
) )
@@ -71,236 +68,3 @@ type InventoryMergeInput struct {
Desired []DesiredRule Desired []DesiredRule
ProtectedObservedKeys map[string]struct{} ProtectedObservedKeys map[string]struct{}
} }
type observedInventoryCandidate struct {
rule ObservedRule
ruleKey string
instanceKey string
claimed bool
}
func MergeInventory(input InventoryMergeInput) ([]InventoryItem, error) {
candidates := make([]observedInventoryCandidate, len(input.Observed))
byRuleKey := make(map[string][]int)
byInstanceKey := make(map[string][]int)
byMarker := make(map[string][]int)
for index, observed := range input.Observed {
candidate := observedInventoryCandidate{rule: observed}
if marker := strings.TrimSpace(candidate.rule.Marker); marker != "" {
markerKey := candidate.rule.Rule.Scope.Key() + "\x00" + marker
byMarker[markerKey] = append(byMarker[markerKey], index)
}
if observed.ParseStatus == ParseStatusSupported {
normalized, err := NormalizeRule(observed.Rule)
if err != nil {
return nil, fmt.Errorf("normalize observed firewall rule %d: %w", index, err)
}
candidate.rule.Rule = normalized
candidate.ruleKey, err = normalizedRuleKey(normalized)
if err != nil {
return nil, err
}
byRuleKey[candidate.ruleKey] = append(byRuleKey[candidate.ruleKey], index)
if instanceKey, err := instanceKeyWithRuleKey(candidate.rule, candidate.ruleKey); err == nil {
candidate.instanceKey = instanceKey
candidate.rule.InstanceKey = instanceKey
byInstanceKey[instanceKey] = append(byInstanceKey[instanceKey], index)
}
}
candidates[index] = candidate
}
normalizedDesired := make([]DesiredRule, 0, len(input.Desired))
desiredMatches := make(map[int]int)
desiredMatchStates := make([]InventoryMatch, 0, len(input.Desired))
for _, desired := range input.Desired {
normalized, err := NormalizeRule(desired.Rule)
if err != nil {
return nil, fmt.Errorf("normalize desired firewall rule %q: %w", desired.UUID, err)
}
desired.Rule = normalized
calculatedKey, err := normalizedRuleKey(normalized)
if err != nil {
return nil, err
}
if desired.RuleKey != "" && desired.RuleKey != calculatedKey {
return nil, fmt.Errorf("%w: desired rule %q key does not match its semantics", ErrInvalidRule, desired.UUID)
}
desired.RuleKey = calculatedKey
match, matchState := findObservedInventoryMatch(desired, candidates, byRuleKey, byInstanceKey, byMarker)
normalizedIndex := len(normalizedDesired)
normalizedDesired = append(normalizedDesired, desired)
desiredMatchStates = append(desiredMatchStates, matchState)
if match >= 0 {
candidates[match].claimed = true
desiredMatches[match] = normalizedIndex
}
}
items := make([]InventoryItem, 0, len(candidates)+len(normalizedDesired))
matchedDesired := make(map[int]struct{}, len(desiredMatches))
for index := range candidates {
candidate := &candidates[index]
if desiredIndex, exists := desiredMatches[index]; exists {
desired := normalizedDesired[desiredIndex]
observed := candidate.rule
match := desiredMatchStates[desiredIndex]
if match == InventoryMatchExact && observed.ParseStatus != ParseStatusSupported {
orderIndex := observed.Rule.OrderIndex
observed.Rule = desired.Rule
observed.Rule.OrderIndex = orderIndex
observed.ParseStatus = ParseStatusSupported
observed.UncertainFields = nil
}
displayRule := observed.Rule
displayRule.Description = desired.Rule.Description
state := inventoryStateForDesired(desired, match)
if observed.Protected {
state = InventoryStateProtected
} else if observed.Persistence != "" && observed.Persistence != PersistenceStatusConverged {
state = InventoryStateDrifted
}
items = append(items, InventoryItem{
Rule: displayRule,
Observed: &observed,
Desired: &desired,
State: state,
Match: match,
})
matchedDesired[desiredIndex] = struct{}{}
continue
}
observed := candidate.rule
state := InventoryStateExternal
if observed.Protected {
state = InventoryStateProtected
} else if _, protected := input.ProtectedObservedKeys[candidate.ruleKey]; protected {
state = InventoryStateProtected
}
match := InventoryMatchNone
if observed.ParseStatus != ParseStatusSupported {
match = InventoryMatchOpaque
}
items = append(items, InventoryItem{Rule: observed.Rule, Observed: &observed, State: state, Match: match})
}
for index, desired := range normalizedDesired {
if _, matched := matchedDesired[index]; matched {
continue
}
desiredCopy := desired
match := desiredMatchStates[index]
items = append(items, InventoryItem{
Rule: desired.Rule,
Desired: &desiredCopy,
State: inventoryStateForDesired(desired, match),
Match: match,
})
}
return items, nil
}
func findObservedInventoryMatch(
desired DesiredRule,
candidates []observedInventoryCandidate,
byRuleKey map[string][]int,
byInstanceKey map[string][]int,
byMarker map[string][]int,
) (int, InventoryMatch) {
if marker := strings.TrimSpace(desired.Marker); marker != "" {
markerKey := desired.Rule.Scope.Key() + "\x00" + marker
match, status := uniqueUnclaimedCandidate(byMarker[markerKey], candidates)
if match >= 0 && candidates[match].rule.ParseStatus != ParseStatusOpaque &&
!ObservedRuleMatchesExpected(candidates[match].rule, desired.Rule) {
return match, InventoryMatchChanged
}
if status != InventoryMatchMissing {
return match, status
}
if desired.Origin == RuleOriginAdopted {
match, status = uniqueUnclaimedSemanticCandidate(desired.Rule, "", candidates)
if status != InventoryMatchMissing {
if match >= 0 {
return match, InventoryMatchChanged
}
return match, status
}
}
legacyMarker := "1panel-rule:" + strings.TrimSpace(desired.UUID)
if legacyMarker != "1panel-rule:" && legacyMarker != marker {
match, status = uniqueUnclaimedSemanticCandidate(desired.Rule, legacyMarker, candidates)
if status != InventoryMatchMissing {
if match >= 0 {
return match, InventoryMatchChanged
}
return match, status
}
}
return match, status
}
if desired.ObservedInstanceKey != "" {
return uniqueUnclaimedCandidate(byInstanceKey[desired.ObservedInstanceKey], candidates)
}
return uniqueUnclaimedCandidate(byRuleKey[desired.RuleKey], candidates)
}
func uniqueUnclaimedSemanticCandidate(
expected FirewallRule,
marker string,
candidates []observedInventoryCandidate,
) (int, InventoryMatch) {
match := -1
count := 0
for index := range candidates {
candidate := candidates[index]
if candidate.claimed || strings.TrimSpace(candidate.rule.Marker) != marker ||
!ObservedRuleMatchesExpected(candidate.rule, expected) {
continue
}
match = index
count++
}
switch count {
case 0:
return -1, InventoryMatchMissing
case 1:
return match, InventoryMatchExact
default:
return -1, InventoryMatchAmbiguous
}
}
func uniqueUnclaimedCandidate(indices []int, candidates []observedInventoryCandidate) (int, InventoryMatch) {
match := -1
count := 0
for _, index := range indices {
if candidates[index].claimed {
continue
}
match = index
count++
}
switch count {
case 0:
return -1, InventoryMatchMissing
case 1:
return match, InventoryMatchExact
default:
return -1, InventoryMatchAmbiguous
}
}
func inventoryStateForDesired(desired DesiredRule, match InventoryMatch) InventoryState {
if match != InventoryMatchExact {
return InventoryStateDrifted
}
if desired.Protected {
return InventoryStateProtected
}
switch desired.Origin {
case RuleOriginAdopted:
return InventoryStateAdopted
default:
return InventoryStateManaged
}
}
@@ -1,38 +0,0 @@
package filter
func InventoryPositionRanges(provider Provider, items []InventoryItem) (ipv4, ipv6 PositionRange) {
if provider == ProviderFirewalld {
return PositionRange{Min: -32768, Max: 32767}, PositionRange{Min: -32768, Max: 32767}
}
for _, item := range items {
if item.Observed == nil || item.Observed.Locator.Position == nil {
continue
}
scope := item.Observed.Rule.Scope
if scope.Provider != provider || scope.Direction != DirectionInput {
continue
}
if (provider == ProviderIptables || provider == ProviderNftables) &&
(scope.Table != "filter" || scope.Chain != IptablesInputChain) {
continue
}
bounds := &ipv4
if scope.Family == FamilyIPv6 {
bounds = &ipv6
} else if scope.Family != FamilyIPv4 {
continue
}
position := *item.Observed.Locator.Position
if position < 1 {
continue
}
if bounds.Min == 0 || position < bounds.Min {
bounds.Min = position
}
bounds.Max = max(bounds.Max, position)
if provider != ProviderUFW {
bounds.Min = 1
}
}
return
}
+16 -15
View File
@@ -28,9 +28,11 @@ const (
) )
type PortWhitelist struct { type PortWhitelist struct {
Family string `json:"family"` Family string `json:"-"`
Port string `json:"port"` Port string `json:"port,omitempty"`
Protocol string `json:"protocol"` Protocol string `json:"protocol,omitempty"`
Type string `json:"type,omitempty"`
Sources []string `json:"sources"`
} }
type Direction string type Direction string
@@ -108,13 +110,12 @@ type ScopeNotice struct {
} }
var ( var (
ErrInvalidScope = errors.New("invalid firewall scope") ErrInvalidScope = errors.New("invalid firewall scope")
ErrUnsupportedScope = errors.New("unsupported firewall scope") ErrUnsupportedScope = errors.New("unsupported firewall scope")
ErrManagedScopeChange = fmt.Errorf("%w: managed rule scope cannot be changed", ErrUnsupportedScope) ErrInvalidRule = errors.New("invalid firewall rule")
ErrInvalidRule = errors.New("invalid firewall rule") ErrProtectedRule = errors.New("protected firewall rule cannot be modified")
ErrProtectedRule = errors.New("protected firewall rule cannot be modified") ErrCompositeRule = errors.New("firewall rule must be atomic")
ErrCompositeRule = errors.New("firewall rule must be atomic") ErrExpansionLimit = errors.New("firewall rule expansion limit exceeded")
ErrExpansionLimit = errors.New("firewall rule expansion limit exceeded")
) )
type Scope struct { type Scope struct {
@@ -296,11 +297,11 @@ type ObservedRule struct {
Persistence PersistenceStatus `json:"persistence,omitempty"` Persistence PersistenceStatus `json:"persistence,omitempty"`
} }
type Snapshot struct { type RuleSet struct {
Scope Scope `json:"scope"` LastPosition int `json:"-"`
Revision string `json:"revision"` Scope Scope `json:"scope"`
Rules []ObservedRule `json:"rules"` Rules []ObservedRule `json:"rules"`
Notices []ScopeNotice `json:"notices,omitempty"` Notices []ScopeNotice `json:"notices,omitempty"`
} }
type Capabilities struct { type Capabilities struct {
+7 -19
View File
@@ -8,7 +8,7 @@ import (
"strings" "strings"
) )
const MaxAtomicExpansion = 256 const MaxAtomicExpansion = 500
func NormalizeRule(rule FirewallRule) (FirewallRule, error) { func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
rule.Scope = rule.Scope.Normalize() rule.Scope = rule.Scope.Normalize()
@@ -16,9 +16,9 @@ func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
return FirewallRule{}, err return FirewallRule{}, err
} }
if hasCompositeValue(rule.SourceAddress) || hasCompositeValue(rule.DestinationAddress) || if strings.Contains(rule.SourceAddress, ",") || strings.Contains(rule.DestinationAddress, ",") ||
hasCompositeValue(rule.SourcePort) || strings.Contains(rule.SourcePort, ",") ||
(hasCompositeValue(rule.DestinationPort) && !supportsNativeDestinationPortSet(rule.Scope.Provider)) || (strings.Contains(rule.DestinationPort, ",") && rule.Scope.Provider != ProviderIptables && rule.Scope.Provider != ProviderUFW) ||
isCompositeProtocol(rule.Protocol) { isCompositeProtocol(rule.Protocol) {
return FirewallRule{}, fmt.Errorf("%w: expand addresses, ports and protocols before normalization", ErrCompositeRule) return FirewallRule{}, fmt.Errorf("%w: expand addresses, ports and protocols before normalization", ErrCompositeRule)
} }
@@ -37,11 +37,11 @@ func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
if err != nil { if err != nil {
return FirewallRule{}, fmt.Errorf("%w: destination address: %v", ErrInvalidRule, err) return FirewallRule{}, fmt.Errorf("%w: destination address: %v", ErrInvalidRule, err)
} }
rule.SourcePort, err = normalizePort(rule.SourcePort) rule.SourcePort, err = normalizePortValue(rule.SourcePort, false)
if err != nil { if err != nil {
return FirewallRule{}, fmt.Errorf("%w: source port: %v", ErrInvalidRule, err) return FirewallRule{}, fmt.Errorf("%w: source port: %v", ErrInvalidRule, err)
} }
rule.DestinationPort, err = normalizePortValue(rule.DestinationPort, supportsNativeDestinationPortSet(rule.Scope.Provider)) rule.DestinationPort, err = normalizePortValue(rule.DestinationPort, rule.Scope.Provider == ProviderIptables || rule.Scope.Provider == ProviderUFW)
if err != nil { if err != nil {
return FirewallRule{}, fmt.Errorf("%w: destination port: %v", ErrInvalidRule, err) return FirewallRule{}, fmt.Errorf("%w: destination port: %v", ErrInvalidRule, err)
} }
@@ -136,7 +136,7 @@ func ExpandAtomicRules(input FirewallRule) ([]FirewallRule, error) {
destinationAddresses := splitValues(input.DestinationAddress) destinationAddresses := splitValues(input.DestinationAddress)
sourcePorts := splitValues(input.SourcePort) sourcePorts := splitValues(input.SourcePort)
destinationPorts := splitValues(input.DestinationPort) destinationPorts := splitValues(input.DestinationPort)
if supportsNativeDestinationPortSet(input.Scope.Provider) { if input.Scope.Provider == ProviderIptables || input.Scope.Provider == ProviderUFW {
destinationPorts = []string{input.DestinationPort} destinationPorts = []string{input.DestinationPort}
} }
@@ -272,10 +272,6 @@ func validateAddressFamily(address netip.Addr, family Family) error {
return nil return nil
} }
func normalizePort(value string) (string, error) {
return normalizePortValue(value, false)
}
func normalizePortValue(value string, allowSet bool) (string, error) { func normalizePortValue(value string, allowSet bool) (string, error) {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" || strings.EqualFold(value, "any") || strings.EqualFold(value, "anywhere") { if value == "" || strings.EqualFold(value, "any") || strings.EqualFold(value, "anywhere") {
@@ -348,10 +344,6 @@ func normalizePortValue(value string, allowSet bool) (string, error) {
return fmt.Sprintf("%d-%d", start, end), nil return fmt.Sprintf("%d-%d", start, end), nil
} }
func supportsNativeDestinationPortSet(provider Provider) bool {
return provider == ProviderIptables || provider == ProviderUFW
}
func parsePort(value string) (int, error) { func parsePort(value string) (int, error) {
port, err := strconv.Atoi(strings.TrimSpace(value)) port, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || port < 1 || port > 65535 { if err != nil || port < 1 || port > 65535 {
@@ -404,10 +396,6 @@ func normalizeConnectionStates(values []string, provider Provider) ([]string, er
return states, nil return states, nil
} }
func hasCompositeValue(value string) bool {
return strings.Contains(value, ",")
}
func isCompositeProtocol(value string) bool { func isCompositeProtocol(value string) bool {
value = strings.ToLower(strings.TrimSpace(value)) value = strings.ToLower(strings.TrimSpace(value))
return value == "tcp/udp" || value == "udp/tcp" || strings.Contains(value, ",") return value == "tcp/udp" || value == "udp/tcp" || strings.Contains(value, ",")
-25
View File
@@ -1,25 +0,0 @@
package filter
const ObservedFieldProtocol = "protocol"
// ObservedRuleMatchesExpected compares the semantic fields that the backend
// could actually observe. Partial rules name fields omitted by their native
// listing; those fields are supplied from the expected rule before comparing
// normalized identities.
func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) bool {
if observed.ParseStatus == ParseStatusOpaque {
return false
}
hydrated := observed.Rule
for _, field := range observed.UncertainFields {
switch field {
case ObservedFieldProtocol:
hydrated.Protocol = expected.Protocol
default:
return false
}
}
gotKey, gotErr := RuleKey(hydrated)
wantKey, wantErr := RuleKey(expected)
return gotErr == nil && wantErr == nil && gotKey == wantKey
}
@@ -15,6 +15,8 @@ import (
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
) )
var ErrAlreadyEnabled = errors.New("firewalld rule already exists")
type CommandReader interface { type CommandReader interface {
Read(context.Context, ...string) (string, error) Read(context.Context, ...string) (string, error)
} }
@@ -114,17 +116,17 @@ func parseFirewalldVersion(output string) (int, int, error) {
return major, minor, nil return major, minor, nil
} }
func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) { func (a *Adapter) ListRules(ctx context.Context, scope filter.Scope) (filter.RuleSet, error) {
scope = scope.Normalize() scope = scope.Normalize()
if err := scope.ValidateMVP(); err != nil { if err := scope.ValidateMVP(); err != nil {
return filter.Snapshot{}, err return filter.RuleSet{}, err
} }
if scope.Provider != filter.ProviderFirewalld { if scope.Provider != filter.ProviderFirewalld {
return filter.Snapshot{}, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, scope.Key()) return filter.RuleSet{}, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, scope.Key())
} }
scope.Family = filter.FamilyInet scope.Family = filter.FamilyInet
if a.reader == nil { if a.reader == nil {
return filter.Snapshot{}, errors.New("firewalld reader is required") return filter.RuleSet{}, errors.New("firewalld reader is required")
} }
var runtime, permanent zoneOutput var runtime, permanent zoneOutput
@@ -141,15 +143,15 @@ func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snaps
}() }()
reads.Wait() reads.Wait()
if err := errors.Join(runtimeErr, permanentErr); err != nil { if err := errors.Join(runtimeErr, permanentErr); err != nil {
return filter.Snapshot{}, err return filter.RuleSet{}, err
} }
rules, err := mergeZoneObjects(scope, runtime, permanent) rules, err := mergeZoneObjects(scope, runtime, permanent)
if err != nil { if err != nil {
return filter.Snapshot{}, err return filter.RuleSet{}, err
} }
snapshot, err := filter.NewSnapshot(scope, rules) snapshot, err := filter.NewRuleSet(scope, rules)
if err != nil { if err != nil {
return filter.Snapshot{}, err return filter.RuleSet{}, err
} }
snapshot.Notices = publicZoneNotices(runtime, permanent) snapshot.Notices = publicZoneNotices(runtime, permanent)
return snapshot, nil return snapshot, nil
@@ -186,135 +188,144 @@ func (a *Adapter) PrepareRule(rule filter.FirewallRule) (filter.FirewallRule, er
return normalized, nil return normalized, nil
} }
func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) { func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
if snapshot.Revision == "" {
return filter.BackendPlan{}, filter.ErrRuleStale
}
if err := validateFirewalldScope(snapshot.Scope); err != nil { if err := validateFirewalldScope(snapshot.Scope); err != nil {
return filter.BackendPlan{}, err return filter.CommandBatch{}, err
} }
if len(changes) != 1 { if len(changes) == 0 || len(changes) > filter.MaxAtomicExpansion {
return filter.BackendPlan{}, fmt.Errorf("%w: firewalld plans currently require exactly one change", filter.ErrInvalidRule) return filter.CommandBatch{}, fmt.Errorf("%w: invalid firewalld batch size", filter.ErrInvalidRule)
} }
rulePlan, err := a.compileChange(snapshot, changes[0]) plan := filter.CommandBatch{Provider: filter.ProviderFirewalld, Scope: snapshot.Scope}
if err != nil { for _, change := range changes {
return filter.BackendPlan{}, err rulePlan, err := a.compileChange(snapshot, change)
if err != nil {
return filter.CommandBatch{}, err
}
plan.Rules = append(plan.Rules, rulePlan)
} }
return filter.BackendPlan{ return plan, nil
Provider: filter.ProviderFirewalld, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision,
Rules: []filter.NativeRulePlan{rulePlan},
}, nil
} }
func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.ApplyResult, error) { func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) error {
if plan.Provider != filter.ProviderFirewalld || len(plan.Rules) != 1 { commands, err := batchCommands(plan)
return filter.ApplyResult{}, fmt.Errorf("%w: invalid firewalld backend plan", filter.ErrInvalidRule) if err != nil {
return err
} }
if err := validateFirewalldScope(plan.Scope); err != nil { if len(commands.Commands) != 0 && a.writer == nil {
return filter.ApplyResult{}, err return errors.New("firewalld writer is required")
} }
rulePlan := plan.Rules[0] executed, alreadyEnabled := 0, 0
if len(rulePlan.Commands) != 0 && a.writer == nil { for index, command := range commands.Commands {
return filter.ApplyResult{}, errors.New("firewalld writer is required") err := a.writer.Run(ctx, command)
} if errors.Is(err, ErrAlreadyEnabled) {
if len(rulePlan.Commands) != len(rulePlan.RollbackCommands) { alreadyEnabled++
return filter.ApplyResult{}, fmt.Errorf("%w: incomplete firewalld rollback plan", filter.ErrInvalidRule) err = nil
}
for _, command := range append(append([]filter.NativeCommand(nil), rulePlan.Commands...), rulePlan.RollbackCommands...) {
if err := validateScopeCommand(plan.Scope, command); err != nil {
return filter.ApplyResult{}, err
} }
} if err != nil {
executed := 0
for index, command := range rulePlan.Commands {
if err := a.writer.Run(ctx, command); err != nil {
if plan.CommandOnly { if plan.CommandOnly {
return filter.ApplyResult{}, err return err
} }
return filter.ApplyResult{}, a.compensate(ctx, rulePlan, executed, err) return a.compensate(ctx, commands, executed, err)
} }
executed = index + 1 executed = index + 1
} }
return filter.ApplyResult{Applied: []filter.ObservedRule{rulePlan.Expected}}, nil if commands.Operation == filter.ChangeCreate && alreadyEnabled > 0 && alreadyEnabled == len(commands.Commands) {
return ErrAlreadyEnabled
}
return nil
} }
func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, error) { func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
if plan.Provider != filter.ProviderFirewalld || len(plan.Rules) != 1 { commands, err := batchCommands(plan)
return filter.VerifyResult{}, fmt.Errorf("%w: invalid firewalld backend plan", filter.ErrInvalidRule)
}
snapshot, err := a.Observe(ctx, plan.Scope)
if err != nil { if err != nil {
return filter.VerifyResult{}, err
}
rulePlan := plan.Rules[0]
if rulePlan.Operation == filter.ChangeDelete {
return filter.VerifyResult{Snapshot: snapshot, Matched: countCanonical(snapshot, rulePlan.Previous) == 0}, nil
}
if rulePlan.Operation == filter.ChangeUpdate && rulePlan.Previous != nil &&
rulePlan.Previous.Locator.Canonical != rulePlan.Expected.Locator.Canonical && countCanonical(snapshot, rulePlan.Previous) != 0 {
return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil
}
matches := 0
for _, observed := range snapshot.Rules {
if observed.Locator.Canonical != rulePlan.Expected.Locator.Canonical || observed.Persistence != filter.PersistenceStatusConverged {
continue
}
want, wantErr := filter.RuleKey(rulePlan.Expected.Rule)
got, gotErr := filter.RuleKey(observed.Rule)
if wantErr == nil && gotErr == nil && want == got {
matches++
}
}
return filter.VerifyResult{Snapshot: snapshot, Matched: matches == 1}, nil
}
func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
if plan.Provider != filter.ProviderFirewalld {
return fmt.Errorf("%w: invalid firewalld backend plan", filter.ErrInvalidRule)
}
if err := validateFirewalldScope(plan.Scope); err != nil {
return err return err
} }
if a.writer == nil { if a.writer == nil {
return errors.New("firewalld writer is required") return errors.New("firewalld writer is required")
} }
for ruleIndex := len(plan.Rules) - 1; ruleIndex >= 0; ruleIndex-- { return a.rollback(ctx, plan.Scope, commands, len(commands.RollbackCommands))
rulePlan := plan.Rules[ruleIndex]
if err := a.rollback(ctx, plan.Scope, rulePlan, len(rulePlan.RollbackCommands)); err != nil {
return err
}
}
return nil
} }
func (a *Adapter) compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filter.NativeRulePlan, error) { func batchCommands(plan filter.CommandBatch) (filter.RuleCommands, error) {
if plan.Provider != filter.ProviderFirewalld || len(plan.Rules) == 0 || len(plan.Rules) > filter.MaxAtomicExpansion {
return filter.RuleCommands{}, fmt.Errorf("%w: invalid firewalld backend plan", filter.ErrInvalidRule)
}
if err := validateFirewalldScope(plan.Scope); err != nil {
return filter.RuleCommands{}, err
}
result := filter.RuleCommands{Expected: filter.ObservedRule{Rule: filter.FirewallRule{Scope: plan.Scope}}}
if plan.CreatesOnly() {
result.Operation = filter.ChangeCreate
}
for _, rule := range plan.Rules {
if len(rule.Commands) != len(rule.RollbackCommands) {
return filter.RuleCommands{}, fmt.Errorf("%w: incomplete firewalld rollback plan", filter.ErrInvalidRule)
}
for _, commands := range [][]filter.NativeCommand{rule.Commands, rule.RollbackCommands} {
for _, command := range commands {
if len(command.Args) != 2 || command.Args[0] != "--zone="+filter.FirewalldInputZone {
return filter.RuleCommands{}, fmt.Errorf("%w: expected one firewalld rule option", filter.ErrInvalidRule)
}
if err := validateScopeCommand(plan.Scope, command); err != nil {
return filter.RuleCommands{}, err
}
}
}
}
for _, permanent := range []bool{false, true} {
previousOperation, commandBytes := "", 0
for _, rule := range plan.Rules {
for index, command := range rule.Commands {
option := command.Args[len(command.Args)-1]
rollback := rule.RollbackCommands[index].Args[len(rule.RollbackCommands[index].Args)-1]
operation, _, _ := strings.Cut(option, "=")
if operation != previousOperation || commandBytes+max(len(option), len(rollback))+1 > 64*1024 {
args := []string{"--zone=" + filter.FirewalldInputZone}
if permanent {
args = append(args, "--permanent")
}
result.Commands = append(result.Commands, filter.NativeCommand{Executable: "firewall-cmd", Args: append([]string(nil), args...)})
result.RollbackCommands = append(result.RollbackCommands, filter.NativeCommand{Executable: "firewall-cmd", Args: append([]string(nil), args...)})
previousOperation, commandBytes = operation, 0
}
last := len(result.Commands) - 1
result.Commands[last].Args = append(result.Commands[last].Args, option)
result.RollbackCommands[last].Args = append(result.RollbackCommands[last].Args, rollback)
commandBytes += max(len(option), len(rollback)) + 1
}
}
}
return result, nil
}
func (a *Adapter) compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) {
rule := change.After rule := change.After
if change.Operation == filter.ChangeDelete { if change.Operation == filter.ChangeDelete {
rule = change.Before rule = change.Before
} }
if rule == nil { if rule == nil {
return filter.NativeRulePlan{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation)
} }
normalized, err := a.PrepareRule(*rule) normalized, err := a.PrepareRule(*rule)
if err != nil { if err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
if normalized.Scope.Key() != snapshot.Scope.Key() { if normalized.Scope.Key() != snapshot.Scope.Key() {
return filter.NativeRulePlan{}, fmt.Errorf("%w: change scope %s", filter.ErrUnsupportedScope, normalized.Scope.Key()) return filter.RuleCommands{}, fmt.Errorf("%w: change scope %s", filter.ErrUnsupportedScope, normalized.Scope.Key())
} }
if normalized.UUID == "" { if normalized.UUID == "" {
return filter.NativeRulePlan{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule) return filter.RuleCommands{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule)
} }
expected := observedForRule(normalized) expected := observedForRule(normalized)
plan := filter.NativeRulePlan{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected} plan := filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected}
switch change.Operation { switch change.Operation {
case filter.ChangeCreate: case filter.ChangeCreate:
plan.Commands, plan.RollbackCommands = missingRuleCommands(snapshot, normalized) plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove"))
case filter.ChangeAdopt: case filter.ChangeAdopt:
target, targetErr := validateMutationTarget(snapshot, change, normalized, false) target, targetErr := validateMutationTarget(snapshot, change, normalized, false)
if targetErr != nil { if targetErr != nil {
return filter.NativeRulePlan{}, targetErr return filter.RuleCommands{}, targetErr
} }
plan.Previous = &target plan.Previous = &target
plan.Expected = target plan.Expected = target
@@ -322,35 +333,31 @@ func (a *Adapter) compileChange(snapshot filter.Snapshot, change filter.DesiredC
case filter.ChangeUpdate: case filter.ChangeUpdate:
target, targetErr := validateMutationTarget(snapshot, change, normalized, true) target, targetErr := validateMutationTarget(snapshot, change, normalized, true)
if targetErr != nil { if targetErr != nil {
return filter.NativeRulePlan{}, targetErr return filter.RuleCommands{}, targetErr
} }
plan.Previous = &target plan.Previous = &target
if target.Locator.Canonical == expected.Locator.Canonical { if target.Locator.Canonical == expected.Locator.Canonical {
break break
} }
removeCommands, restoreCommands := observedPairedCommands(target, "remove", "add") removeCommands, restoreCommands := observedRuleCommands(target, "remove", "add")
addCommands, removeNewCommands := missingRuleCommands(snapshot, normalized) addCommands, removeNewCommands := ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove"))
plan.Commands = append(removeCommands, addCommands...) plan.Commands = append(removeCommands, addCommands...)
plan.RollbackCommands = append(restoreCommands, removeNewCommands...) plan.RollbackCommands = append(restoreCommands, removeNewCommands...)
case filter.ChangeDelete: 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) target, targetErr := validateMutationTarget(snapshot, change, normalized, true)
if targetErr != nil { if targetErr != nil {
return filter.NativeRulePlan{}, targetErr return filter.RuleCommands{}, targetErr
} }
plan.Previous = &target plan.Previous = &target
plan.Expected = target plan.Expected = target
plan.Expected.Rule.UUID = normalized.UUID plan.Expected.Rule.UUID = normalized.UUID
plan.Commands, plan.RollbackCommands = observedPairedCommands(target, "remove", "add") plan.Commands, plan.RollbackCommands = observedRuleCommands(target, "remove", "add")
if change.CommandOnly {
switch target.Persistence {
case filter.PersistenceStatusRuntimeOnly:
plan.Commands, plan.RollbackCommands = plan.Commands[:1], plan.RollbackCommands[:1]
case filter.PersistenceStatusPermanentOnly:
plan.Commands, plan.RollbackCommands = plan.Commands[1:], plan.RollbackCommands[1:]
}
}
default: default:
return filter.NativeRulePlan{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation)
} }
return plan, nil return plan, nil
} }
@@ -407,48 +414,22 @@ func nativeCanonical(rule filter.FirewallRule) string {
return "rich:" + canonicalRichRule(rule) return "rich:" + canonicalRichRule(rule)
} }
func missingRuleCommands(snapshot filter.Snapshot, rule filter.FirewallRule) ([]filter.NativeCommand, []filter.NativeCommand) { func observedRuleCommands(observed filter.ObservedRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) {
commands, rollback := pairedCommands(rule, "add", "remove")
var runtimeExists, permanentExists bool
for _, observed := range snapshot.Rules {
if observed.Locator.Canonical != nativeCanonical(rule) {
continue
}
runtimeExists = runtimeExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusRuntimeOnly
permanentExists = permanentExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusPermanentOnly
}
var changes, inverses []filter.NativeCommand
for index, exists := range []bool{runtimeExists, permanentExists} {
if !exists {
changes = append(changes, commands[index])
inverses = append(inverses, rollback[index])
}
}
return changes, inverses
}
func pairedCommands(rule filter.FirewallRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) {
return pairedNativeCommands(rule.Scope, nativeOption(rule, operation), nativeOption(rule, inverse))
}
func observedPairedCommands(observed filter.ObservedRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) {
if observed.Rule.NativeKind != filter.NativeKindRichRule || observed.Raw == "" { if observed.Rule.NativeKind != filter.NativeKindRichRule || observed.Raw == "" {
return pairedCommands(observed.Rule, operation, inverse) return ruleCommands(nativeOption(observed.Rule, operation), nativeOption(observed.Rule, inverse))
} }
return pairedNativeCommands(observed.Rule.Scope, return ruleCommands(
"--"+operation+"-rich-rule="+observed.Raw, "--"+operation+"-rich-rule="+observed.Raw,
"--"+inverse+"-rich-rule="+observed.Raw) "--"+inverse+"-rich-rule="+observed.Raw)
} }
func pairedNativeCommands(scope filter.Scope, option, rollback string) ([]filter.NativeCommand, []filter.NativeCommand) { func ruleCommands(option, rollback string) ([]filter.NativeCommand, []filter.NativeCommand) {
selector := scopeSelector(scope) selector := "--zone=" + filter.FirewalldInputZone
commands := []filter.NativeCommand{ commands := []filter.NativeCommand{
{Executable: "firewall-cmd", Args: []string{selector, option}}, {Executable: "firewall-cmd", Args: []string{selector, option}},
{Executable: "firewall-cmd", Args: []string{"--permanent", selector, option}},
} }
rollbackCommands := []filter.NativeCommand{ rollbackCommands := []filter.NativeCommand{
{Executable: "firewall-cmd", Args: []string{selector, rollback}}, {Executable: "firewall-cmd", Args: []string{selector, rollback}},
{Executable: "firewall-cmd", Args: []string{"--permanent", selector, rollback}},
} }
return commands, rollbackCommands return commands, rollbackCommands
} }
@@ -460,7 +441,7 @@ func nativeOption(rule filter.FirewallRule, operation string) string {
return "--" + operation + "-rich-rule=" + canonicalRichRule(rule) return "--" + operation + "-rich-rule=" + canonicalRichRule(rule)
} }
func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChange, normalized filter.FirewallRule, requireOwned bool) (filter.ObservedRule, error) { func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, normalized filter.FirewallRule, requireOwned bool) (filter.ObservedRule, error) {
if change.Locator == nil || change.Locator.Canonical == "" { if change.Locator == nil || change.Locator.Canonical == "" {
return filter.ObservedRule{}, fmt.Errorf("%w: firewalld mutation requires canonical locator", filter.ErrInvalidRule) return filter.ObservedRule{}, fmt.Errorf("%w: firewalld mutation requires canonical locator", filter.ErrInvalidRule)
} }
@@ -483,8 +464,7 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
if target.Protected { if target.Protected {
return filter.ObservedRule{}, filter.ErrProtectedRule return filter.ObservedRule{}, filter.ErrProtectedRule
} }
if target.ParseStatus != filter.ParseStatusSupported || if target.ParseStatus != filter.ParseStatusSupported {
(target.Persistence != filter.PersistenceStatusConverged && !(change.CommandOnly && change.Operation == filter.ChangeDelete)) {
return filter.ObservedRule{}, filter.ErrRuleStale return filter.ObservedRule{}, filter.ErrRuleStale
} }
want := normalized want := normalized
@@ -507,30 +487,37 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
} }
func validateScopeCommand(scope filter.Scope, command filter.NativeCommand) error { func validateScopeCommand(scope filter.Scope, command filter.NativeCommand) error {
if command.Executable != "firewall-cmd" { if err := validateFirewalldScope(scope); err != nil {
return fmt.Errorf("%w: unexpected firewalld executable %q", filter.ErrInvalidRule, command.Executable) return err
} }
expected := scopeSelector(scope) if command.Executable != "firewall-cmd" || command.Stdin != "" {
foundSelector := false return fmt.Errorf("%w: invalid firewalld command", filter.ErrInvalidRule)
}
expected := "--zone=" + filter.FirewalldInputZone
foundSelector, options := false, 0
for _, arg := range command.Args { for _, arg := range command.Args {
if arg == expected { switch {
case arg == expected:
foundSelector = true foundSelector = true
} case arg == "--permanent":
if (strings.HasPrefix(arg, "--zone=") || strings.HasPrefix(arg, "--policy=")) && arg != expected { case strings.HasPrefix(arg, "--add-port="), strings.HasPrefix(arg, "--remove-port="),
return fmt.Errorf("%w: firewalld command targets another scope", filter.ErrUnsupportedScope) strings.HasPrefix(arg, "--add-rich-rule="), strings.HasPrefix(arg, "--remove-rich-rule="),
strings.HasPrefix(arg, "--add-service="), strings.HasPrefix(arg, "--remove-service="):
options++
default:
return fmt.Errorf("%w: unexpected firewalld argument %q", filter.ErrInvalidRule, arg)
} }
} }
if !foundSelector { if !foundSelector {
return fmt.Errorf("%w: firewalld command must explicitly target %s", filter.ErrUnsupportedScope, expected) return fmt.Errorf("%w: firewalld command must explicitly target %s", filter.ErrUnsupportedScope, expected)
} }
if options == 0 {
return fmt.Errorf("%w: firewalld command requires rule options", filter.ErrInvalidRule)
}
return nil return nil
} }
func scopeSelector(scope filter.Scope) string { func (a *Adapter) compensate(ctx context.Context, plan filter.RuleCommands, executed int, cause error) error {
return "--zone=" + filter.FirewalldInputZone
}
func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, executed int, cause error) error {
if plan.Operation == filter.ChangeCreate { if plan.Operation == filter.ChangeCreate {
return cause return cause
} }
@@ -543,7 +530,7 @@ func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, ex
return cause return cause
} }
func (a *Adapter) rollback(ctx context.Context, scope filter.Scope, plan filter.NativeRulePlan, executed int) error { func (a *Adapter) rollback(ctx context.Context, scope filter.Scope, plan filter.RuleCommands, executed int) error {
var rollbackErr error var rollbackErr error
for index := executed - 1; index >= 0; index-- { for index := executed - 1; index >= 0; index-- {
if index >= len(plan.RollbackCommands) { if index >= len(plan.RollbackCommands) {
@@ -559,26 +546,13 @@ func (a *Adapter) rollback(ctx context.Context, scope filter.Scope, plan filter.
} }
continue continue
} }
if err := a.writer.Run(ctx, command); err != nil && rollbackErr == nil { if err := a.writer.Run(ctx, command); err != nil && !errors.Is(err, ErrAlreadyEnabled) && rollbackErr == nil {
rollbackErr = err rollbackErr = err
} }
} }
return rollbackErr return rollbackErr
} }
func countCanonical(snapshot filter.Snapshot, expected *filter.ObservedRule) int {
if expected == nil {
return 0
}
count := 0
for _, observed := range snapshot.Rules {
if observed.Locator.Canonical == expected.Locator.Canonical {
count++
}
}
return count
}
type zoneOutput struct { type zoneOutput struct {
ports string ports string
rich string rich string
@@ -590,7 +564,7 @@ func (a *Adapter) readScope(ctx context.Context, scope filter.Scope, permanent b
if permanent { if permanent {
args = append(args, "--permanent") args = append(args, "--permanent")
} }
args = append(args, scopeSelector(scope), "--list-all") args = append(args, "--zone="+filter.FirewalldInputZone, "--list-all")
output, err := a.reader.Read(ctx, args...) output, err := a.reader.Read(ctx, args...)
if err != nil { if err != nil {
return zoneOutput{}, err return zoneOutput{}, err
@@ -1075,9 +1049,46 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
if err := validateSystemCommand(command); err != nil { if err := validateSystemCommand(command); err != nil {
return err return err
} }
return cmd.NewCommandMgr( options, removals, alreadyEnabled := 0, 0, 0
cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"), for _, arg := range command.Args {
if strings.HasPrefix(arg, "--add-") || strings.HasPrefix(arg, "--remove-") {
options++
}
if strings.HasPrefix(arg, "--remove-") {
removals++
}
}
timeout := 60 * time.Second
if options > 1 {
timeout = 15 * time.Minute
}
var stderr strings.Builder
err := cmd.NewCommandMgr(
cmd.WithContext(ctx), cmd.WithTimeout(timeout), cmd.WithEnv("LC_ALL=C", "LANGUAGE=en_US:en"), cmd.WithStderr(&stderr),
).RunWithOptionalSudo(command.Executable, command.Args...) ).RunWithOptionalSudo(command.Executable, command.Args...)
if err != nil {
return err
}
for _, line := range strings.Split(stderr.String(), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
switch {
case strings.HasPrefix(line, "Warning: ALREADY_ENABLED:"):
alreadyEnabled++
case strings.HasPrefix(line, "Warning: NOT_ENABLED:"):
if removals != options {
return fmt.Errorf("%w: %s", filter.ErrRuleStale, stderr.String())
}
default:
return fmt.Errorf("firewalld rule batch failed: %s", stderr.String())
}
}
if alreadyEnabled > 0 && alreadyEnabled == options {
return ErrAlreadyEnabled
}
return nil
} }
func validateSystemCommand(command filter.NativeCommand) error { func validateSystemCommand(command filter.NativeCommand) error {
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -17,140 +16,187 @@ import (
) )
type RuleReader interface { type RuleReader interface {
filter.CommentRuleReader
ListChain(context.Context, filter.Scope) (string, error) ListChain(context.Context, filter.Scope) (string, error)
} }
type TableReader interface {
ListTable(context.Context, filter.Scope) (string, error)
}
type RuleWriter interface { type RuleWriter interface {
Run(context.Context, filter.NativeCommand) error Run(context.Context, filter.NativeCommand) error
Save(context.Context, filter.Scope) error Save(context.Context, filter.Scope) error
} }
type MultiportChecker interface {
CheckMultiport(context.Context, filter.Family) error
}
type Adapter struct { type Adapter struct {
reader RuleReader reader RuleReader
writer RuleWriter writer RuleWriter
checker MultiportChecker
multiportMu sync.Mutex
multiportOK map[filter.Family]bool
} }
func NewAdapter() *Adapter { func NewAdapter() *Adapter {
backend := systemBackend{} backend := systemBackend{}
return &Adapter{reader: backend, writer: backend, checker: backend} return &Adapter{reader: backend, writer: backend}
} }
func NewAdapterWithReader(reader RuleReader) *Adapter { func NewAdapterWithReader(reader RuleReader) *Adapter {
adapter := &Adapter{reader: reader} return &Adapter{reader: reader}
adapter.checker, _ = reader.(MultiportChecker)
return adapter
} }
func NewAdapterWithBackend(reader RuleReader, writer RuleWriter) *Adapter { func NewAdapterWithBackend(reader RuleReader, writer RuleWriter) *Adapter {
adapter := &Adapter{reader: reader, writer: writer} return &Adapter{reader: reader, writer: writer}
if checker, ok := reader.(MultiportChecker); ok {
adapter.checker = checker
} else if checker, ok := writer.(MultiportChecker); ok {
adapter.checker = checker
}
return adapter
} }
func (a *Adapter) Provider() filter.Provider { return filter.ProviderIptables } func (a *Adapter) Provider() filter.Provider { return filter.ProviderIptables }
func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error {
if !strings.Contains(rule.DestinationPort, ",") && !strings.Contains(rule.SourcePort, ",") {
return nil
}
if rule.Protocol != "tcp" && rule.Protocol != "udp" {
return fmt.Errorf("%w: iptables multiport requires tcp or udp", filter.ErrInvalidRule)
}
if a.checker == nil {
return nil
}
a.multiportMu.Lock()
defer a.multiportMu.Unlock()
if a.multiportOK[rule.Scope.Family] {
return nil
}
if err := a.checker.CheckMultiport(ctx, rule.Scope.Family); err != nil {
return fmt.Errorf("inspect iptables multiport for %s: %w", rule.Scope.Family, err)
}
if a.multiportOK == nil {
a.multiportOK = make(map[filter.Family]bool, 2)
}
a.multiportOK[rule.Scope.Family] = true
return nil
}
func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) { func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
return filter.Capabilities{ return filter.Capabilities{
Marker: true, OwnedChains: true, ExplicitPosition: true, Marker: true, OwnedChains: true, ExplicitPosition: true,
}, nil }, nil
} }
func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) { func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
scope = scope.Normalize() rule, err := filter.NormalizeRule(rule)
if err := scope.ValidateMVP(); err != nil { if err != nil {
return filter.Snapshot{}, err return err
} }
if scope.Provider != filter.ProviderIptables { if err := validateAdapterScope(rule.Scope); err != nil {
return filter.Snapshot{}, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, scope.Key()) return err
}
args := []string{"-w", "-t", rule.Scope.Table, "-A", rule.Scope.Chain}
args = append(args, compileRuleArgs(rule, comment)...)
return a.writer.Run(ctx, filter.NativeCommand{Executable: executableForFamily(rule.Scope.Family), Args: args})
}
func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, comment string) ([]filter.ObservedRule, error) {
var rules []filter.ObservedRule
for _, scope := range scopes {
scope = scope.Normalize()
if err := validateAdapterScope(scope); err != nil {
return nil, err
}
output, err := a.reader.ReadRulesByComment(ctx, scope, comment)
if err != nil {
return nil, err
}
rules = append(rules, parseChainRules(scope, output)...)
}
return rules, nil
}
func (a *Adapter) ListRules(ctx context.Context, scope filter.Scope) (filter.RuleSet, error) {
snapshots, err := a.ListRuleScopes(ctx, []filter.Scope{scope})
if err != nil {
return filter.RuleSet{}, err
}
return snapshots[0], nil
}
func (a *Adapter) ListRuleScopes(ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
normalized := make([]filter.Scope, len(scopes))
for index, scope := range scopes {
scope = scope.Normalize()
if err := validateAdapterScope(scope); err != nil {
return nil, err
}
normalized[index] = scope
} }
if a.reader == nil { if a.reader == nil {
return filter.Snapshot{}, fmt.Errorf("iptables reader is required") return nil, fmt.Errorf("iptables reader is required")
} }
output, err := a.reader.ListChain(ctx, scope) tableReader, readsTable := a.reader.(TableReader)
if err != nil { snapshots := make([]filter.RuleSet, len(scopes))
if errors.Is(err, filter.ErrProviderUnavailable) { for index, scope := range normalized {
snapshot, snapshotErr := filter.NewSnapshot(scope, nil) if snapshots[index].Scope.Provider != "" {
if snapshotErr != nil { continue
return filter.Snapshot{}, snapshotErr }
} var output string
snapshot.Notices = []filter.ScopeNotice{{ var err error
Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(scope.Family), scope.Chain}, if readsTable {
}} output, err = tableReader.ListTable(ctx, scope)
return snapshot, nil } else {
output, err = a.reader.ListChain(ctx, scope)
}
if err != nil && (readsTable || !errors.Is(err, filter.ErrProviderUnavailable)) {
return nil, err
}
for target := index; target < len(normalized); target++ {
current := normalized[target]
if readsTable {
if current.Family != scope.Family || current.Table != scope.Table {
continue
}
} else if target != index {
continue
}
missing := errors.Is(err, filter.ErrProviderUnavailable) || readsTable && !containsChainDeclaration(output, current.Chain)
var rules []filter.ObservedRule
if !missing {
rules = parseChainRules(current, output)
}
snapshot, buildErr := filter.NewRuleSet(current, rules)
if buildErr != nil {
return nil, buildErr
}
if missing {
snapshot.Notices = []filter.ScopeNotice{{Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(current.Family), current.Chain}}}
}
snapshots[target] = snapshot
} }
return filter.Snapshot{}, err
} }
rules := parseChainRules(scope, output) return snapshots, nil
return filter.NewSnapshot(scope, rules)
} }
func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) { func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
if snapshot.Revision == "" {
return filter.BackendPlan{}, filter.ErrRuleStale
}
snapshot.Scope = snapshot.Scope.Normalize() snapshot.Scope = snapshot.Scope.Normalize()
if err := validateAdapterScope(snapshot.Scope); err != nil { if err := validateAdapterScope(snapshot.Scope); err != nil {
return filter.BackendPlan{}, err return filter.CommandBatch{}, err
} }
if len(changes) == 0 { if len(changes) == 0 {
return filter.BackendPlan{}, fmt.Errorf("%w: iptables plan requires at least one change", filter.ErrInvalidRule) return filter.CommandBatch{}, fmt.Errorf("%w: iptables plan requires at least one change", filter.ErrInvalidRule)
} }
return compileBatch(snapshot, changes) createOnly, deleteOnly := true, true
for _, change := range changes {
createOnly = createOnly && change.Operation == filter.ChangeCreate && change.CommandOnly
deleteOnly = deleteOnly && change.Operation == filter.ChangeDelete && change.CommandOnly
}
if createOnly {
return compileCreateBatch(snapshot, changes)
}
externalDelete := len(changes) == 1 && changes[0].Locator == nil && (changes[0].UnmarkedAdopted || changes[0].PreviousMarker != "")
if deleteOnly && !externalDelete {
return compileDeleteBatch(snapshot, changes)
}
if len(changes) != 1 {
return filter.CommandBatch{}, fmt.Errorf("%w: iptables mutation requires exactly one change", filter.ErrInvalidRule)
}
rulePlan, err := compileChange(snapshot, changes[0])
if err != nil {
return filter.CommandBatch{}, err
}
return filter.CommandBatch{
Provider: filter.ProviderIptables, Scope: snapshot.Scope, CommandOnly: changes[0].CommandOnly,
Rules: []filter.RuleCommands{rulePlan},
}, nil
} }
func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.ApplyResult, error) { func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) error {
if a.writer == nil { if a.writer == nil {
return filter.ApplyResult{}, fmt.Errorf("iptables writer is required") return fmt.Errorf("iptables writer is required")
} }
if plan.Provider != filter.ProviderIptables { if plan.Provider != filter.ProviderIptables {
return filter.ApplyResult{}, fmt.Errorf("%w: backend plan provider %q", filter.ErrUnsupportedScope, plan.Provider) return fmt.Errorf("%w: backend plan provider %q", filter.ErrUnsupportedScope, plan.Provider)
} }
if err := validateAdapterScope(plan.Scope); err != nil { if err := validateAdapterScope(plan.Scope); err != nil {
return filter.ApplyResult{}, err return err
} }
if len(plan.Rules) == 0 { if len(plan.Rules) == 0 {
return filter.ApplyResult{}, fmt.Errorf("%w: iptables plan requires at least one rule", filter.ErrInvalidRule) return fmt.Errorf("%w: iptables plan requires at least one rule", filter.ErrInvalidRule)
} }
for _, rulePlan := range plan.Rules { for _, rulePlan := range plan.Rules {
for _, command := range rulePlan.Commands { for _, command := range rulePlan.Commands {
if err := validateNativeCommand(plan.Scope, command); err != nil { if err := validateNativeCommand(plan.Scope, command); err != nil {
return filter.ApplyResult{}, err return err
} }
} }
} }
@@ -159,134 +205,69 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
for _, command := range rulePlan.Commands { for _, command := range rulePlan.Commands {
if err := a.writer.Run(ctx, command); err != nil { if err := a.writer.Run(ctx, command); err != nil {
if plan.CommandOnly { if plan.CommandOnly {
return filter.ApplyResult{}, err return err
} }
return filter.ApplyResult{}, a.compensate(ctx, plan, ruleIndex, executed, err) return a.compensate(ctx, plan, ruleIndex, executed, err)
} }
executed++ executed++
} }
} }
if err := a.writer.Save(ctx, plan.Scope); err != nil {
if plan.CommandOnly { return nil
return filter.ApplyResult{}, err
}
return filter.ApplyResult{}, a.compensate(ctx, plan, len(plan.Rules)-1, -1, err)
}
applied := make([]filter.ObservedRule, 0, len(plan.Rules))
for _, rulePlan := range plan.Rules {
applied = append(applied, rulePlan.Expected)
}
return filter.ApplyResult{Applied: applied}, nil
} }
func compileBatch(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) { func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
plan := filter.BackendPlan{ plan := filter.CommandBatch{Provider: filter.ProviderIptables, Scope: snapshot.Scope, CommandOnly: true}
Provider: filter.ProviderIptables, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision, var script strings.Builder
Rules: make([]filter.NativeRulePlan, 0, len(changes)), fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
}
current := snapshot
for _, change := range changes { for _, change := range changes {
rulePlan, err := compileChange(current, change) rulePlan, err := compileChange(snapshot, change)
if err != nil { if err != nil {
return filter.BackendPlan{}, err return filter.CommandBatch{}, err
} }
rulePlan.Commands = nil line, err := restoreRuleLine(snapshot.Scope, *rulePlan.Previous)
rulePlan.RollbackCommands = nil if err != nil {
return filter.CommandBatch{}, err
}
script.WriteString(strings.Replace(line, "-A ", "-D ", 1))
script.WriteByte('\n')
rulePlan.Commands, rulePlan.RollbackCommands = nil, nil
plan.Rules = append(plan.Rules, rulePlan) plan.Rules = append(plan.Rules, rulePlan)
current, err = applyRestoreRulePlan(current, rulePlan)
if err != nil {
return filter.BackendPlan{}, err
}
}
applyScript, err := buildRestoreScript(snapshot.Scope, current.Rules)
if err != nil {
return filter.BackendPlan{}, err
}
rollbackScript, err := buildRestoreScript(snapshot.Scope, snapshot.Rules)
if err != nil {
return filter.BackendPlan{}, err
} }
script.WriteString("COMMIT\n")
plan.Rules[0].Commands = []filter.NativeCommand{{ plan.Rules[0].Commands = []filter.NativeCommand{{
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: applyScript, Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: script.String(),
}}
plan.Rules[0].RollbackCommands = []filter.NativeCommand{{
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: rollbackScript,
}} }}
return plan, nil return plan, nil
} }
func applyRestoreRulePlan(snapshot filter.Snapshot, plan filter.NativeRulePlan) (filter.Snapshot, error) { func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
position := plan.Expected.Locator.Position plan := filter.CommandBatch{Provider: filter.ProviderIptables, Scope: snapshot.Scope, CommandOnly: true}
if plan.Operation == filter.ChangeDelete && plan.Previous != nil {
position = plan.Previous.Locator.Position
}
if position == nil {
return filter.Snapshot{}, fmt.Errorf("%w: batch rule has no target position", filter.ErrInvalidRule)
}
nativePosition := *position
rules := append([]filter.ObservedRule(nil), snapshot.Rules...)
switch plan.Operation {
case filter.ChangeCreate:
if nativePosition < 1 || nativePosition > len(rules)+1 {
return filter.Snapshot{}, fmt.Errorf("%w: batch target position %d is out of range", filter.ErrInvalidRule, nativePosition)
}
expected := plan.Expected
expected.Raw = ""
rules = append(rules, filter.ObservedRule{})
copy(rules[nativePosition:], rules[nativePosition-1:])
rules[nativePosition-1] = expected
case filter.ChangeDelete:
if nativePosition < 1 || nativePosition > len(rules) {
return filter.Snapshot{}, fmt.Errorf("%w: batch target position %d is out of range", filter.ErrRuleStale, nativePosition)
}
rules = append(rules[:nativePosition-1], rules[nativePosition:]...)
case filter.ChangeAdopt, filter.ChangeUpdate, filter.ChangeReorder:
if plan.Previous == nil || plan.Previous.Locator.Position == nil {
return filter.Snapshot{}, fmt.Errorf("%w: mutation has no previous position", filter.ErrInvalidRule)
}
previousPosition := *plan.Previous.Locator.Position
if previousPosition < 1 || previousPosition > len(rules) || nativePosition < 1 || nativePosition > len(rules) {
return filter.Snapshot{}, fmt.Errorf("%w: mutation position is out of range", filter.ErrRuleStale)
}
expected := plan.Expected
expected.Raw = ""
if previousPosition == nativePosition {
rules[previousPosition-1] = expected
break
}
rules = append(rules[:previousPosition-1], rules[previousPosition:]...)
rules = append(rules, filter.ObservedRule{})
copy(rules[nativePosition:], rules[nativePosition-1:])
rules[nativePosition-1] = expected
default:
return filter.Snapshot{}, fmt.Errorf("%w: unsupported batch operation %s", filter.ErrInvalidRule, plan.Operation)
}
for index := range rules {
position := index + 1
rules[index].Locator.Position = &position
}
return filter.NewSnapshot(snapshot.Scope, rules)
}
func buildRestoreScript(scope filter.Scope, rules []filter.ObservedRule) (string, error) {
var script strings.Builder var script strings.Builder
script.WriteByte('*') fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
script.WriteString(scope.Table) for _, change := range changes {
script.WriteByte('\n') rulePlan, err := compileChange(snapshot, change)
script.WriteString("-F ")
script.WriteString(scope.Chain)
script.WriteByte('\n')
for _, observed := range rules {
line, err := restoreRuleLine(scope, observed)
if err != nil { if err != nil {
return "", err return filter.CommandBatch{}, err
} }
line, err := restoreRuleLine(snapshot.Scope, rulePlan.Expected)
if err != nil {
return filter.CommandBatch{}, err
}
if !change.Append {
line = strings.Replace(line, "-A "+snapshot.Scope.Chain+" ", fmt.Sprintf("-I %s %d ", snapshot.Scope.Chain, *rulePlan.Expected.Locator.Position), 1)
}
rulePlan.Expected.Locator.Position = nil
script.WriteString(line) script.WriteString(line)
script.WriteByte('\n') script.WriteByte('\n')
rulePlan.Commands, rulePlan.RollbackCommands = nil, nil
plan.Rules = append(plan.Rules, rulePlan)
} }
script.WriteString("COMMIT\n") script.WriteString("COMMIT\n")
return script.String(), nil plan.Rules[0].Commands = []filter.NativeCommand{{
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: script.String(),
}}
return plan, nil
} }
func restoreRuleLine(scope filter.Scope, observed filter.ObservedRule) (string, error) { func restoreRuleLine(scope filter.Scope, observed filter.ObservedRule) (string, error) {
@@ -309,50 +290,7 @@ func restoreRuleLine(scope filter.Scope, observed filter.ObservedRule) (string,
return strings.Join(tokens, " "), nil return strings.Join(tokens, " "), nil
} }
func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, error) { func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
if plan.Provider != filter.ProviderIptables {
return filter.VerifyResult{}, fmt.Errorf("%w: backend plan provider %q", filter.ErrUnsupportedScope, plan.Provider)
}
snapshot, err := a.Observe(ctx, plan.Scope)
if err != nil {
return filter.VerifyResult{}, err
}
for _, expected := range plan.Rules {
markerMatches := 0
semanticMatches := 0
for _, observed := range snapshot.Rules {
if observed.Marker != "" && observed.Marker == expected.Expected.Marker {
markerMatches++
want, wantErr := filter.RuleKey(expected.Expected.Rule)
got, gotErr := filter.RuleKey(observed.Rule)
if wantErr == nil && gotErr == nil && want == got {
semanticMatches++
}
}
}
requiresPositionMatch := expected.Operation == filter.ChangeReorder ||
(expected.Operation == filter.ChangeUpdate && expected.Expected.Rule.OrderIndex != nil)
positionMatches := true
if requiresPositionMatch {
positionMatches = false
for _, observed := range snapshot.Rules {
if observed.Marker == expected.Expected.Marker && observed.Locator.Position != nil &&
expected.Expected.Locator.Position != nil && *observed.Locator.Position == *expected.Expected.Locator.Position {
positionMatches = true
break
}
}
}
if (expected.Operation == filter.ChangeDelete && markerMatches != 0) ||
(requiresPositionMatch && !positionMatches) ||
(expected.Operation != filter.ChangeDelete && (markerMatches != 1 || semanticMatches != 1)) {
return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil
}
}
return filter.VerifyResult{Snapshot: snapshot, Matched: true}, nil
}
func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
if a.writer == nil { if a.writer == nil {
return fmt.Errorf("iptables writer is required") return fmt.Errorf("iptables writer is required")
} }
@@ -368,7 +306,7 @@ func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
return a.rollback(ctx, plan, len(plan.Rules)-1, -1) return a.rollback(ctx, plan, len(plan.Rules)-1, -1)
} }
func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, lastRule, lastCommandCount int, cause error) error { func (a *Adapter) compensate(ctx context.Context, plan filter.CommandBatch, lastRule, lastCommandCount int, cause error) error {
if plan.CreatesOnly() { if plan.CreatesOnly() {
return cause return cause
} }
@@ -381,7 +319,7 @@ func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, lastR
return cause return cause
} }
func (a *Adapter) rollback(ctx context.Context, plan filter.BackendPlan, lastRule, lastCommandCount int) error { func (a *Adapter) rollback(ctx context.Context, plan filter.CommandBatch, lastRule, lastCommandCount int) error {
var rollbackErr error var rollbackErr error
for index := lastRule; index >= 0; index-- { for index := lastRule; index >= 0; index-- {
commands := plan.Rules[index].RollbackCommands commands := plan.Rules[index].RollbackCommands
@@ -418,53 +356,65 @@ func validateAdapterScope(scope filter.Scope) error {
return nil return nil
} }
func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filter.NativeRulePlan, error) { func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) {
rule := change.After rule := change.After
if change.Operation == filter.ChangeDelete { if change.Operation == filter.ChangeDelete {
rule = change.Before rule = change.Before
} }
if rule == nil { if rule == nil {
return filter.NativeRulePlan{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation)
} }
normalized, err := filter.NormalizeRule(*rule) normalized, err := filter.NormalizeRule(*rule)
if err != nil { if err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
if normalized.Scope.Key() != snapshot.Scope.Key() { if normalized.Scope.Key() != snapshot.Scope.Key() {
return filter.NativeRulePlan{}, fmt.Errorf("%w: change scope %s", filter.ErrUnsupportedScope, normalized.Scope.Key()) return filter.RuleCommands{}, fmt.Errorf("%w: change scope %s", filter.ErrUnsupportedScope, normalized.Scope.Key())
} }
if normalized.UUID == "" { if normalized.UUID == "" {
return filter.NativeRulePlan{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule) return filter.RuleCommands{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule)
} }
if (normalized.Scope.Family == filter.FamilyIPv4 && normalized.Protocol == "icmpv6") || if (normalized.Scope.Family == filter.FamilyIPv4 && normalized.Protocol == "icmpv6") ||
(normalized.Scope.Family == filter.FamilyIPv6 && normalized.Protocol == "icmp") { (normalized.Scope.Family == filter.FamilyIPv6 && normalized.Protocol == "icmp") {
return filter.NativeRulePlan{}, fmt.Errorf("%w: protocol %q does not match %s", filter.ErrInvalidRule, normalized.Protocol, normalized.Scope.Family) return filter.RuleCommands{}, fmt.Errorf("%w: protocol %q does not match %s", filter.ErrInvalidRule, normalized.Protocol, normalized.Scope.Family)
} }
marker := "1panel-rule:" + normalized.UUID marker := "1panel-rule:" + normalized.UUID
if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil {
previous := filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported}
var commands []filter.NativeCommand
if change.UnmarkedAdopted || change.PreviousMarker != "" {
previous.Marker = change.PreviousMarker
args := []string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain}
args = append(args, compileObservedRuleArgs(previous)...)
commands = []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: args}}
}
return filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Previous: &previous, Expected: previous, Commands: commands}, nil
}
position := len(snapshot.Rules) + 1 position := len(snapshot.Rules) + 1
verb := "-I" verb := "-I"
var target filter.ObservedRule var target filter.ObservedRule
switch change.Operation { switch change.Operation {
case filter.ChangeCreate: case filter.ChangeCreate:
if normalized.OrderIndex != nil && (*normalized.OrderIndex < 1 || *normalized.OrderIndex > int64(len(snapshot.Rules)+1)) { hasSnapshot := !change.CommandOnly || snapshot.Rules != nil
return filter.NativeRulePlan{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule) if normalized.OrderIndex != nil && (*normalized.OrderIndex < 1 || hasSnapshot && *normalized.OrderIndex > int64(len(snapshot.Rules)+1)) {
return filter.RuleCommands{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule)
} }
position = insertionPosition(snapshot, normalized) position = insertionPosition(snapshot, normalized)
case filter.ChangeAdopt: case filter.ChangeAdopt:
position, target, err = validateMutationTarget(snapshot, change, normalized, marker) position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
if err != nil { if err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
verb = "-R" verb = "-R"
case filter.ChangeUpdate: case filter.ChangeUpdate:
position, target, err = validateMutationTarget(snapshot, change, normalized, marker) position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
if err != nil { if err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
targetPosition := position targetPosition := position
if normalized.OrderIndex != nil { if normalized.OrderIndex != nil {
if *normalized.OrderIndex < 1 || *normalized.OrderIndex > int64(len(snapshot.Rules)) { if *normalized.OrderIndex < 1 || *normalized.OrderIndex > int64(len(snapshot.Rules)) {
return filter.NativeRulePlan{}, fmt.Errorf("%w: update target is out of range", filter.ErrInvalidRule) return filter.RuleCommands{}, fmt.Errorf("%w: update target is out of range", filter.ErrInvalidRule)
} }
targetPosition = int(*normalized.OrderIndex) targetPosition = int(*normalized.OrderIndex)
} }
@@ -475,21 +425,21 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
case filter.ChangeDelete: case filter.ChangeDelete:
position, target, err = validateMutationTarget(snapshot, change, normalized, marker) position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
if err != nil { if err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
verb = "-D" verb = "-D"
case filter.ChangeReorder: case filter.ChangeReorder:
position, target, err = validateMutationTarget(snapshot, change, normalized, marker) position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
if err != nil { if err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
if normalized.OrderIndex == nil || *normalized.OrderIndex < 1 || *normalized.OrderIndex > int64(len(snapshot.Rules)) { if normalized.OrderIndex == nil || *normalized.OrderIndex < 1 || *normalized.OrderIndex > int64(len(snapshot.Rules)) {
return filter.NativeRulePlan{}, fmt.Errorf("%w: reorder target is out of range", filter.ErrInvalidRule) return filter.RuleCommands{}, fmt.Errorf("%w: reorder target is out of range", filter.ErrInvalidRule)
} }
targetPosition := int(*normalized.OrderIndex) targetPosition := int(*normalized.OrderIndex)
return positionalMutationPlan(snapshot, normalized, target, marker, position, targetPosition, change.Operation), nil return positionalMutationPlan(snapshot, normalized, target, marker, position, targetPosition, change.Operation), nil
default: default:
return filter.NativeRulePlan{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation)
} }
args := []string{"-w", "-t", snapshot.Scope.Table, verb, snapshot.Scope.Chain, strconv.Itoa(position)} args := []string{"-w", "-t", snapshot.Scope.Table, verb, snapshot.Scope.Chain, strconv.Itoa(position)}
if change.Operation != filter.ChangeDelete { if change.Operation != filter.ChangeDelete {
@@ -511,36 +461,32 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported, Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported,
Locator: filter.Locator{Provider: filter.ProviderIptables, ScopeKey: snapshot.Scope.Key(), Position: &position}, Locator: filter.Locator{Provider: filter.ProviderIptables, ScopeKey: snapshot.Scope.Key(), Position: &position},
} }
return filter.NativeRulePlan{ var previous *filter.ObservedRule
if change.Operation != filter.ChangeCreate {
previous = &target
}
return filter.RuleCommands{
RuleUUID: normalized.UUID, Operation: change.Operation, RuleUUID: normalized.UUID, Operation: change.Operation,
Commands: []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: args}}, Commands: []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: args}},
RollbackCommands: []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: rollbackArgs}}, RollbackCommands: []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: rollbackArgs}},
Previous: pointerToObserved(target, change.Operation != filter.ChangeCreate), Previous: previous,
Expected: expected, Expected: expected,
}, nil }, nil
} }
func positionalMutationPlan( func positionalMutationPlan(snapshot filter.RuleSet, rule filter.FirewallRule, previous filter.ObservedRule, marker string, position int, targetPosition int, operation filter.ChangeOperation) filter.RuleCommands {
snapshot filter.Snapshot,
rule filter.FirewallRule,
previous filter.ObservedRule,
marker string,
position int,
targetPosition int,
operation filter.ChangeOperation,
) filter.NativeRulePlan {
expected := filter.ObservedRule{ expected := filter.ObservedRule{
Rule: rule, Marker: marker, ParseStatus: filter.ParseStatusSupported, Rule: rule, Marker: marker, ParseStatus: filter.ParseStatusSupported,
Locator: filter.Locator{Provider: filter.ProviderIptables, ScopeKey: snapshot.Scope.Key(), Position: &targetPosition}, Locator: filter.Locator{Provider: filter.ProviderIptables, ScopeKey: snapshot.Scope.Key(), Position: &targetPosition},
} }
plan := filter.NativeRulePlan{ plan := filter.RuleCommands{
RuleUUID: rule.UUID, Operation: operation, Previous: &previous, Expected: expected, RuleUUID: rule.UUID, Operation: operation, Previous: &previous, Expected: expected,
} }
if position == targetPosition { if position == targetPosition {
return plan return plan
} }
executable := executableForFamily(snapshot.Scope.Family) executable := executableForFamily(snapshot.Scope.Family)
deleteArgs := []string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain, strconv.Itoa(position)} deleteArgs := append([]string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain}, compileObservedRuleArgs(previous)...)
insertArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(targetPosition)} insertArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(targetPosition)}
insertArgs = append(insertArgs, compileRuleArgs(rule, marker)...) insertArgs = append(insertArgs, compileRuleArgs(rule, marker)...)
restoreArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(position)} restoreArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(position)}
@@ -558,13 +504,6 @@ func positionalMutationPlan(
return plan return plan
} }
func pointerToObserved(rule filter.ObservedRule, include bool) *filter.ObservedRule {
if !include {
return nil
}
return &rule
}
func compileRuleArgs(rule filter.FirewallRule, marker string) []string { func compileRuleArgs(rule filter.FirewallRule, marker string) []string {
args := make([]string, 0, 24) args := make([]string, 0, 24)
if rule.Protocol != "all" { if rule.Protocol != "all" {
@@ -585,16 +524,16 @@ func compileRuleArgs(rule filter.FirewallRule, marker string) []string {
} }
if rule.SourcePort != "" { if rule.SourcePort != "" {
if strings.Contains(rule.SourcePort, ",") { if strings.Contains(rule.SourcePort, ",") {
args = append(args, "-m", "multiport", "--sports", nativePort(rule.SourcePort)) args = append(args, "-m", "multiport", "--sports", strings.ReplaceAll(rule.SourcePort, "-", ":"))
} else { } else {
args = append(args, "--sport", nativePort(rule.SourcePort)) args = append(args, "--sport", strings.ReplaceAll(rule.SourcePort, "-", ":"))
} }
} }
if rule.DestinationPort != "" { if rule.DestinationPort != "" {
if strings.Contains(rule.DestinationPort, ",") { if strings.Contains(rule.DestinationPort, ",") {
args = append(args, "-m", "multiport", "--dports", nativePort(rule.DestinationPort)) args = append(args, "-m", "multiport", "--dports", strings.ReplaceAll(rule.DestinationPort, "-", ":"))
} else { } else {
args = append(args, "--dport", nativePort(rule.DestinationPort)) args = append(args, "--dport", strings.ReplaceAll(rule.DestinationPort, "-", ":"))
} }
} }
if len(rule.ConnectionStates) != 0 { if len(rule.ConnectionStates) != 0 {
@@ -615,11 +554,7 @@ func compileObservedRuleArgs(observed filter.ObservedRule) []string {
return compileRuleArgs(observed.Rule, comment) return compileRuleArgs(observed.Rule, comment)
} }
func nativePort(port string) string { func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, after filter.FirewallRule, marker string) (int, filter.ObservedRule, error) {
return strings.ReplaceAll(port, "-", ":")
}
func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChange, after filter.FirewallRule, marker string) (int, filter.ObservedRule, error) {
if change.Locator == nil || change.Locator.Position == nil { if change.Locator == nil || change.Locator.Position == nil {
return 0, filter.ObservedRule{}, fmt.Errorf("%w: mutation requires a position locator", filter.ErrInvalidRule) return 0, filter.ObservedRule{}, fmt.Errorf("%w: mutation requires a position locator", filter.ErrInvalidRule)
} }
@@ -659,7 +594,7 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
return position, observed, nil return position, observed, nil
} }
func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int { func insertionPosition(snapshot filter.RuleSet, rule filter.FirewallRule) int {
if rule.OrderIndex != nil { if rule.OrderIndex != nil {
return int(*rule.OrderIndex) return int(*rule.OrderIndex)
} }
@@ -677,16 +612,20 @@ func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int {
type systemBackend struct{} type systemBackend struct{}
func (systemBackend) CheckMultiport(ctx context.Context, family filter.Family) error { func (systemBackend) ReadRulesByComment(ctx context.Context, scope filter.Scope, comment string) (string, error) {
executable, err := runtimeExecutableForFamily(family) executable, err := runtimeExecutable(executableForFamily(scope.Family))
if err != nil { if err != nil {
return err return "", err
} }
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(20*time.Second)).RunWithOptionalSudo(executable, "-m", "multiport", "--help") return filter.ReadRulesByComment(ctx, executable, []string{"-w", "-t", scope.Table, "-S", scope.Chain}, comment)
} }
func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) { func (systemBackend) ListTable(ctx context.Context, scope filter.Scope) (string, error) {
output, err := native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6) return native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6)
}
func (b systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) {
output, err := b.ListTable(ctx, scope)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -752,10 +691,6 @@ func restoreExecutableForFamily(family filter.Family) string {
return "iptables-restore" return "iptables-restore"
} }
func runtimeExecutableForFamily(family filter.Family) (string, error) {
return runtimeExecutable(executableForFamily(family))
}
func runtimeExecutable(logical string) (string, error) { func runtimeExecutable(logical string) (string, error) {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
@@ -909,7 +844,7 @@ func parseRule(scope filter.Scope, raw string, position int) filter.ObservedRule
} }
return filter.ObservedRule{ return filter.ObservedRule{
Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw,
Protected: isProtectedRule(scope, normalized, comment), Protected: filter.IsBuiltinProtectedRule(normalized),
} }
} }
@@ -920,29 +855,6 @@ func isDefaultRejectWith(family filter.Family, value string) bool {
return value == "icmp-port-unreachable" return value == "icmp-port-unreachable"
} }
func isProtectedRule(scope filter.Scope, rule filter.FirewallRule, comment string) bool {
if scope.Chain == native.BasicBeforeChain || scope.Chain == native.BasicAfterChain {
return true
}
if rule.Action == filter.ActionAccept && rule.Interface == "lo" {
return true
}
if rule.Action == filter.ActionAccept {
states := make(map[string]struct{}, len(rule.ConnectionStates))
for _, state := range rule.ConnectionStates {
states[state] = struct{}{}
}
if _, established := states["established"]; established {
return true
}
}
if scope.Chain == native.BasicAfterChain && rule.Action == filter.ActionDrop &&
rule.SourceAddress == "" && rule.DestinationAddress == "" && rule.SourcePort == "" && rule.DestinationPort == "" {
return true
}
return strings.Contains(strings.ToLower(comment), "whitelist")
}
func takeValue(args []string, index *int, target *string) bool { func takeValue(args []string, index *int, target *string) bool {
if *index+1 >= len(args) { if *index+1 >= len(args) {
return false return false
@@ -951,3 +863,7 @@ func takeValue(args []string, index *int, target *string) bool {
*target = args[*index] *target = args[*index]
return true return true
} }
func (a *Adapter) SaveRules(ctx context.Context, scope filter.Scope) error {
return a.writer.Save(ctx, scope)
}
@@ -15,11 +15,16 @@ import (
) )
type Backend interface { type Backend interface {
filter.CommentRuleReader
ListChain(context.Context, filter.Scope) (string, error) ListChain(context.Context, filter.Scope) (string, error)
Run(context.Context, filter.NativeCommand) error Run(context.Context, filter.NativeCommand) error
Save(context.Context) error Save(context.Context) error
} }
type TableReader interface {
ListTable(context.Context, filter.Scope) (string, bool, error)
}
type Adapter struct{ backend Backend } type Adapter struct{ backend Backend }
func NewAdapter() *Adapter { return &Adapter{backend: systemBackend{}} } func NewAdapter() *Adapter { return &Adapter{backend: systemBackend{}} }
@@ -34,19 +39,47 @@ func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
}, nil }, nil
} }
func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) { func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
rule, err := filter.NormalizeRule(rule)
if err != nil {
return err
}
if err := validateScope(rule.Scope); err != nil {
return err
}
script := fmt.Sprintf("add rule %s %s %s %s\n", nftables_helper.TableFamily(rule.Scope.Family), nftables_helper.TableName, nativeChainName(rule.Scope), strings.Join(compileExpressionArgs(rule, comment), " "))
return a.backend.Run(ctx, filter.NativeCommand{Executable: "nft", Stdin: script})
}
func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, comment string) ([]filter.ObservedRule, error) {
var rules []filter.ObservedRule
for _, scope := range scopes {
scope = scope.Normalize()
if err := validateScope(scope); err != nil {
return nil, err
}
output, err := a.backend.ReadRulesByComment(ctx, scope, comment)
if err != nil {
return nil, err
}
rules = append(rules, parseChain(scope, output)...)
}
return rules, nil
}
func (a *Adapter) ListRules(ctx context.Context, scope filter.Scope) (filter.RuleSet, error) {
scope = scope.Normalize() scope = scope.Normalize()
if err := validateScope(scope); err != nil { if err := validateScope(scope); err != nil {
return filter.Snapshot{}, err return filter.RuleSet{}, err
} }
if a.backend == nil { if a.backend == nil {
return filter.Snapshot{}, fmt.Errorf("nftables backend is required") return filter.RuleSet{}, fmt.Errorf("nftables backend is required")
} }
output, err := a.backend.ListChain(ctx, scope) output, err := a.backend.ListChain(ctx, scope)
if errors.Is(err, nftables_helper.ErrChainNotFound) { if errors.Is(err, nftables_helper.ErrChainNotFound) {
snapshot, snapshotErr := filter.NewSnapshot(scope, nil) snapshot, snapshotErr := filter.NewRuleSet(scope, nil)
if snapshotErr != nil { if snapshotErr != nil {
return filter.Snapshot{}, snapshotErr return filter.RuleSet{}, snapshotErr
} }
snapshot.Notices = []filter.ScopeNotice{{ snapshot.Notices = []filter.ScopeNotice{{
Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(scope.Family), scope.Chain}, Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(scope.Family), scope.Chain},
@@ -54,68 +87,189 @@ func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snaps
return snapshot, nil return snapshot, nil
} }
if err != nil { if err != nil {
return filter.Snapshot{}, err return filter.RuleSet{}, err
} }
return filter.NewSnapshot(scope, parseChain(scope, output)) return filter.NewRuleSet(scope, parseChain(scope, output))
} }
func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) { func (a *Adapter) ListRuleScopes(ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
if snapshot.Revision == "" { normalized := make([]filter.Scope, len(scopes))
return filter.BackendPlan{}, filter.ErrRuleStale for index, scope := range scopes {
scope = scope.Normalize()
if err := validateScope(scope); err != nil {
return nil, err
}
normalized[index] = scope
} }
if a.backend == nil {
return nil, fmt.Errorf("nftables backend is required")
}
snapshots := make([]filter.RuleSet, len(scopes))
reader, readsTable := a.backend.(TableReader)
for index, scope := range normalized {
if snapshots[index].Scope.Provider != "" {
continue
}
if !readsTable {
snapshot, err := a.ListRules(ctx, scope)
if err != nil {
return nil, err
}
snapshots[index] = snapshot
continue
}
output, _, err := reader.ListTable(ctx, scope)
if err != nil {
return nil, err
}
chains := nftables_helper.ParseTableChains(output)
for target := index; target < len(normalized); target++ {
current := normalized[target]
if current.Family != scope.Family || current.Table != scope.Table {
continue
}
chain, exists := chains[nativeChainName(current)]
snapshot, err := filter.NewRuleSet(current, parseChain(current, chain))
if err != nil {
return nil, err
}
if !exists {
snapshot.Notices = []filter.ScopeNotice{{Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(current.Family), current.Chain}}}
}
snapshots[target] = snapshot
}
}
return snapshots, nil
}
func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
if err := validateScope(snapshot.Scope); err != nil { if err := validateScope(snapshot.Scope); err != nil {
return filter.BackendPlan{}, err return filter.CommandBatch{}, err
} }
if len(changes) == 0 { if len(changes) == 0 {
return filter.BackendPlan{}, fmt.Errorf("%w: nftables plan requires at least one change", filter.ErrInvalidRule) return filter.CommandBatch{}, fmt.Errorf("%w: nftables plan requires at least one change", filter.ErrInvalidRule)
}
if len(changes) > 1 && changes[0].Operation != filter.ChangeCreate && changes[0].Operation != filter.ChangeDelete {
return filter.BackendPlan{}, fmt.Errorf("%w: nftables batch plans only support create or delete operations", filter.ErrInvalidRule)
}
operation := changes[0].Operation
current := snapshot
plan := filter.BackendPlan{
Provider: filter.ProviderNftables, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision,
Rules: make([]filter.NativeRulePlan, 0, len(changes)),
} }
createOnly, deleteOnly := true, true
for _, change := range changes { for _, change := range changes {
if len(changes) > 1 && change.Operation != operation { createOnly = createOnly && change.Operation == filter.ChangeCreate && change.CommandOnly
return filter.BackendPlan{}, fmt.Errorf("%w: nftables batch plan operations must be homogeneous", filter.ErrInvalidRule) deleteOnly = deleteOnly && change.Operation == filter.ChangeDelete && change.CommandOnly
}
if createOnly {
return compileCreateBatch(snapshot, changes)
}
if deleteOnly {
return compileDeleteBatch(snapshot, changes)
}
if len(changes) != 1 {
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 {
return filter.CommandBatch{}, fmt.Errorf("%w: unsupported nftables mutation %s", filter.ErrInvalidRule, change.Operation)
}
expected, previous, err := compileChange(snapshot, change)
if err != nil {
return filter.CommandBatch{}, err
}
handle := previous.Locator.NativeID
if _, err := strconv.ParseUint(handle, 10, 64); err != nil || handle != change.Locator.NativeID {
return filter.CommandBatch{}, filter.ErrRuleStale
}
chain := strings.Join([]string{nftables_helper.TableFamily(snapshot.Scope.Family), nftables_helper.TableName, nativeChainName(snapshot.Scope)}, " ")
rulePlan := filter.RuleCommands{RuleUUID: ruleUUID(change), Operation: change.Operation, Previous: previous, Expected: expected}
target := *expected.Locator.Position
var script string
if target == *previous.Locator.Position {
if change.Operation != filter.ChangeReorder {
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)
}
rulePlan.RollbackCommands = []filter.NativeCommand{{Executable: "nft", Stdin: fmt.Sprintf("replace rule %s handle %s %s\n", chain, handle, previous.Raw)}}
} }
rules, expected, previous, err := applyChange(current, change) } else {
script = fmt.Sprintf("delete rule %s handle %s\n", chain, handle)
if target == len(snapshot.Rules) {
script += fmt.Sprintf("add rule %s %s\n", chain, expected.Raw)
} else {
anchorIndex := target - 1
if target > *previous.Locator.Position {
anchorIndex++
}
anchor := snapshot.Rules[anchorIndex].Locator.NativeID
if _, err := strconv.ParseUint(anchor, 10, 64); err != nil {
return filter.CommandBatch{}, filter.ErrRuleStale
}
script += fmt.Sprintf("insert rule %s position %s %s\n", chain, anchor, expected.Raw)
}
}
if script != "" {
rulePlan.Commands = []filter.NativeCommand{{Executable: "nft", Stdin: script}}
}
return filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: change.CommandOnly, Rules: []filter.RuleCommands{rulePlan}}, nil
}
func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
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 { if err != nil {
return filter.BackendPlan{}, err return filter.CommandBatch{}, err
} }
plan.Rules = append(plan.Rules, filter.NativeRulePlan{ handle := previous.Locator.NativeID
RuleUUID: ruleUUID(change), Operation: change.Operation, Previous: previous, Expected: expected, if _, err := strconv.ParseUint(handle, 10, 64); err != nil || change.Locator.NativeID != handle {
return filter.CommandBatch{}, filter.ErrRuleStale
}
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,
}) })
current, err = filter.NewSnapshot(snapshot.Scope, rules)
if err != nil {
return filter.BackendPlan{}, err
}
} }
applyCommand, err := rebuildCommand(snapshot.Scope, current.Rules) plan.Rules[0].Commands = []filter.NativeCommand{{Executable: "nft", Stdin: script.String()}}
if err != nil {
return filter.BackendPlan{}, err
}
rollbackCommand, err := rebuildCommand(snapshot.Scope, snapshot.Rules)
if err != nil {
return filter.BackendPlan{}, err
}
plan.Rules[0].Commands = []filter.NativeCommand{applyCommand}
plan.Rules[0].RollbackCommands = []filter.NativeCommand{rollbackCommand}
return plan, nil return plan, nil
} }
func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.ApplyResult, error) { func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
plan := filter.CommandBatch{Provider: filter.ProviderNftables, Scope: snapshot.Scope, CommandOnly: true}
var script strings.Builder
for _, change := range changes {
if change.After == nil {
return filter.CommandBatch{}, fmt.Errorf("%w: create rule is required", filter.ErrInvalidRule)
}
rule, err := filter.NormalizeRule(*change.After)
if err != nil {
return filter.CommandBatch{}, err
}
if rule.Scope.Key() != snapshot.Scope.Key() || rule.UUID == "" {
return filter.CommandBatch{}, fmt.Errorf("%w: invalid nftables creation rule", filter.ErrInvalidRule)
}
marker := "1panel-rule:" + rule.UUID
verb := "add"
if !change.Append {
if rule.OrderIndex == nil || *rule.OrderIndex != 1 {
return filter.CommandBatch{}, fmt.Errorf("%w: batch insertion requires the first position", filter.ErrInvalidRule)
}
verb = "insert"
}
fmt.Fprintf(&script, "%s rule %s %s %s %s\n", verb, nftables_helper.TableFamily(rule.Scope.Family), nftables_helper.TableName, nativeChainName(rule.Scope), strings.Join(compileExpressionArgs(rule, marker), " "))
plan.Rules = append(plan.Rules, filter.RuleCommands{
RuleUUID: rule.UUID, Operation: filter.ChangeCreate,
Expected: filter.ObservedRule{Rule: rule, Marker: marker, ParseStatus: filter.ParseStatusSupported},
})
}
plan.Rules[0].Commands = []filter.NativeCommand{{Executable: "nft", Stdin: script.String()}}
return plan, nil
}
func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) error {
if err := validatePlan(plan); err != nil { if err := validatePlan(plan); err != nil {
return filter.ApplyResult{}, err return err
} }
for _, rulePlan := range plan.Rules { for _, rulePlan := range plan.Rules {
for _, command := range rulePlan.Commands { for _, command := range rulePlan.Commands {
if err := validateNativeCommand(command); err != nil { if err := validateNativeCommand(command); err != nil {
return filter.ApplyResult{}, err return err
} }
} }
} }
@@ -123,57 +277,17 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
for _, command := range rulePlan.Commands { for _, command := range rulePlan.Commands {
if err := a.backend.Run(ctx, command); err != nil { if err := a.backend.Run(ctx, command); err != nil {
if plan.CommandOnly { if plan.CommandOnly {
return filter.ApplyResult{}, err return err
} }
return filter.ApplyResult{}, a.compensate(ctx, plan, err) return a.compensate(ctx, plan, err)
} }
} }
} }
if err := a.backend.Save(ctx); err != nil {
if plan.CommandOnly { return nil
return filter.ApplyResult{}, err
}
return filter.ApplyResult{}, a.compensate(ctx, plan, err)
}
applied := make([]filter.ObservedRule, 0, len(plan.Rules))
for _, rulePlan := range plan.Rules {
applied = append(applied, rulePlan.Expected)
}
return filter.ApplyResult{Applied: applied}, nil
} }
func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, error) { func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
if err := validatePlan(plan); err != nil {
return filter.VerifyResult{}, err
}
snapshot, err := a.Observe(ctx, plan.Scope)
if err != nil {
return filter.VerifyResult{}, err
}
for _, expected := range plan.Rules {
matches := 0
for _, observed := range snapshot.Rules {
if observed.Marker == expected.Expected.Marker {
if expected.Operation == filter.ChangeDelete {
matches++
continue
}
want, wantErr := filter.RuleKey(expected.Expected.Rule)
got, gotErr := filter.RuleKey(observed.Rule)
if wantErr == nil && gotErr == nil && want == got {
matches++
}
}
}
if (expected.Operation == filter.ChangeDelete && matches != 0) ||
(expected.Operation != filter.ChangeDelete && matches != 1) {
return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil
}
}
return filter.VerifyResult{Snapshot: snapshot, Matched: true}, nil
}
func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
if err := validatePlan(plan); err != nil { if err := validatePlan(plan); err != nil {
return err return err
} }
@@ -194,7 +308,7 @@ func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
return a.backend.Save(ctx) return a.backend.Save(ctx)
} }
func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, cause error) error { func (a *Adapter) compensate(ctx context.Context, plan filter.CommandBatch, cause error) error {
if plan.CreatesOnly() { if plan.CreatesOnly() {
return cause return cause
} }
@@ -230,7 +344,7 @@ func nativeChainName(scope filter.Scope) string {
} }
} }
func validatePlan(plan filter.BackendPlan) error { func validatePlan(plan filter.CommandBatch) error {
if plan.Provider != filter.ProviderNftables || len(plan.Rules) == 0 { if plan.Provider != filter.ProviderNftables || len(plan.Rules) == 0 {
return fmt.Errorf("%w: invalid nftables plan", filter.ErrInvalidRule) return fmt.Errorf("%w: invalid nftables plan", filter.ErrInvalidRule)
} }
@@ -244,66 +358,42 @@ func validateNativeCommand(command filter.NativeCommand) error {
return nil return nil
} }
func applyChange(snapshot filter.Snapshot, change filter.DesiredChange) ([]filter.ObservedRule, filter.ObservedRule, *filter.ObservedRule, error) { func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.ObservedRule, *filter.ObservedRule, error) {
rules := append([]filter.ObservedRule(nil), snapshot.Rules...)
rule := change.After rule := change.After
if change.Operation == filter.ChangeDelete { if change.Operation == filter.ChangeDelete {
rule = change.Before rule = change.Before
} }
if rule == nil { if rule == nil {
return nil, filter.ObservedRule{}, nil, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) return filter.ObservedRule{}, nil, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation)
} }
normalized, err := filter.NormalizeRule(*rule) normalized, err := filter.NormalizeRule(*rule)
if err != nil { if err != nil {
return nil, filter.ObservedRule{}, nil, err return filter.ObservedRule{}, nil, err
} }
if normalized.Scope.Key() != snapshot.Scope.Key() || normalized.UUID == "" { if normalized.Scope.Key() != snapshot.Scope.Key() || normalized.UUID == "" {
return nil, filter.ObservedRule{}, nil, fmt.Errorf("%w: invalid nftables mutation rule", filter.ErrInvalidRule) return filter.ObservedRule{}, nil, fmt.Errorf("%w: invalid nftables mutation rule", filter.ErrInvalidRule)
} }
position := len(rules) + 1 if change.Locator == nil || change.Locator.Position == nil {
marker := "1panel-rule:" + normalized.UUID return filter.ObservedRule{}, nil, fmt.Errorf("%w: mutation requires a position locator", filter.ErrInvalidRule)
var previous *filter.ObservedRule }
position := *change.Locator.Position
if change.Operation != filter.ChangeCreate { if position < 1 || position > len(snapshot.Rules) {
if change.Locator == nil || change.Locator.Position == nil { return filter.ObservedRule{}, nil, filter.ErrRuleStale
return nil, filter.ObservedRule{}, nil, fmt.Errorf("%w: mutation requires a position locator", filter.ErrInvalidRule) }
} previous := snapshot.Rules[position-1]
position = *change.Locator.Position if previous.Protected {
if position < 1 || position > len(rules) { return filter.ObservedRule{}, nil, filter.ErrProtectedRule
return nil, filter.ObservedRule{}, nil, filter.ErrRuleStale
}
selected := rules[position-1]
if selected.Protected {
return nil, filter.ObservedRule{}, nil, filter.ErrProtectedRule
}
previousCopy := selected
previous = &previousCopy
rules = append(rules[:position-1], rules[position:]...)
} }
target := position target := position
if normalized.OrderIndex != nil { if normalized.OrderIndex != nil && (change.Operation == filter.ChangeUpdate || change.Operation == filter.ChangeReorder) {
target = int(*normalized.OrderIndex) target = int(*normalized.OrderIndex)
} }
if change.Operation == filter.ChangeCreate && normalized.OrderIndex == nil { if target < 1 || target > len(snapshot.Rules) {
target = len(rules) + 1 return filter.ObservedRule{}, nil, fmt.Errorf("%w: target position is out of range", filter.ErrInvalidRule)
}
if change.Operation == filter.ChangeDelete {
expected := observedRule(normalized, marker, position, "")
return rules, expected, previous, nil
}
if target < 1 || target > len(rules)+1 {
return nil, filter.ObservedRule{}, nil, fmt.Errorf("%w: target position is out of range", filter.ErrInvalidRule)
} }
marker := "1panel-rule:" + normalized.UUID
expected := observedRule(normalized, marker, target, strings.Join(compileExpressionArgs(normalized, marker), " ")) expected := observedRule(normalized, marker, target, strings.Join(compileExpressionArgs(normalized, marker), " "))
rules = append(rules, filter.ObservedRule{}) return expected, &previous, nil
copy(rules[target:], rules[target-1:])
rules[target-1] = expected
for index := range rules {
position := index + 1
rules[index].Locator.Position = &position
}
return rules, expected, previous, nil
} }
func observedRule(rule filter.FirewallRule, marker string, position int, raw string) filter.ObservedRule { func observedRule(rule filter.FirewallRule, marker string, position int, raw string) filter.ObservedRule {
@@ -313,7 +403,7 @@ func observedRule(rule filter.FirewallRule, marker string, position int, raw str
} }
} }
func ruleUUID(change filter.DesiredChange) string { func ruleUUID(change filter.RuleChange) string {
if change.After != nil { if change.After != nil {
return change.After.UUID return change.After.UUID
} }
@@ -323,39 +413,6 @@ func ruleUUID(change filter.DesiredChange) string {
return "" return ""
} }
func rebuildCommand(scope filter.Scope, rules []filter.ObservedRule) (filter.NativeCommand, error) {
tableFamily := nftables_helper.TableFamily(scope.Family)
chain := nativeChainName(scope)
var script strings.Builder
script.WriteString("flush chain ")
script.WriteString(tableFamily)
script.WriteByte(' ')
script.WriteString(nftables_helper.TableName)
script.WriteByte(' ')
script.WriteString(chain)
script.WriteByte('\n')
for _, rule := range rules {
raw := strings.TrimSpace(rule.Raw)
if rule.ParseStatus == filter.ParseStatusSupported && rule.Marker != "" {
script.WriteString(strings.Join([]string{"add", "rule", tableFamily, nftables_helper.TableName, chain}, " "))
script.WriteByte(' ')
script.WriteString(strings.Join(compileExpressionArgs(rule.Rule, rule.Marker), " "))
script.WriteByte('\n')
continue
}
if raw != "" {
if strings.ContainsAny(raw, "\r\n") {
return filter.NativeCommand{}, fmt.Errorf("%w: invalid newline in native nftables rule", filter.ErrInvalidRule)
}
script.WriteString(strings.Join([]string{"add", "rule", tableFamily, nftables_helper.TableName, chain}, " "))
script.WriteByte(' ')
script.WriteString(raw)
script.WriteByte('\n')
}
}
return filter.NativeCommand{Executable: "nft", Stdin: script.String()}, nil
}
func compileExpressionArgs(rule filter.FirewallRule, marker string) []string { func compileExpressionArgs(rule filter.FirewallRule, marker string) []string {
parts := make([]string, 0, 24) parts := make([]string, 0, 24)
if rule.Protocol != "all" { if rule.Protocol != "all" {
@@ -416,7 +473,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
return filter.ObservedRule{ return filter.ObservedRule{
Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque}, Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque},
Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw, Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw,
Protected: scope.Chain != filter.IptablesInputChain, Protected: scope.Chain == filter.BasicAfterChain,
} }
} }
tokens, err := shellwords.Parse(raw) tokens, err := shellwords.Parse(raw)
@@ -533,7 +590,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
if err != nil { if err != nil {
return opaque() return opaque()
} }
return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: scope.Chain != filter.IptablesInputChain} return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: filter.IsBuiltinProtectedRule(normalized)}
} }
func parseProtocol(value string) string { func parseProtocol(value string) string {
@@ -582,6 +639,10 @@ func numericSymbol(value string) string {
type systemBackend struct{} type systemBackend struct{}
func (systemBackend) ReadRulesByComment(ctx context.Context, scope filter.Scope, comment string) (string, error) {
return filter.ReadRulesByComment(ctx, "nft", []string{"-a", "-n", "-n", "list", "chain", nftables_helper.TableFamily(scope.Family), nftables_helper.TableName, nativeChainName(scope)}, comment)
}
func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) { func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) {
run := func(args ...string) (string, error) { run := func(args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout( return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout(
@@ -591,6 +652,13 @@ func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string,
return nftables_helper.ReadChain(run, nftables_helper.TableFamily(scope.Family), nftables_helper.TableName, nativeChainName(scope)) return nftables_helper.ReadChain(run, nftables_helper.TableFamily(scope.Family), nftables_helper.TableName, nativeChainName(scope))
} }
func (systemBackend) ListTable(ctx context.Context, scope filter.Scope) (string, bool, error) {
run := func(args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", append([]string{"-n", "-n"}, args...)...)
}
return nftables_helper.ReadTable(run, nftables_helper.TableFamily(scope.Family), nftables_helper.TableName)
}
func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) error { func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) error {
if command.Executable != "nft" { if command.Executable != "nft" {
return fmt.Errorf("unexpected nftables executable %q", command.Executable) return fmt.Errorf("unexpected nftables executable %q", command.Executable)
@@ -605,3 +673,7 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
func (systemBackend) Save(ctx context.Context) error { func (systemBackend) Save(ctx context.Context) error {
return nftables_helper.PersistRuleset(ctx) return nftables_helper.PersistRuleset(ctx)
} }
func (a *Adapter) SaveRules(ctx context.Context, scope filter.Scope) error {
return a.backend.Save(ctx)
}
@@ -16,6 +16,7 @@ import (
) )
type CommandReader interface { type CommandReader interface {
filter.CommentRuleReader
Read(context.Context, ...string) (string, error) Read(context.Context, ...string) (string, error)
} }
@@ -69,7 +70,7 @@ func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule
} }
comment = strings.TrimSpace(comment) comment = strings.TrimSpace(comment)
if comment == "" || strings.ContainsAny(comment, "\r\n\x00") { if comment == "" || strings.ContainsAny(comment, "\r\n\x00") {
return fmt.Errorf("%w: invalid UFW fallback comment", filter.ErrInvalidRule) return fmt.Errorf("%w: invalid UFW external rule comment", filter.ErrInvalidRule)
} }
command := commentCommand(normalized, comment) command := commentCommand(normalized, comment)
if err := validateCommand(command); err != nil { if err := validateCommand(command); err != nil {
@@ -85,15 +86,35 @@ func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
}, nil }, nil
} }
func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) { func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, comment string) ([]filter.ObservedRule, error) {
snapshots, err := a.ObserveScopes(ctx, []filter.Scope{scope}) var rules []filter.ObservedRule
var output string
for index, scope := range scopes {
scope = scope.Normalize()
if err := validateScope(scope); err != nil {
return nil, err
}
if index == 0 {
var err error
output, err = a.reader.ReadRulesByComment(ctx, scope, comment)
if err != nil {
return nil, err
}
}
rules = append(rules, parseNumberedRules(scope, output)...)
}
return rules, nil
}
func (a *Adapter) ListRules(ctx context.Context, scope filter.Scope) (filter.RuleSet, error) {
snapshots, err := a.ListRuleScopes(ctx, []filter.Scope{scope})
if err != nil { if err != nil {
return filter.Snapshot{}, err return filter.RuleSet{}, err
} }
return snapshots[0], nil return snapshots[0], nil
} }
func (a *Adapter) ObserveScopes(ctx context.Context, scopes []filter.Scope) ([]filter.Snapshot, error) { func (a *Adapter) ListRuleScopes(ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
if len(scopes) == 0 { if len(scopes) == 0 {
return nil, fmt.Errorf("%w: UFW observation requires at least one scope", filter.ErrInvalidScope) return nil, fmt.Errorf("%w: UFW observation requires at least one scope", filter.ErrInvalidScope)
} }
@@ -113,14 +134,28 @@ func (a *Adapter) ObserveScopes(ctx context.Context, scopes []filter.Scope) ([]f
return nil, fmt.Errorf("%w: read UFW numbered status: %w", filter.ErrInventoryUnavailable, err) return nil, fmt.Errorf("%w: read UFW numbered status: %w", filter.ErrInventoryUnavailable, err)
} }
lastPositions := make(map[filter.Family]int)
for _, line := range strings.Split(numbered, "\n") {
matches := re.UFWNumberedRuleRegex.FindStringSubmatch(strings.TrimSpace(line))
if len(matches) != 0 {
position, err := strconv.Atoi(matches[1])
if err == nil {
family := familyForNumberedRule(matches[2], matches[5])
lastPositions[family] = max(lastPositions[family], position)
}
} else if observed, family, _, ok := parseUnrecognizedNumberedRule(normalizedScopes[0], strings.TrimSpace(line)); ok && observed.Locator.Position != nil {
lastPositions[family] = max(lastPositions[family], *observed.Locator.Position)
}
}
notices := statusNotices(numbered) notices := statusNotices(numbered)
snapshots := make([]filter.Snapshot, 0, len(normalizedScopes)) snapshots := make([]filter.RuleSet, 0, len(normalizedScopes))
for _, scope := range normalizedScopes { for _, scope := range normalizedScopes {
snapshot, err := filter.NewSnapshot(scope, parseNumberedRules(scope, numbered)) snapshot, err := filter.NewRuleSet(scope, parseNumberedRules(scope, numbered))
if err != nil { if err != nil {
return nil, err return nil, err
} }
snapshot.Notices = append([]filter.ScopeNotice(nil), notices...) snapshot.Notices = append([]filter.ScopeNotice(nil), notices...)
snapshot.LastPosition = lastPositions[scope.Family]
snapshots = append(snapshots, snapshot) snapshots = append(snapshots, snapshot)
} }
return snapshots, nil return snapshots, nil
@@ -145,46 +180,61 @@ func (a *Adapter) NativeDetail(ctx context.Context, profile string, _ bool) (str
return info, nil return info, nil
} }
func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) { func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
if snapshot.Revision == "" {
return filter.BackendPlan{}, filter.ErrRuleStale
}
if err := validateScope(snapshot.Scope); err != nil { if err := validateScope(snapshot.Scope); err != nil {
return filter.BackendPlan{}, err return filter.CommandBatch{}, err
} }
if hasScopeNotice(snapshot.Notices, filter.ScopeNoticeManagedScopeInactive) { if hasScopeNotice(snapshot.Notices, filter.ScopeNoticeManagedScopeInactive) {
return filter.BackendPlan{}, fmt.Errorf("%w: ufw is inactive", filter.ErrProviderUnavailable) return filter.CommandBatch{}, fmt.Errorf("%w: ufw is inactive", filter.ErrProviderUnavailable)
} }
if len(changes) != 1 { if len(changes) != 1 {
return filter.BackendPlan{}, fmt.Errorf("%w: ufw plans currently require exactly one change", filter.ErrInvalidRule) return filter.CommandBatch{}, fmt.Errorf("%w: ufw plans currently require exactly one change", filter.ErrInvalidRule)
} }
rulePlan, err := compileChange(snapshot, changes[0]) rulePlan, err := compileChange(snapshot, changes[0])
if err != nil { if err != nil {
return filter.BackendPlan{}, err return filter.CommandBatch{}, err
} }
return filter.BackendPlan{ return filter.CommandBatch{
Provider: filter.ProviderUFW, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision, Provider: filter.ProviderUFW, Scope: snapshot.Scope,
Rules: []filter.NativeRulePlan{rulePlan}, Rules: []filter.RuleCommands{rulePlan},
}, nil }, nil
} }
func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.ApplyResult, error) { func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) error {
if err := validateBackendPlan(plan); err != nil { if err := validateBackendPlan(plan); err != nil {
return filter.ApplyResult{}, err return err
} }
if a.writer == nil { if a.writer == nil {
return filter.ApplyResult{}, errors.New("ufw writer is required") return errors.New("ufw writer is required")
} }
for _, command := range append(append([]filter.NativeCommand(nil), plan.Rules[0].Commands...), plan.Rules[0].RollbackCommands...) { for _, command := range append(append([]filter.NativeCommand(nil), plan.Rules[0].Commands...), plan.Rules[0].RollbackCommands...) {
if err := validateCommand(command); err != nil { if err := validateCommand(command); err != nil {
return filter.ApplyResult{}, err return err
} }
} }
executed := 0 executed := 0
for index, command := range plan.Rules[0].Commands { for index, command := range plan.Rules[0].Commands {
if err := a.writer.Run(ctx, command); err != nil { 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 { if plan.CommandOnly {
return filter.ApplyResult{}, fmt.Errorf("execute UFW rule: %w", err) return fmt.Errorf("execute UFW rule: %w", err)
} }
if !plan.CreatesOnly() { if !plan.CreatesOnly() {
probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
@@ -194,46 +244,22 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
cancel() cancel()
} }
cause := ufwApplyError(plan.Rules[0], fmt.Errorf("execute UFW rule: %w", err)) cause := ufwApplyError(plan.Rules[0], fmt.Errorf("execute UFW rule: %w", err))
return filter.ApplyResult{}, a.compensate(ctx, plan.Rules[0], executed, cause) return a.compensate(ctx, plan.Rules[0], executed, cause)
} }
executed = index + 1 executed = index + 1
} }
if plan.CommandOnly || plan.CreatesOnly() { return nil
return filter.ApplyResult{Applied: []filter.ObservedRule{plan.Rules[0].Expected}}, nil
}
verification, err := a.verify(ctx, plan)
if err != nil {
cause := ufwApplyError(plan.Rules[0], fmt.Errorf("verify UFW rule: %w", err))
return filter.ApplyResult{}, a.compensate(ctx, plan.Rules[0], executed, cause)
}
if !verification.Matched {
cause := ufwApplyError(plan.Rules[0], fmt.Errorf(
"ufw write verification failed for marker %q in scope %s",
plan.Rules[0].Expected.Marker,
plan.Scope.Key(),
))
return filter.ApplyResult{}, a.compensate(
ctx,
plan.Rules[0],
executed,
cause,
)
}
return filter.ApplyResult{
Applied: []filter.ObservedRule{plan.Rules[0].Expected},
Verification: &verification,
}, nil
} }
func ufwApplyError(plan filter.NativeRulePlan, cause error) error { func ufwApplyError(plan filter.RuleCommands, cause error) error {
if plan.Operation == filter.ChangeAdopt { if plan.Operation == filter.ChangeAdopt {
return buserr.WithDetail("ErrUFWRuleAdopt", cause.Error(), cause) return buserr.WithDetail("ErrUFWRuleAdopt", cause.Error(), cause)
} }
return cause return cause
} }
func (a *Adapter) failedCommandApplied(ctx context.Context, plan filter.NativeRulePlan, commandIndex int) bool { func (a *Adapter) failedCommandApplied(ctx context.Context, plan filter.RuleCommands, commandIndex int) bool {
snapshot, err := a.Observe(ctx, plan.Expected.Rule.Scope) snapshot, err := a.ListRules(ctx, plan.Expected.Rule.Scope)
if err != nil { if err != nil {
return true return true
} }
@@ -258,14 +284,7 @@ func (a *Adapter) failedCommandApplied(ctx context.Context, plan filter.NativeRu
} }
} }
func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, error) { func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
if err := validateBackendPlan(plan); err != nil {
return filter.VerifyResult{}, err
}
return a.verify(ctx, plan)
}
func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
if err := validateBackendPlan(plan); err != nil { if err := validateBackendPlan(plan); err != nil {
return err return err
} }
@@ -291,8 +310,8 @@ func validateScope(scope filter.Scope) error {
return nil return nil
} }
func validateBackendPlan(plan filter.BackendPlan) error { func validateBackendPlan(plan filter.CommandBatch) error {
if plan.Provider != filter.ProviderUFW || len(plan.Rules) != 1 || plan.SnapshotRevision == "" { if plan.Provider != filter.ProviderUFW || len(plan.Rules) != 1 {
return fmt.Errorf("%w: invalid ufw backend plan", filter.ErrInvalidRule) return fmt.Errorf("%w: invalid ufw backend plan", filter.ErrInvalidRule)
} }
if err := validateScope(plan.Scope); err != nil { if err := validateScope(plan.Scope); err != nil {
@@ -304,40 +323,56 @@ func validateBackendPlan(plan filter.BackendPlan) error {
return nil return nil
} }
func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filter.NativeRulePlan, error) { func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.RuleCommands, error) {
rule := change.After rule := change.After
if change.Operation == filter.ChangeDelete { if change.Operation == filter.ChangeDelete {
rule = change.Before rule = change.Before
} }
if rule == nil { if rule == nil {
return filter.NativeRulePlan{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation) return filter.RuleCommands{}, fmt.Errorf("%w: %s rule is required", filter.ErrInvalidRule, change.Operation)
} }
normalized, err := filter.NormalizeRule(*rule) normalized, err := filter.NormalizeRule(*rule)
if err != nil { if err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
if normalized.Scope.Key() != snapshot.Scope.Key() { if normalized.Scope.Key() != snapshot.Scope.Key() {
return filter.NativeRulePlan{}, fmt.Errorf("%w: change scope %s", filter.ErrUnsupportedScope, normalized.Scope.Key()) return filter.RuleCommands{}, fmt.Errorf("%w: change scope %s", filter.ErrUnsupportedScope, normalized.Scope.Key())
} }
if err := validateWritableRule(normalized); err != nil { if err := validateWritableRule(normalized); err != nil {
return filter.NativeRulePlan{}, err return filter.RuleCommands{}, err
} }
if normalized.UUID == "" { if normalized.UUID == "" {
return filter.NativeRulePlan{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule) return filter.RuleCommands{}, fmt.Errorf("%w: rule UUID is required", filter.ErrInvalidRule)
} }
marker := "1panel-rule:" + normalized.UUID marker := "1panel-rule:" + normalized.UUID
if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil {
if change.UnmarkedAdopted || change.PreviousMarker != "" {
marker = observedComment(filter.ObservedRule{Rule: normalized, Marker: change.PreviousMarker})
}
return filter.RuleCommands{
RuleUUID: normalized.UUID, Operation: change.Operation,
Expected: filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported},
Commands: []filter.NativeCommand{deleteRuleCommand(normalized, marker)},
RollbackCommands: []filter.NativeCommand{commentCommand(normalized, marker)},
}, nil
}
position := insertionPosition(snapshot, normalized) position := insertionPosition(snapshot, normalized)
expected := observedForRule(normalized, marker, position) expected := observedForRule(normalized, marker, position)
plan := filter.NativeRulePlan{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected} plan := filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected}
switch change.Operation { switch change.Operation {
case filter.ChangeCreate: case filter.ChangeCreate:
if normalized.OrderIndex != nil && *normalized.OrderIndex < 1 { if normalized.OrderIndex != nil && *normalized.OrderIndex < 1 {
return filter.NativeRulePlan{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule) return filter.RuleCommands{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule)
} }
command := insertCommand(position, normalized, marker) command := insertCommand(position, normalized, marker)
if change.Append || position == maximumObservedPosition(snapshot)+1 { hasSnapshot := !change.CommandOnly || snapshot.Rules != nil
if !change.Append && normalized.OrderIndex != nil && position == 1 && hasSnapshot {
command = filter.NativeCommand{Executable: "ufw", Args: append([]string{"prepend"}, compileRuleArgs(normalized, marker)...)}
} else if change.Append || normalized.OrderIndex == nil || hasSnapshot && position == maximumObservedPosition(snapshot)+1 {
command = commentCommand(normalized, marker) command = commentCommand(normalized, marker)
}
if command.Args[0] != "insert" {
plan.Expected.Locator.NativeID = "" plan.Expected.Locator.NativeID = ""
plan.Expected.Locator.Position = nil plan.Expected.Locator.Position = nil
} }
@@ -346,7 +381,7 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
case filter.ChangeAdopt: case filter.ChangeAdopt:
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, false) target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, false)
if targetErr != nil { if targetErr != nil {
return filter.NativeRulePlan{}, targetErr return filter.RuleCommands{}, targetErr
} }
position = *target.Locator.Position position = *target.Locator.Position
appendAtEnd := position == maximumObservedPosition(snapshot) appendAtEnd := position == maximumObservedPosition(snapshot)
@@ -366,21 +401,28 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
deleteRuleCommand(normalized, marker), deleteRuleCommand(normalized, marker),
} }
case filter.ChangeUpdate, filter.ChangeReorder: case filter.ChangeUpdate, filter.ChangeReorder:
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, true) if change.Before == nil {
return filter.RuleCommands{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule)
}
before, err := filter.NormalizeRule(*change.Before)
if err != nil {
return filter.RuleCommands{}, err
}
target, targetErr := validateMutationTarget(snapshot, change, before, marker, true)
if targetErr != nil { if targetErr != nil {
return filter.NativeRulePlan{}, targetErr return filter.RuleCommands{}, targetErr
} }
position = *target.Locator.Position position = *target.Locator.Position
targetPosition := position targetPosition := position
if normalized.OrderIndex != nil { if normalized.OrderIndex != nil {
if *normalized.OrderIndex < 1 { if *normalized.OrderIndex < 1 {
return filter.NativeRulePlan{}, fmt.Errorf("%w: update target is out of range", filter.ErrInvalidRule) return filter.RuleCommands{}, fmt.Errorf("%w: update target is out of range", filter.ErrInvalidRule)
} }
targetPosition = int(*normalized.OrderIndex) targetPosition = int(*normalized.OrderIndex)
} }
maximumPosition := maximumObservedPosition(snapshot) maximumPosition := maximumObservedPosition(snapshot)
if targetPosition > maximumPosition { if targetPosition > maximumPosition {
return filter.NativeRulePlan{}, fmt.Errorf( return filter.RuleCommands{}, fmt.Errorf(
"%w: update target position %d is out of range 1-%d", "%w: update target position %d is out of range 1-%d",
filter.ErrInvalidRule, targetPosition, maximumPosition, filter.ErrInvalidRule, targetPosition, maximumPosition,
) )
@@ -404,18 +446,18 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
case filter.ChangeDelete: case filter.ChangeDelete:
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, !change.UnmarkedAdopted) target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, !change.UnmarkedAdopted)
if targetErr != nil { if targetErr != nil {
return filter.NativeRulePlan{}, targetErr return filter.RuleCommands{}, targetErr
} }
position = *target.Locator.Position position = *target.Locator.Position
plan.Previous = &target plan.Previous = &target
plan.Expected = target plan.Expected = target
plan.Commands = []filter.NativeCommand{deletePositionCommand(position)} plan.Commands = []filter.NativeCommand{deleteRuleCommand(target.Rule, observedComment(target))}
restoreAtEnd := change.RestoreAtEnd || position == maximumObservedPosition(snapshot) restoreAtEnd := change.RestoreAtEnd || position == maximumObservedPosition(snapshot)
plan.RollbackCommands = []filter.NativeCommand{ plan.RollbackCommands = []filter.NativeCommand{
positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd), positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd),
} }
default: default:
return filter.NativeRulePlan{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation) return filter.RuleCommands{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation)
} }
return plan, nil return plan, nil
} }
@@ -434,7 +476,7 @@ func validateWritableRule(rule filter.FirewallRule) error {
return nil return nil
} }
func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChange, desired filter.FirewallRule, marker string, requireOwned bool) (filter.ObservedRule, error) { 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 { if change.Locator == nil || change.Locator.Position == nil {
return filter.ObservedRule{}, fmt.Errorf("%w: ufw mutation requires a numbered locator", filter.ErrInvalidRule) return filter.ObservedRule{}, fmt.Errorf("%w: ufw mutation requires a numbered locator", filter.ErrInvalidRule)
} }
@@ -504,7 +546,7 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
return target, nil return target, nil
} }
func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int { func insertionPosition(snapshot filter.RuleSet, rule filter.FirewallRule) int {
if rule.OrderIndex != nil && *rule.OrderIndex > 0 { if rule.OrderIndex != nil && *rule.OrderIndex > 0 {
return int(*rule.OrderIndex) return int(*rule.OrderIndex)
} }
@@ -512,8 +554,8 @@ func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int {
return position return position
} }
func maximumObservedPosition(snapshot filter.Snapshot) int { func maximumObservedPosition(snapshot filter.RuleSet) int {
maximum := 0 maximum := snapshot.LastPosition
for _, observed := range snapshot.Rules { for _, observed := range snapshot.Rules {
if observed.Locator.Position != nil && *observed.Locator.Position > maximum { if observed.Locator.Position != nil && *observed.Locator.Position > maximum {
maximum = *observed.Locator.Position maximum = *observed.Locator.Position
@@ -609,47 +651,7 @@ func observedComment(observed filter.ObservedRule) string {
return observed.Rule.Description return observed.Rule.Description
} }
func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, error) { func (a *Adapter) compensate(ctx context.Context, plan filter.RuleCommands, executed int, cause error) error {
scopes := append([]filter.Scope{plan.Scope}, relatedScopes(plan.Scope)...)
snapshots, err := a.ObserveScopes(ctx, scopes)
if err != nil {
return filter.VerifyResult{}, err
}
if len(snapshots) != len(scopes) {
return filter.VerifyResult{}, errors.New("ufw observation returned an incomplete scope set")
}
snapshot := snapshots[0]
rulePlan := plan.Rules[0]
marker := rulePlan.Expected.Marker
count := 0
for _, observedSnapshot := range snapshots {
count += countMarker(observedSnapshot, marker)
}
if rulePlan.Operation == filter.ChangeDelete {
return filter.VerifyResult{Snapshot: snapshot, Matched: count == 0}, nil
}
if count != 1 {
return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil
}
for _, observed := range snapshot.Rules {
if observed.Marker != marker {
continue
}
positionMatches := rulePlan.Expected.Locator.Position == nil ||
(observed.Locator.Position != nil && *observed.Locator.Position == *rulePlan.Expected.Locator.Position)
semanticMatches := filter.ObservedRuleMatchesExpected(observed, rulePlan.Expected.Rule)
if observed.ParseStatus == filter.ParseStatusOpaque {
semanticMatches = true
}
if !semanticMatches || !positionMatches {
return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil
}
return filter.VerifyResult{Snapshot: snapshot, Matched: true}, nil
}
return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil
}
func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, executed int, cause error) error {
if plan.Operation == filter.ChangeCreate { if plan.Operation == filter.ChangeCreate {
return cause return cause
} }
@@ -662,7 +664,7 @@ func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, ex
return cause return cause
} }
func (a *Adapter) rollback(ctx context.Context, plan filter.NativeRulePlan, executed int) error { func (a *Adapter) rollback(ctx context.Context, plan filter.RuleCommands, executed int) error {
var rollbackErr error var rollbackErr error
for index := executed - 1; index >= 0; index-- { for index := executed - 1; index >= 0; index-- {
if index >= len(plan.RollbackCommands) { if index >= len(plan.RollbackCommands) {
@@ -678,18 +680,7 @@ func (a *Adapter) rollback(ctx context.Context, plan filter.NativeRulePlan, exec
return rollbackErr return rollbackErr
} }
func relatedScopes(scope filter.Scope) []filter.Scope { func countMarker(snapshot filter.RuleSet, marker string) int {
scope = scope.Normalize()
other := scope
if other.Family == filter.FamilyIPv4 {
other.Family = filter.FamilyIPv6
} else {
other.Family = filter.FamilyIPv4
}
return []filter.Scope{other}
}
func countMarker(snapshot filter.Snapshot, marker string) int {
count := 0 count := 0
for _, observed := range snapshot.Rules { for _, observed := range snapshot.Rules {
if observed.Marker == marker { if observed.Marker == marker {
@@ -699,7 +690,7 @@ func countMarker(snapshot filter.Snapshot, marker string) int {
return count return count
} }
func containsObservedRule(snapshot filter.Snapshot, expected filter.ObservedRule) bool { func containsObservedRule(snapshot filter.RuleSet, expected filter.ObservedRule) bool {
for _, observed := range snapshot.Rules { for _, observed := range snapshot.Rules {
if expected.Locator.Position != nil && if expected.Locator.Position != nil &&
(observed.Locator.Position == nil || *observed.Locator.Position != *expected.Locator.Position) { (observed.Locator.Position == nil || *observed.Locator.Position != *expected.Locator.Position) {
@@ -733,7 +724,7 @@ func validateCommand(command filter.NativeCommand) error {
return nil return nil
} }
switch first { switch first {
case "insert", "allow", "deny", "reject": case "insert", "prepend", "allow", "deny", "reject":
return nil return nil
default: default:
return fmt.Errorf("%w: unsupported ufw command %q", filter.ErrInvalidRule, first) return fmt.Errorf("%w: unsupported ufw command %q", filter.ErrInvalidRule, first)
@@ -756,7 +747,7 @@ func parseNumberedRules(scope filter.Scope, output string) []filter.ObservedRule
if err != nil || position < 1 { if err != nil || position < 1 {
continue continue
} }
if !isInboundNumberedRule(matches[4], matches[5]) { if matches[4] == "OUT" || matches[4] == "FWD" || strings.Contains(matches[5], "(out)") {
continue continue
} }
family := familyForNumberedRule(matches[2], matches[5]) family := familyForNumberedRule(matches[2], matches[5])
@@ -774,7 +765,7 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
positionCopy := position positionCopy := position
locator := filter.Locator{ locator := filter.Locator{
Provider: filter.ProviderUFW, ScopeKey: scope.Key(), NativeID: strconv.Itoa(position), Provider: filter.ProviderUFW, ScopeKey: scope.Key(), NativeID: strconv.Itoa(position),
Canonical: normalizedDisplay(raw), Position: &positionCopy, Canonical: strings.Join(strings.Fields(raw), " "), Position: &positionCopy,
} }
ruleAction := map[string]filter.Action{ ruleAction := map[string]filter.Action{
"ALLOW": filter.ActionAccept, "ALLOW": filter.ActionAccept,
@@ -911,7 +902,7 @@ func looksLikeAddressedService(value string) bool {
if len(tokens) < 2 { if len(tokens) < 2 {
return false return false
} }
if isAnywhere(tokens[0]) { if strings.EqualFold(strings.TrimSpace(tokens[0]), "Anywhere") {
return true return true
} }
_, ok := parseAddress(tokens[0]) _, ok := parseAddress(tokens[0])
@@ -981,7 +972,7 @@ func parseUnrecognizedNumberedRule(scope filter.Scope, raw string) (filter.Obser
}, },
Locator: filter.Locator{ Locator: filter.Locator{
Provider: filter.ProviderUFW, ScopeKey: scope.Key(), NativeID: strconv.Itoa(position), Provider: filter.ProviderUFW, ScopeKey: scope.Key(), NativeID: strconv.Itoa(position),
Canonical: normalizedDisplay(raw), Position: &positionCopy, Canonical: strings.Join(strings.Fields(raw), " "), Position: &positionCopy,
}, },
Marker: marker, ParseStatus: filter.ParseStatusOpaque, Raw: raw, Persistence: filter.PersistenceStatusConverged, Marker: marker, ParseStatus: filter.ParseStatusOpaque, Raw: raw, Persistence: filter.PersistenceStatusConverged,
}, family, inbound, true }, family, inbound, true
@@ -1025,7 +1016,7 @@ func parseDestination(value string) (address, port, protocol, iface, annotation
tokens := strings.Fields(value) tokens := strings.Fields(value)
switch len(tokens) { switch len(tokens) {
case 1: case 1:
if isAnywhere(tokens[0]) { if strings.EqualFold(strings.TrimSpace(tokens[0]), "Anywhere") {
return "", "", "all", iface, annotation, true return "", "", "all", iface, annotation, true
} }
if parsedAddress, parsedProtocol, endpointOK := parseAddressProtocol(tokens[0]); endpointOK { if parsedAddress, parsedProtocol, endpointOK := parseAddressProtocol(tokens[0]); endpointOK {
@@ -1057,7 +1048,7 @@ func parseAddressProtocol(value string) (string, string, bool) {
return "", "", false return "", "", false
} }
endpoint := strings.TrimSpace(value[:separator]) endpoint := strings.TrimSpace(value[:separator])
if isAnywhere(endpoint) { if strings.EqualFold(strings.TrimSpace(endpoint), "Anywhere") {
return "", protocol, true return "", protocol, true
} }
address, ok := parseAddress(endpoint) address, ok := parseAddress(endpoint)
@@ -1090,7 +1081,7 @@ func splitDestinationAnnotation(value string) (string, string) {
} }
func parseSource(value string) (string, bool) { func parseSource(value string) (string, bool) {
if isAnywhere(value) { if strings.EqualFold(strings.TrimSpace(value), "Anywhere") {
return "", true return "", true
} }
if strings.Contains(value, " on ") || len(strings.Fields(value)) != 1 { if strings.Contains(value, " on ") || len(strings.Fields(value)) != 1 {
@@ -1099,10 +1090,6 @@ func parseSource(value string) (string, bool) {
return parseAddress(value) return parseAddress(value)
} }
func isInboundNumberedRule(direction, source string) bool {
return direction != "OUT" && direction != "FWD" && !strings.Contains(source, "(out)")
}
func splitInterface(value string) (endpoint, iface string, ok bool) { func splitInterface(value string) (endpoint, iface string, ok bool) {
const delimiter = " on " const delimiter = " on "
index := strings.LastIndex(value, delimiter) index := strings.LastIndex(value, delimiter)
@@ -1157,7 +1144,7 @@ func parsePortProtocol(value string) (string, string, bool) {
} }
func parseAddress(value string) (string, bool) { func parseAddress(value string) (string, bool) {
if isAnywhere(value) { if strings.EqualFold(strings.TrimSpace(value), "Anywhere") {
return "", true return "", true
} }
if prefix, err := netip.ParsePrefix(value); err == nil { if prefix, err := netip.ParsePrefix(value); err == nil {
@@ -1169,10 +1156,6 @@ func parseAddress(value string) (string, bool) {
return "", false return "", false
} }
func isAnywhere(value string) bool {
return strings.EqualFold(strings.TrimSpace(value), "Anywhere")
}
func canonicalRule(rule filter.FirewallRule) string { func canonicalRule(rule filter.FirewallRule) string {
return strings.Join([]string{ return strings.Join([]string{
string(rule.Action), rule.Protocol, rule.SourceAddress, rule.DestinationAddress, string(rule.Action), rule.Protocol, rule.SourceAddress, rule.DestinationAddress,
@@ -1180,10 +1163,6 @@ func canonicalRule(rule filter.FirewallRule) string {
}, "|") }, "|")
} }
func normalizedDisplay(raw string) string {
return strings.Join(strings.Fields(raw), " ")
}
func statusNotices(numbered string) []filter.ScopeNotice { func statusNotices(numbered string) []filter.ScopeNotice {
notices := make([]filter.ScopeNotice, 0, 1) notices := make([]filter.ScopeNotice, 0, 1)
if !statusActive(numbered) { if !statusActive(numbered) {
@@ -1229,6 +1208,10 @@ func IsIPv6Unavailable(err error) bool {
type systemBackend struct{} type systemBackend struct{}
func (systemBackend) ReadRulesByComment(ctx context.Context, _ filter.Scope, comment string) (string, error) {
return filter.ReadRulesByComment(ctx, "ufw", []string{"status", "numbered"}, comment)
}
func (systemBackend) Read(ctx context.Context, args ...string) (string, error) { func (systemBackend) Read(ctx context.Context, args ...string) (string, error) {
return cmd.NewCommandMgr( return cmd.NewCommandMgr(
cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"), cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"),
@@ -1239,7 +1222,14 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
if err := validateCommand(command); err != nil { if err := validateCommand(command); err != nil {
return err return err
} }
return cmd.NewCommandMgr( output, err := cmd.NewCommandMgr(
cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"), cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"),
).RunWithOptionalSudo(command.Executable, command.Args...) ).RunWithOptionalSudoAndStdout(command.Executable, command.Args...)
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))
}
return nil
} }
@@ -1,354 +0,0 @@
package runtime
import (
"context"
"errors"
"fmt"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld"
filteriptables "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/iptables"
filternftables "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/nftables"
filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw"
)
type SnapshotPolicy func(context.Context, filter.Snapshot) (filter.Snapshot, error)
type Engine struct {
adapter filter.Adapter
policy SnapshotPolicy
}
type Registry map[filter.Provider]*Engine
func NewRegistry(policy SnapshotPolicy) Registry {
return Registry{
filter.ProviderIptables: New(filteriptables.NewAdapter(), policy),
filter.ProviderNftables: New(filternftables.NewAdapter(), policy),
filter.ProviderFirewalld: New(filterfirewalld.NewAdapter(), policy),
filter.ProviderUFW: New(filterufw.NewAdapter(), policy),
}
}
func New(adapter filter.Adapter, policy SnapshotPolicy) *Engine {
return &Engine{adapter: adapter, policy: policy}
}
func (r Registry) Resolve(provider filter.Provider) (*Engine, error) {
engine, exists := r[provider]
if !exists || engine == nil || engine.adapter == nil {
return nil, fmt.Errorf("%w: %s", filter.ErrAdapterUnavailable, provider)
}
return engine, nil
}
func (r Registry) Providers() []filter.Provider {
providers := make([]filter.Provider, 0, len(r))
for provider := range r {
providers = append(providers, provider)
}
return providers
}
func (e *Engine) Provider() filter.Provider {
if e == nil || e.adapter == nil {
return ""
}
return e.adapter.Provider()
}
func (e *Engine) Observe(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) {
snapshot, err := e.adapter.Observe(ctx, scope)
if err != nil {
return filter.Snapshot{}, err
}
if e.policy == nil {
return snapshot, nil
}
return e.policy(ctx, snapshot)
}
func (e *Engine) ObserveScopes(ctx context.Context, scopes []filter.Scope) ([]filter.Snapshot, error) {
observer, ok := e.adapter.(filter.MultiScopeObserver)
if !ok {
return nil, fmt.Errorf("%w: %s multi-scope inventory", filter.ErrAdapterUnavailable, e.adapter.Provider())
}
snapshots, err := observer.ObserveScopes(ctx, scopes)
if err != nil {
return nil, err
}
if e.policy == nil {
return snapshots, nil
}
for index := range snapshots {
snapshots[index], err = e.policy(ctx, snapshots[index])
if err != nil {
return nil, err
}
}
return snapshots, nil
}
func (e *Engine) ObserveMutation(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) {
snapshot, err := e.Observe(ctx, scope)
if err != nil {
return filter.Snapshot{}, err
}
for _, notice := range snapshot.Notices {
if notice.Code == filter.ScopeNoticeManagedScopeInactive || notice.Code == filter.ScopeNoticeManagedScopeMissing {
return filter.Snapshot{}, fmt.Errorf("%w: managed firewall scope is unavailable", filter.ErrProviderUnavailable)
}
}
return snapshot, nil
}
func (e *Engine) Prepare(rule filter.FirewallRule) (filter.FirewallRule, error) {
preparer, ok := e.adapter.(filter.RulePreparer)
if !ok {
return rule, nil
}
return preparer.PrepareRule(rule)
}
func (e *Engine) CheckRule(ctx context.Context, rule filter.FirewallRule) error {
checker, ok := e.adapter.(filter.RuleChecker)
if !ok {
return nil
}
return checker.CheckRule(ctx, rule)
}
func (e *Engine) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
appender, ok := e.adapter.(filter.UnverifiedRuleAppender)
if !ok {
return fmt.Errorf("%w: %s does not support unverified rule appends", filter.ErrAdapterUnavailable, e.Provider())
}
return appender.AppendUnverified(ctx, rule, comment)
}
func (e *Engine) CompileDesired(
ctx context.Context,
policyUUID string,
origin filter.RuleOrigin,
rules []filter.FirewallRule,
) ([]filter.DesiredRule, error) {
capabilities, err := e.Capabilities(ctx)
if err != nil {
return nil, err
}
result := make([]filter.DesiredRule, 0, len(rules))
for ordinal, rule := range rules {
prepared, err := e.Prepare(rule)
if err != nil {
return nil, err
}
if err = e.CheckRule(ctx, prepared); err != nil {
return nil, err
}
ruleKey, err := filter.RuleKey(prepared)
if err != nil {
return nil, err
}
prepared.UUID = compiledRuleUUID(policyUUID, ruleKey, ordinal)
desired := filter.DesiredRule{UUID: policyUUID, Rule: prepared, RuleKey: ruleKey, Origin: origin}
if capabilities.Marker {
desired.Marker = "1panel-rule:" + prepared.UUID
}
result = append(result, desired)
}
return result, nil
}
func (e *Engine) ValidatePosition(
ctx context.Context,
snapshot filter.Snapshot,
rule filter.FirewallRule,
target int64,
) error {
if target < 1 {
return fmt.Errorf("%w: target position must be positive", filter.ErrInvalidRule)
}
if rule.Scope.Provider == filter.ProviderUFW {
minimum, maximum := positionBounds(snapshot)
if target < minimum || target > maximum {
return fmt.Errorf(
"%w: target position %d is outside the %s range %d-%d",
filter.ErrInvalidRule, target, rule.Scope.Family, minimum, maximum,
)
}
return nil
}
maximum, err := e.MaxPosition(ctx, snapshot, rule)
if err != nil {
return err
}
if target > maximum {
return fmt.Errorf("%w: target position %d is out of range 1-%d", filter.ErrInvalidRule, target, maximum)
}
return nil
}
func (e *Engine) AppendPosition(ctx context.Context, snapshot filter.Snapshot, rule filter.FirewallRule) (int64, error) {
if rule.Scope.Family == filter.FamilyIPv4 {
return snapshotMaxPosition(snapshot) + 1, nil
}
maximum, err := e.MaxPosition(ctx, snapshot, rule)
if err != nil {
return 0, err
}
return maximum + 1, nil
}
func (e *Engine) MaxPosition(
ctx context.Context,
snapshot filter.Snapshot,
rule filter.FirewallRule,
) (int64, error) {
maximum := snapshotMaxPosition(snapshot)
if rule.Scope.Provider != filter.ProviderUFW {
return maximum, nil
}
relatedScope := rule.Scope
if relatedScope.Family == filter.FamilyIPv4 {
relatedScope.Family = filter.FamilyIPv6
} else {
relatedScope.Family = filter.FamilyIPv4
}
relatedSnapshot, err := e.ObserveMutation(ctx, relatedScope)
if err != nil {
return 0, err
}
if relatedMaximum := snapshotMaxPosition(relatedSnapshot); relatedMaximum > maximum {
maximum = relatedMaximum
}
return maximum, nil
}
func (e *Engine) NativeDetail(ctx context.Context, name string, permanent bool) (string, error) {
reader, ok := e.adapter.(filter.NativeDetailReader)
if !ok {
return "", fmt.Errorf("%w: native details for %s", filter.ErrAdapterUnavailable, e.Provider())
}
return reader.NativeDetail(ctx, name, permanent)
}
func (e *Engine) Capabilities(ctx context.Context) (filter.Capabilities, error) {
return e.adapter.Capabilities(ctx)
}
func (e *Engine) ExecuteCreate(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error {
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return err
}
if !plan.CreatesOnly() {
return fmt.Errorf("%w: expected a create-only plan", filter.ErrInvalidRule)
}
_, err = e.adapter.Apply(ctx, plan)
return err
}
func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error {
if err := ctx.Err(); err != nil {
return err
}
changes = append([]filter.DesiredChange(nil), changes...)
for index := range changes {
changes[index].CommandOnly = true
}
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return err
}
plan.CommandOnly = true
_, err = e.adapter.Apply(ctx, plan)
return err
}
func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, filter.VerifyResult, error) {
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return filter.BackendPlan{}, filter.VerifyResult{}, err
}
result, err := e.adapter.Apply(ctx, plan)
if err != nil {
return plan, filter.VerifyResult{}, err
}
if result.Verification != nil {
if !result.Verification.Matched && !plan.CreatesOnly() {
if rollbackErr := e.Rollback(ctx, plan); rollbackErr != nil {
return plan, *result.Verification, errors.Join(filter.ErrVerificationFailed, rollbackErr)
}
}
return plan, *result.Verification, nil
}
verification, err := e.adapter.Verify(ctx, plan)
if err != nil {
if plan.CreatesOnly() {
return plan, verification, err
}
return plan, verification, e.rollback(ctx, plan, err)
}
if !verification.Matched && !plan.CreatesOnly() {
if rollbackErr := e.Rollback(ctx, plan); rollbackErr != nil {
return plan, verification, errors.Join(filter.ErrVerificationFailed, rollbackErr)
}
}
return plan, verification, nil
}
func (e *Engine) Rollback(ctx context.Context, plan filter.BackendPlan) error {
rollbacker, ok := e.adapter.(filter.PlanRollbacker)
if !ok {
return fmt.Errorf("%w: provider %s does not support applied-plan rollback", filter.ErrAdapterUnavailable, e.adapter.Provider())
}
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
return rollbacker.Rollback(ctx, plan)
}
func (e *Engine) rollback(ctx context.Context, plan filter.BackendPlan, cause error) error {
if err := e.Rollback(ctx, plan); err != nil {
return errors.Join(cause, fmt.Errorf("rollback applied firewall plan: %w", err))
}
return cause
}
func positionBounds(snapshot filter.Snapshot) (int64, int64) {
minimum, maximum := int64(0), int64(0)
for _, observed := range snapshot.Rules {
if observed.Locator.Position == nil {
continue
}
position := int64(*observed.Locator.Position)
if minimum == 0 || position < minimum {
minimum = position
}
if position > maximum {
maximum = position
}
}
return minimum, maximum
}
func snapshotMaxPosition(snapshot filter.Snapshot) int64 {
var maximum int64
for _, observed := range snapshot.Rules {
if observed.Locator.Position != nil && int64(*observed.Locator.Position) > maximum {
maximum = int64(*observed.Locator.Position)
}
}
return maximum
}
func compiledRuleUUID(policyUUID, ruleKey string, scopeOrdinal int) string {
if scopeOrdinal == 0 {
return policyUUID
}
const suffixLength = 12
if len(ruleKey) > suffixLength {
ruleKey = ruleKey[:suffixLength]
}
return fmt.Sprintf("%s-%d-%s", policyUUID, scopeOrdinal+1, ruleKey)
}
+95 -90
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
@@ -16,45 +17,107 @@ var (
var ErrVerificationFailed = errors.New("firewall rule verification failed") var ErrVerificationFailed = errors.New("firewall rule verification failed")
func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error) { func ProtectRuleSet(snapshot RuleSet, ports []PortWhitelist) (RuleSet, error) {
rules := append([]ObservedRule(nil), snapshot.Rules...) rules := slices.Clone(snapshot.Rules)
whitelist := NewPortWhitelistIndex(ports)
for index := range rules { for index := range rules {
rule := rules[index].Rule if rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule) {
if rules[index].ParseStatus != ParseStatusSupported || rule.Action != ActionAccept ||
rule.SourceAddress != "" || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" {
continue
}
protected := false
for _, protectedPort := range ports {
family := strings.ToLower(strings.TrimSpace(protectedPort.Family))
if family != "" && rule.Scope.Family != FamilyInet && string(rule.Scope.Family) != family {
continue
}
if rule.Protocol != "all" && rule.Protocol != protectedPort.Protocol {
continue
}
if portCovers(rule.DestinationPort, protectedPort.Port) {
protected = true
break
}
}
if protected {
rules[index].Protected = true rules[index].Protected = true
} }
} }
protected := snapshot protected := snapshot
protected.Rules = rules protected.Rules = rules
if protected.Revision == "" {
var err error protected.LastPosition = snapshot.LastPosition
protected, err = NewSnapshot(snapshot.Scope, rules)
if err != nil {
return Snapshot{}, err
}
}
protected.Notices = append([]ScopeNotice(nil), snapshot.Notices...) protected.Notices = append([]ScopeNotice(nil), snapshot.Notices...)
return protected, nil return protected, nil
} }
type portWhitelistKey struct {
family Family
protocol, port, source string
}
type PortWhitelistIndex map[portWhitelistKey]bool
func NewPortWhitelistIndex(ports []PortWhitelist) PortWhitelistIndex {
index := make(PortWhitelistIndex)
for _, port := range ports {
protocol, err := normalizeProtocol(port.Protocol)
if err != nil {
continue
}
portRange, err := normalizePortValue(port.Port, false)
if err != nil {
continue
}
portFamily := Family(strings.ToLower(strings.TrimSpace(port.Family)))
sources := port.Sources
if len(sources) == 0 {
sources = []string{""}
}
for _, family := range []Family{FamilyIPv4, FamilyIPv6} {
if portFamily != "" && portFamily != FamilyInet && portFamily != family {
continue
}
for _, source := range sources {
normalized, err := normalizeAddress(source, family)
if err == nil {
index[portWhitelistKey{family, protocol, portRange, normalized}] = true
}
}
}
}
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}
}
for _, family := range families {
if !index[portWhitelistKey{family, rule.Protocol, rule.DestinationPort, rule.SourceAddress}] {
return false
}
}
return true
}
func IsBuiltinProtectedRule(rule FirewallRule) bool {
if rule.Scope.Provider != ProviderIptables && rule.Scope.Provider != ProviderNftables {
return false
}
rule, err := NormalizeRule(rule)
if err != nil || rule.SourceAddress != "" || rule.DestinationAddress != "" || rule.SourcePort != "" || rule.DestinationPort != "" {
return false
}
switch rule.Scope.Chain {
case BasicBeforeChain:
if rule.Action != ActionAccept || rule.Protocol != "all" {
return false
}
return rule.Interface == "lo" && len(rule.ConnectionStates) == 0 ||
rule.Interface == "" && slices.Equal(rule.ConnectionStates, []string{"established", "related"})
case BasicAfterChain:
return rule.Action == ActionDrop && (rule.Protocol == "tcp" || rule.Protocol == "udp") &&
rule.Interface == "" && len(rule.ConnectionStates) == 0
}
return false
}
func GuardMutation(target ObservedRule) error { func GuardMutation(target ObservedRule) error {
if target.Protected { if target.Protected {
return ErrProtectedRule return ErrProtectedRule
@@ -99,27 +162,7 @@ func MatchObservedByRuleKey(observed []ObservedRule, rule FirewallRule) ([]Obser
return matches, nil return matches, nil
} }
func ManagedObserved(snapshot Snapshot, desired DesiredRule) (ObservedRule, error) { func FindCommittedObserved(snapshot RuleSet, requested FirewallRule, plan CommandBatch) (ObservedRule, error) {
items, err := MergeInventory(InventoryMergeInput{Observed: snapshot.Rules, Desired: []DesiredRule{desired}})
if err != nil {
return ObservedRule{}, err
}
for _, item := range items {
if item.Desired == nil || item.Desired.UUID != desired.UUID {
continue
}
if item.State == InventoryStateProtected || (item.Observed != nil && item.Observed.Protected) {
return ObservedRule{}, ErrProtectedRule
}
if item.Observed == nil || item.Match != InventoryMatchExact || item.State == InventoryStateDrifted {
return ObservedRule{}, ErrRuleStale
}
return *item.Observed, nil
}
return ObservedRule{}, ErrRuleStale
}
func FindCommittedObserved(snapshot Snapshot, requested FirewallRule, plan BackendPlan) (ObservedRule, error) {
if len(plan.Rules) == 1 && plan.Rules[0].Expected.Marker != "" { if len(plan.Rules) == 1 && plan.Rules[0].Expected.Marker != "" {
matches := make([]ObservedRule, 0, 1) matches := make([]ObservedRule, 0, 1)
for _, observed := range snapshot.Rules { for _, observed := range snapshot.Rules {
@@ -147,8 +190,8 @@ func RulesOverlap(left, right FirewallRule) bool {
if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() { if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() {
return false return false
} }
return familiesOverlap(left.Scope.Family, right.Scope.Family) && return (left.Scope.Family == FamilyInet || right.Scope.Family == FamilyInet || left.Scope.Family == right.Scope.Family) &&
protocolsOverlap(left.Protocol, right.Protocol) && (left.Protocol == "all" || right.Protocol == "all" || left.Protocol == right.Protocol) &&
addressesOverlap(left.SourceAddress, right.SourceAddress) && addressesOverlap(left.SourceAddress, right.SourceAddress) &&
addressesOverlap(left.DestinationAddress, right.DestinationAddress) && addressesOverlap(left.DestinationAddress, right.DestinationAddress) &&
portsOverlap(left.SourcePort, right.SourcePort) && portsOverlap(left.SourcePort, right.SourcePort) &&
@@ -156,14 +199,6 @@ func RulesOverlap(left, right FirewallRule) bool {
(left.Interface == "" || right.Interface == "" || left.Interface == right.Interface) (left.Interface == "" || right.Interface == "" || left.Interface == right.Interface)
} }
func familiesOverlap(left, right Family) bool {
return left == FamilyInet || right == FamilyInet || left == right
}
func protocolsOverlap(left, right string) bool {
return left == "all" || right == "all" || left == right
}
func addressesOverlap(left, right string) bool { func addressesOverlap(left, right string) bool {
if left == "" || right == "" { if left == "" || right == "" {
return true return true
@@ -176,36 +211,6 @@ func addressesOverlap(left, right string) bool {
return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr()) return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr())
} }
func portCovers(existing, requested string) bool {
if existing == "" {
return true
}
if requested == "" {
return false
}
existingIntervals, err := portIntervals(existing)
if err != nil {
return false
}
requestedIntervals, err := portIntervals(requested)
if err != nil {
return false
}
for _, requestedInterval := range requestedIntervals {
covered := false
for _, existingInterval := range existingIntervals {
if existingInterval[0] <= requestedInterval[0] && existingInterval[1] >= requestedInterval[1] {
covered = true
break
}
}
if !covered {
return false
}
}
return true
}
func portsOverlap(left, right string) bool { func portsOverlap(left, right string) bool {
if left == "" || right == "" { if left == "" || right == "" {
return true return true
+9 -86
View File
@@ -1,30 +1,25 @@
package forwarding package forwarding
import ( import (
"errors" "context"
"fmt" "fmt"
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/re" "github.com/1Panel-dev/1Panel/agent/utils/re"
) )
var ErrRuleExists = errors.New("forwarding rule already exists")
const ( const (
FamilyIPv4 = constant.FirewallFamilyIPv4 FamilyIPv4 = constant.FirewallFamilyIPv4
FamilyIPv6 = constant.FirewallFamilyIPv6 FamilyIPv6 = constant.FirewallFamilyIPv6
ChainPreRouting = "1PANEL_PREROUTING" ChainPreRouting = "1PANEL_PREROUTING"
ChainPostRouting = "1PANEL_POSTROUTING" ChainPostRouting = "1PANEL_POSTROUTING"
ChainForward = "1PANEL_FORWARD" ChainForward = "1PANEL_FORWARD"
ForwardFile = "1panel_forward.rules"
ForwardFile = "1panel_forward.rules" PreRoutingFile = "1panel_forward_pre.rules"
PreRoutingFile = "1panel_forward_pre.rules" PostRoutingFile = "1panel_forward_post.rules"
PostRoutingFile = "1panel_forward_post.rules"
) )
type Rule struct { type Rule struct {
@@ -51,7 +46,9 @@ const (
type Adapter interface { type Adapter interface {
Name() string Name() string
List() ([]Rule, error) List() ([]Rule, error)
Reconcile(rules []Rule) error CreateRules(context.Context, []Rule) error
DeleteRules(context.Context, []Rule) error
ReplaceRules(rules []Rule) error
Enable() error Enable() error
Cleanup() error Cleanup() error
InitStatus() (bool, bool, error) InitStatus() (bool, bool, error)
@@ -59,80 +56,6 @@ type Adapter interface {
Replay() error Replay() error
} }
type Status struct {
Name string
Version string
IsInit bool
IsBind bool
}
type RuntimeClient interface {
Version() (string, error)
}
type Manager struct {
adapter Adapter
runtime RuntimeClient
}
func NewManager(adapter Adapter, runtime RuntimeClient) *Manager {
return &Manager{adapter: adapter, runtime: runtime}
}
func (m *Manager) Status() (Status, error) {
status := Status{Name: m.adapter.Name(), Version: "-"}
var versionErr error
var initErr error
var wg sync.WaitGroup
wg.Add(1)
if m.runtime != nil {
wg.Add(1)
go func() {
defer wg.Done()
status.Version, versionErr = m.runtime.Version()
}()
}
go func() {
defer wg.Done()
status.IsInit, status.IsBind, initErr = m.adapter.InitStatus()
}()
wg.Wait()
return status, errors.Join(versionErr, initErr)
}
func (m *Manager) List(info, strategy string) ([]Rule, error) {
rules, err := m.adapter.List()
if err != nil {
return nil, err
}
if strategy != "" {
return []Rule{}, nil
}
filtered := make([]Rule, 0, len(rules))
for _, rule := range rules {
if info != "" && !strings.Contains(rule.Port, info) &&
!strings.Contains(rule.TargetPort, info) && !strings.Contains(rule.TargetIP, info) {
continue
}
filtered = append(filtered, rule)
}
return filtered, nil
}
func (m *Manager) Enable() error { return m.adapter.Enable() }
func (m *Manager) Reconcile(rules []Rule) error { return m.adapter.Reconcile(rules) }
func (m *Manager) Cleanup() error { return m.adapter.Cleanup() }
func (m *Manager) FamilyStatus(family string) (bool, bool, error) {
return m.adapter.FamilyStatus(family)
}
func (m *Manager) Replay() error { return m.adapter.Replay() }
func (m *Manager) Name() string { return m.adapter.Name() }
func NormalizeRule(rule Rule) (Rule, error) { func NormalizeRule(rule Rule) (Rule, error) {
rule.Family = strings.ToLower(strings.TrimSpace(rule.Family)) rule.Family = strings.ToLower(strings.TrimSpace(rule.Family))
if rule.Family == "" { if rule.Family == "" {
@@ -1,4 +1,4 @@
package providers package forwarding
import ( import (
"context" "context"
@@ -13,7 +13,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall" firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
@@ -25,7 +24,7 @@ type iptablesBackend interface {
RunWithStd(table string, args ...string) (string, error) RunWithStd(table string, args ...string) (string, error)
RunIPv6(table string, args ...string) error RunIPv6(table string, args ...string) error
RunIPv6WithStd(table string, args ...string) (string, error) RunIPv6WithStd(table string, args ...string) (string, error)
Restore(family, input string) error Restore(ctx context.Context, family, input string) error
LoadRulesFromFile(table, chain, fileName string) error LoadRulesFromFile(table, chain, fileName string) error
LoadIPv6RulesFromFile(table, chain, fileName string) error LoadIPv6RulesFromFile(table, chain, fileName string) error
} }
@@ -59,20 +58,24 @@ func (systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (strin
return iptables_helper.RunIPv6WithStd(table, args...) return iptables_helper.RunIPv6WithStd(table, args...)
} }
func (systemIptablesBackend) Restore(family, input string) error { func (systemIptablesBackend) Restore(ctx context.Context, family, input string) error {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
return err return err
} }
executable := commands.Restore4 executable := commands.Restore4
if family == forwarding.FamilyIPv6 { if family == FamilyIPv6 {
executable = commands.Restore6 executable = commands.Restore6
if executable == "" { if executable == "" {
return fmt.Errorf("ip6tables-restore command family is unavailable") return fmt.Errorf("ip6tables-restore command family is unavailable")
} }
} }
manager := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input))) var stderr strings.Builder
manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input)), cmd.WithStderr(&stderr))
err = manager.RunWithOptionalSudo(executable, "--noflush", "--wait") err = manager.RunWithOptionalSudo(executable, "--noflush", "--wait")
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 firewallutil.WrapBatchCommandError(executable+" --noflush --wait", input, err)
} }
@@ -87,7 +90,6 @@ func (systemIptablesBackend) LoadIPv6RulesFromFile(table, chain, fileName string
type forwardingSystem interface { type forwardingSystem interface {
ReadFile(name string) ([]byte, error) ReadFile(name string) ([]byte, error)
WriteFile(name string, data []byte, perm os.FileMode) error WriteFile(name string, data []byte, perm os.FileMode) error
RunWithOptionalSudo(name string, args ...string) error
} }
type defaultForwardingSystem struct{} type defaultForwardingSystem struct{}
@@ -100,34 +102,30 @@ func (defaultForwardingSystem) WriteFile(name string, data []byte, perm os.FileM
return cmd.WriteFileWithOptionalSudo(name, data, perm) return cmd.WriteFileWithOptionalSudo(name, data, perm)
} }
func (defaultForwardingSystem) RunWithOptionalSudo(name string, args ...string) error { type Iptables struct {
return cmd.NewCommandMgr().RunWithOptionalSudo(name, args...)
}
type iptablesNATAdapter struct {
provider string provider string
backend iptablesBackend backend iptablesBackend
system forwardingSystem system forwardingSystem
} }
func newIptablesNATAdapter(provider string) *iptablesNATAdapter { func NewIptables(provider string) *Iptables {
return &iptablesNATAdapter{ return &Iptables{
provider: provider, provider: provider,
backend: systemIptablesBackend{}, backend: systemIptablesBackend{},
system: defaultForwardingSystem{}, system: defaultForwardingSystem{},
} }
} }
func (l *iptablesNATAdapter) Name() string { func (l *Iptables) Name() string {
return l.provider return l.provider
} }
func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) { func (l *Iptables) List() ([]Rule, error) {
stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-S") stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-S")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list NAT rules: %w", err) return nil, fmt.Errorf("failed to list NAT rules: %w", err)
} }
rules := parseIptablesRules(stdout, forwarding.FamilyIPv4) rules := parseIptablesRules(stdout, FamilyIPv4)
if !l.backend.IPv6Available() { if !l.backend.IPv6Available() {
return rules, nil return rules, nil
} }
@@ -135,49 +133,127 @@ func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err) return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err)
} }
return append(rules, parseIptablesRules(stdout, forwarding.FamilyIPv6)...), nil return append(rules, parseIptablesRules(stdout, FamilyIPv6)...), nil
} }
func (l *iptablesNATAdapter) Reconcile(rules []forwarding.Rule) error { func (l *Iptables) ReplaceRules(rules []Rule) error {
byFamily := map[string][]forwarding.Rule{ byFamily := map[string][]Rule{
forwarding.FamilyIPv4: nil, FamilyIPv4: nil,
forwarding.FamilyIPv6: nil, FamilyIPv6: nil,
} }
for _, rule := range rules { for _, rule := range rules {
normalized, err := forwarding.NormalizeRule(rule) normalized, err := NormalizeRule(rule)
if err != nil { if err != nil {
return err return err
} }
if normalized.Family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
return fmt.Errorf("ip6tables command family is unavailable")
}
byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized) byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized)
} }
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { var failures []error
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == FamilyIPv6 && !l.backend.IPv6Available() {
if len(byFamily[family]) > 0 {
failures = append(failures, fmt.Errorf("ip6tables command family is unavailable"))
}
continue 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
}
}
}
if err := l.batchEnsureChains(family); err != nil { if err := l.batchEnsureChains(family); err != nil {
return err return err
} }
script, err := buildIptablesForwardRestoreScript(byFamily[family]) script, err := buildIptablesForwardScript(byFamily[family], OperationAdd, true)
if err != nil { if err != nil {
return err return err
} }
if err := l.backend.Restore(family, script); err != nil { if err := l.backend.Restore(context.Background(), family, script); err != nil {
return fmt.Errorf("restore %s forwarding rules: %w", family, err) return fmt.Errorf("restore %s forwarding rules: %w", family, err)
} }
} }
return nil return errors.Join(failures...)
} }
func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error) { func (l *Iptables) CreateRules(ctx context.Context, rules []Rule) error {
natRules := [][]string{{"-F", forwarding.ChainPreRouting}, {"-F", forwarding.ChainPostRouting}} if len(rules) == 0 {
filterRules := [][]string{{"-F", forwarding.ChainForward}} return nil
}
script, err := buildIptablesForwardScript(rules, OperationAdd, false)
if err != nil {
return err
}
family := rules[0].Family
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)
}
func (l *Iptables) DeleteRules(ctx context.Context, rules []Rule) error {
if len(rules) == 0 {
return nil
}
script, err := buildIptablesForwardScript(rules, OperationRemove, false)
if err != nil {
return err
}
family := rules[0].Family
if family == "" {
family = FamilyIPv4
}
return l.backend.Restore(ctx, family, script)
}
func buildIptablesForwardScript(rules []Rule, operation OperationType, replace bool) (string, error) {
var natRules, filterRules [][]string
if replace {
natRules = [][]string{{"-F", ChainPreRouting}, {"-F", ChainPostRouting}}
filterRules = [][]string{{"-F", ChainForward}}
}
verb := "-A"
if operation == OperationRemove {
verb = "-D"
} else if operation != OperationAdd {
return "", fmt.Errorf("unsupported forwarding operation %q", operation)
}
family := ""
for _, rule := range rules { for _, rule := range rules {
normalized, err := NormalizeRule(rule)
if err != nil {
return "", err
}
rule = normalized
if family != "" && family != rule.Family {
return "", fmt.Errorf("iptables forwarding batch must use one address family")
}
family = rule.Family
sourcePort := strings.ReplaceAll(rule.Port, "-", ":") sourcePort := strings.ReplaceAll(rule.Port, "-", ":")
targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":") targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":")
preRouting := []string{"-A", forwarding.ChainPreRouting} preRouting := []string{verb, ChainPreRouting}
if rule.Interface != "" { if rule.Interface != "" {
preRouting = append(preRouting, "-i", rule.Interface) preRouting = append(preRouting, "-i", rule.Interface)
} }
@@ -188,11 +264,11 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error)
} }
natRules = append(natRules, natRules = append(natRules,
append(preRouting, "-j", "DNAT", "--to-destination", forwardingTarget(rule)), append(preRouting, "-j", "DNAT", "--to-destination", forwardingTarget(rule)),
[]string{"-A", forwarding.ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"}, []string{verb, ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"},
) )
filterRules = append(filterRules, filterRules = append(filterRules,
[]string{"-A", forwarding.ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"}, []string{verb, ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"},
[]string{"-A", forwarding.ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"}, []string{verb, ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"},
) )
} }
var script strings.Builder var script strings.Builder
@@ -220,39 +296,30 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error)
return script.String(), nil return script.String(), nil
} }
func forwardingTarget(rule forwarding.Rule) string { func forwardingTarget(rule Rule) string {
if rule.Family == forwarding.FamilyIPv6 { if rule.Family == FamilyIPv6 {
return "[" + rule.TargetIP + "]:" + rule.TargetPort return "[" + rule.TargetIP + "]:" + rule.TargetPort
} }
return rule.TargetIP + ":" + rule.TargetPort return rule.TargetIP + ":" + rule.TargetPort
} }
func isRemoteTarget(family, target string) bool { func isRemoteTarget(family, target string) bool {
if family == forwarding.FamilyIPv6 { if family == FamilyIPv6 {
return target != "" && target != "::1" && target != "localhost" return target != "" && target != "::1" && target != "localhost"
} }
return target != "" && target != "127.0.0.1" && target != "localhost" return target != "" && target != "127.0.0.1" && target != "localhost"
} }
func (l *iptablesNATAdapter) Enable() error { func (l *Iptables) Enable() error {
if err := ensureForwardingSysctls(l.system, l.backend.IPv6Available()); err != nil { if err := ensureForwardingSysctls(l.system, false); err != nil {
return err return err
} }
return l.batchEnsureChains(FamilyIPv4)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
continue
}
if err := l.batchEnsureChains(family); err != nil {
return err
}
}
return nil
} }
func (l *iptablesNATAdapter) batchEnsureChains(family string) error { func (l *Iptables) batchEnsureChains(family string) error {
list := l.backend.RunWithStd list := l.backend.RunWithStd
if family == forwarding.FamilyIPv6 { if family == FamilyIPv6 {
list = l.backend.RunIPv6WithStd list = l.backend.RunIPv6WithStd
} }
outputs := make(map[string]string, 2) outputs := make(map[string]string, 2)
@@ -267,19 +334,19 @@ func (l *iptablesNATAdapter) batchEnsureChains(family string) error {
if script == "" { if script == "" {
return nil return nil
} }
if err := l.backend.Restore(family, script); err != nil { if err := l.backend.Restore(context.Background(), family, script); err != nil {
return fmt.Errorf("batch initialize %s forwarding chains: %w", family, err) return fmt.Errorf("batch initialize %s forwarding chains: %w", family, err)
} }
return nil return nil
} }
func (l *iptablesNATAdapter) Cleanup() error { func (l *Iptables) Cleanup() error {
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { if family == FamilyIPv6 && !l.backend.IPv6Available() {
continue continue
} }
list := l.backend.RunWithStd list := l.backend.RunWithStd
if family == forwarding.FamilyIPv6 { if family == FamilyIPv6 {
list = l.backend.RunIPv6WithStd list = l.backend.RunIPv6WithStd
} }
outputs := make(map[string]string, 2) outputs := make(map[string]string, 2)
@@ -292,13 +359,13 @@ func (l *iptablesNATAdapter) Cleanup() error {
} }
script := buildIptablesForwardLifecycleScript(outputs, false) script := buildIptablesForwardLifecycleScript(outputs, false)
if script != "" { if script != "" {
if err := l.backend.Restore(family, script); err != nil { if err := l.backend.Restore(context.Background(), family, script); err != nil {
return fmt.Errorf("batch delete %s forwarding chains: %w", family, err) return fmt.Errorf("batch delete %s forwarding chains: %w", family, err)
} }
} }
} }
for _, file := range []string{forwarding.ForwardFile, forwarding.PreRoutingFile, forwarding.PostRoutingFile, for _, file := range []string{ForwardFile, PreRoutingFile, PostRoutingFile,
iptables_helper.IPv6FileName(forwarding.ForwardFile), iptables_helper.IPv6FileName(forwarding.PreRoutingFile), iptables_helper.IPv6FileName(forwarding.PostRoutingFile)} { iptables_helper.IPv6FileName(ForwardFile), iptables_helper.IPv6FileName(PreRoutingFile), iptables_helper.IPv6FileName(PostRoutingFile)} {
if err := os.Remove(filepath.Join(global.Dir.FirewallDir, file)); err != nil && !errors.Is(err, os.ErrNotExist) { if err := os.Remove(filepath.Join(global.Dir.FirewallDir, file)); err != nil && !errors.Is(err, os.ErrNotExist) {
return err return err
} }
@@ -308,8 +375,8 @@ func (l *iptablesNATAdapter) Cleanup() error {
func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool) string { func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool) string {
items := []struct{ table, parent, chain string }{ items := []struct{ table, parent, chain string }{
{iptables_helper.NatTab, "PREROUTING", forwarding.ChainPreRouting}, {iptables_helper.NatTab, "PREROUTING", ChainPreRouting},
{iptables_helper.NatTab, "POSTROUTING", forwarding.ChainPostRouting}, {iptables_helper.NatTab, "POSTROUTING", ChainPostRouting},
} }
byTable := make(map[string][]string, 2) byTable := make(map[string][]string, 2)
for _, item := range items { for _, item := range items {
@@ -335,25 +402,25 @@ func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool)
} }
filterOutput := outputs[iptables_helper.FilterTab] filterOutput := outputs[iptables_helper.FilterTab]
filterChainExists := containsExactLine(filterOutput, "-N "+forwarding.ChainForward) filterChainExists := containsExactLine(filterOutput, "-N "+ChainForward)
filterBinding := "-A FORWARD -j " + forwarding.ChainForward filterBinding := "-A FORWARD -j " + ChainForward
filterBindingCount := countExactLines(filterOutput, filterBinding) filterBindingCount := countExactLines(filterOutput, filterBinding)
if create { if create {
if !filterChainExists { if !filterChainExists {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-N "+forwarding.ChainForward) byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-N "+ChainForward)
} }
if !forwardBindingEffective(filterOutput) { if !forwardBindingEffective(filterOutput) {
for range filterBindingCount { for range filterBindingCount {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+forwarding.ChainForward) byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward)
} }
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], canonicalForwardBindingRule(filterOutput)) byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], canonicalForwardBindingRule(filterOutput))
} }
} else { } else {
for range filterBindingCount { for range filterBindingCount {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+forwarding.ChainForward) byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward)
} }
if filterChainExists { if filterChainExists {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-F "+forwarding.ChainForward, "-X "+forwarding.ChainForward) byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-F "+ChainForward, "-X "+ChainForward)
} }
} }
var script strings.Builder var script strings.Builder
@@ -382,7 +449,7 @@ func countExactLines(output, want string) int {
} }
func forwardBindingEffective(output string) bool { func forwardBindingEffective(output string) bool {
binding := "-A FORWARD -j " + forwarding.ChainForward binding := "-A FORWARD -j " + ChainForward
bindingPosition := 0 bindingPosition := 0
terminalPosition := 0 terminalPosition := 0
position := 0 position := 0
@@ -405,7 +472,7 @@ func forwardBindingEffective(output string) bool {
} }
func canonicalForwardBindingRule(output string) string { func canonicalForwardBindingRule(output string) string {
binding := "-A FORWARD -j " + forwarding.ChainForward binding := "-A FORWARD -j " + ChainForward
position := 1 position := 1
for _, line := range strings.Split(output, "\n") { for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
@@ -413,11 +480,11 @@ func canonicalForwardBindingRule(output string) string {
continue continue
} }
if isUnconditionalForwardTerminal(line) { if isUnconditionalForwardTerminal(line) {
return fmt.Sprintf("-I FORWARD %d -j %s", position, forwarding.ChainForward) return fmt.Sprintf("-I FORWARD %d -j %s", position, ChainForward)
} }
position++ position++
} }
return "-A FORWARD -j " + forwarding.ChainForward return "-A FORWARD -j " + ChainForward
} }
func isUnconditionalForwardTerminal(line string) bool { func isUnconditionalForwardTerminal(line string) bool {
@@ -442,31 +509,34 @@ func containsExactLine(output, want string) bool {
return false return false
} }
func (l *iptablesNATAdapter) InitStatus() (bool, bool, error) { func (l *Iptables) InitStatus() (bool, bool, error) {
ipv4Init, ipv4Bind, err := l.familyInitStatus(forwarding.FamilyIPv4) ipv4Init, ipv4Bind, err := l.familyInitStatus(FamilyIPv4)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
if !l.backend.IPv6Available() { if !l.backend.IPv6Available() {
return ipv4Init, ipv4Bind, nil return ipv4Init, ipv4Bind, nil
} }
ipv6Init, ipv6Bind, err := l.familyInitStatus(forwarding.FamilyIPv6) ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
return ipv4Init && ipv6Init, ipv4Bind && ipv6Bind, nil return ipv4Init || ipv6Init, ipv4Bind || ipv6Bind, nil
} }
func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error) { func (l *Iptables) familyInitStatus(family string) (bool, bool, error) {
sysctlPath := "/proc/sys/net/ipv4/ip_forward" sysctlPath := "/proc/sys/net/ipv4/ip_forward"
label := "IPv4" label := "IPv4"
list := l.backend.RunWithStd list := l.backend.RunWithStd
if family == forwarding.FamilyIPv6 { if family == FamilyIPv6 {
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding" sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
label = "IPv6" label = "IPv6"
list = l.backend.RunIPv6WithStd list = l.backend.RunIPv6WithStd
} }
data, err := l.system.ReadFile(sysctlPath) data, err := l.system.ReadFile(sysctlPath)
if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) {
return false, false, nil
}
if err != nil { if err != nil {
return false, false, fmt.Errorf("read %s forwarding status: %w", label, err) return false, false, fmt.Errorf("read %s forwarding status: %w", label, err)
} }
@@ -476,8 +546,8 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
return false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err) return false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err)
} }
natInit, natBind := checkInitAndBind( natInit, natBind := checkInitAndBind(
[]string{"-N " + forwarding.ChainPreRouting, "-N " + forwarding.ChainPostRouting}, []string{"-N " + ChainPreRouting, "-N " + ChainPostRouting},
[]string{"-A PREROUTING -j " + forwarding.ChainPreRouting, "-A POSTROUTING -j " + forwarding.ChainPostRouting}, []string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting},
strings.Split(natRules, "\n"), strings.Split(natRules, "\n"),
) )
if !natInit { if !natInit {
@@ -488,7 +558,7 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
return false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err) return false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err)
} }
filterInit, _ := checkInitAndBind( filterInit, _ := checkInitAndBind(
[]string{"-N " + forwarding.ChainForward}, []string{"-N " + ChainForward},
nil, nil,
strings.Split(filterRules, "\n"), strings.Split(filterRules, "\n"),
) )
@@ -496,8 +566,8 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
return natInit && filterInit, forwardingEnabled && natBind && filterInit && filterBind, nil return natInit && filterInit, forwardingEnabled && natBind && filterInit && filterBind, nil
} }
func (l *iptablesNATAdapter) FamilyStatus(family string) (bool, bool, error) { func (l *Iptables) FamilyStatus(family string) (bool, bool, error) {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { if family == FamilyIPv6 && !l.backend.IPv6Available() {
return false, false, nil return false, false, nil
} }
return l.familyInitStatus(family) return l.familyInitStatus(family)
@@ -526,9 +596,9 @@ func containsExactRule(lines []string, rule string) bool {
return false return false
} }
func (l *iptablesNATAdapter) Replay() error { func (l *Iptables) Replay() error {
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { if family == FamilyIPv6 && !l.backend.IPv6Available() {
continue continue
} }
if err := l.batchEnsureChains(family); err != nil { if err := l.batchEnsureChains(family); err != nil {
@@ -540,9 +610,9 @@ func (l *iptablesNATAdapter) Replay() error {
chain string chain string
file string file string
}{ }{
{iptables_helper.FilterTab, forwarding.ChainForward, forwarding.ForwardFile}, {iptables_helper.FilterTab, ChainForward, ForwardFile},
{iptables_helper.NatTab, forwarding.ChainPreRouting, forwarding.PreRoutingFile}, {iptables_helper.NatTab, ChainPreRouting, PreRoutingFile},
{iptables_helper.NatTab, forwarding.ChainPostRouting, forwarding.PostRoutingFile}, {iptables_helper.NatTab, ChainPostRouting, PostRoutingFile},
} { } {
if err := l.backend.LoadRulesFromFile(item.table, item.chain, item.file); err != nil { if err := l.backend.LoadRulesFromFile(item.table, item.chain, item.file); err != nil {
return err return err
@@ -556,17 +626,17 @@ func (l *iptablesNATAdapter) Replay() error {
return nil return nil
} }
func parseIptablesRules(stdout, family string) []forwarding.Rule { func parseIptablesRules(stdout, family string) []Rule {
var rules []forwarding.Rule var rules []Rule
num := 0 num := 0
lines: lines:
for _, line := range strings.Split(stdout, "\n") { for _, line := range strings.Split(stdout, "\n") {
fields, err := shellwords.Parse(line) fields, err := shellwords.Parse(line)
if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != forwarding.ChainPreRouting { if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != ChainPreRouting {
continue continue
} }
num++ num++
rule := forwarding.Rule{Num: strconv.Itoa(num), Family: family} rule := Rule{Num: strconv.Itoa(num), Family: family}
target := "" target := ""
for index := 2; index < len(fields); index++ { for index := 2; index < len(fields); index++ {
var value *string var value *string
@@ -609,7 +679,7 @@ lines:
switch target { switch target {
case "REDIRECT": case "REDIRECT":
rule.TargetIP = "127.0.0.1" rule.TargetIP = "127.0.0.1"
if family == forwarding.FamilyIPv6 { if family == FamilyIPv6 {
rule.TargetIP = "::1" rule.TargetIP = "::1"
} }
case "DNAT": case "DNAT":
+469
View File
@@ -0,0 +1,469 @@
package forwarding
import (
"context"
"encoding/base64"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"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/nftables_helper"
)
const (
nftForwardFamily = "ip"
nftForwardTable = "nft_1panel_forward"
nftForwardFile = "1panel_forward.nft"
nftForwardMarker = "1panel-forward:"
)
type Nftables struct{ system forwardingSystem }
func NewNftables() *Nftables {
return &Nftables{system: defaultForwardingSystem{}}
}
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) {
continue
}
if err != nil {
return nil, fmt.Errorf("failed to list nftables %s forwarding rules: %w", family, err)
}
rules = append(rules, parseNftForwardRules(stdout)...)
}
return rules, nil
}
func (n *Nftables) ReplaceRules(rules []Rule) error {
byFamily := map[string][]Rule{FamilyIPv4: nil, FamilyIPv6: nil}
for _, rule := range rules {
normalized, err := NormalizeRule(rule)
if err != nil {
return err
}
byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized)
}
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
}
}
}
if err := ensureNftForwardTables(family); err != nil {
return fmt.Errorf("initialize nftables forwarding table: %w", err)
}
commands, err := rebuildNftForwardCommands(byFamily[family], family)
if err != nil {
return err
}
if err := nftRunCommands(context.Background(), commands); err != nil {
return err
}
}
return errors.Join(failures...)
}
func (n *Nftables) CreateRules(ctx context.Context, rules []Rule) error {
if len(rules) == 0 {
return nil
}
commands, err := createNftForwardCommands(rules)
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)
}
func (n *Nftables) DeleteRules(ctx context.Context, rules []Rule) error {
wanted := make(map[string]map[string]bool)
for _, rule := range rules {
normalized, err := NormalizeRule(rule)
if err != nil {
return err
}
family := nftTableFamily(normalized.Family)
if wanted[family] == nil {
wanted[family] = make(map[string]bool)
}
wanted[family][normalized.Identity()] = true
}
var commands [][]string
for _, family := range []string{"ip", "ip6"} {
if len(wanted[family]) == 0 {
continue
}
output, _, err := nftables_helper.ReadTable(func(args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...)
}, family, nftForwardTable)
if err != nil {
return err
}
chains := nftables_helper.ParseTableChains(output)
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
for _, rule := range parseNftForwardRules(chains["NFT_"+chain]) {
if !wanted[family][rule.Identity()] {
continue
}
if _, err := strconv.ParseUint(rule.Num, 10, 64); err != nil {
return fmt.Errorf("invalid nftables forwarding handle %q", rule.Num)
}
commands = append(commands, []string{"delete", "rule", family, nftForwardTable, "NFT_" + chain, "handle", rule.Num})
}
}
}
if len(commands) == 0 {
return nil
}
return nftRunCommands(ctx, commands)
}
func (n *Nftables) Enable() error {
if err := ensureForwardingSysctls(n.system, false); err != nil {
return err
}
if err := ensureNftForwardTables(FamilyIPv4); err != nil {
return fmt.Errorf("initialize nftables forwarding table: %w", err)
}
return nil
}
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 {
continue
}
commands = append(commands, []string{"delete", "table", tableFamily, nftForwardTable})
}
if len(commands) > 0 {
if err := nftRunCommands(context.Background(), commands); err != nil {
return err
}
}
file := filepath.Join(global.Dir.FirewallDir, nftForwardFile)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
func (n *Nftables) InitStatus() (bool, bool, error) {
var anyInitialized, anyBound bool
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
initialized, bound, err := n.FamilyStatus(family)
if err != nil {
return false, false, err
}
anyInitialized = anyInitialized || initialized
anyBound = anyBound || bound
}
return anyInitialized, anyBound, nil
}
func (n *Nftables) FamilyStatus(family string) (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
}
if err != nil {
return false, false, fmt.Errorf("read %s forwarding status: %w", family, err)
}
output, exists, err := nftables_helper.ReadTable(nftRun, nftTableFamily(family), nftForwardTable)
if err != nil {
return false, false, err
}
if !exists {
return false, false, nil
}
chains := nftables_helper.ParseTableChains(output)
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
if _, exists := chains["NFT_"+chain]; !exists {
return false, false, nil
}
}
return true, strings.TrimSpace(string(data)) != "0", nil
}
func (n *Nftables) Replay() error {
file := filepath.Join(global.Dir.FirewallDir, nftForwardFile)
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
return nil
} else if err != nil {
return err
}
allPresent := true
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if _, err := nftRun("list", "table", nftTableFamily(family), nftForwardTable); err != nil {
allPresent = false
}
}
if allPresent {
return nil
}
return nftRunCommand("-f", file)
}
func ensureNftForwardTables(families ...string) error {
commands := make([][]string, 0, 8)
for _, family := range families {
tableFamily := nftTableFamily(family)
output, tableExists, err := nftables_helper.ReadTable(nftRun, tableFamily, nftForwardTable)
if err != nil {
return err
}
existingChains := nftables_helper.ParseTableChains(output)
if !tableExists {
commands = append(commands, []string{"add", "table", tableFamily, nftForwardTable})
}
chains := []struct {
name, chainType, hook, priority string
}{
{"NFT_" + ChainPreRouting, "nat", "prerouting", "-100"},
{"NFT_" + ChainPostRouting, "nat", "postrouting", "100"},
{"NFT_" + ChainForward, "filter", "forward", "0"},
}
for _, chain := range chains {
if _, exists := existingChains[chain.name]; exists {
continue
}
commands = append(commands, []string{
"add", "chain", tableFamily, nftForwardTable, chain.name,
"{", "type", chain.chainType, "hook", chain.hook, "priority", chain.priority, ";", "policy", "accept", ";", "}",
})
}
}
if len(commands) == 0 {
return nil
}
return nftRunCommands(context.Background(), commands)
}
func rebuildNftForwardCommands(rules []Rule, family string) ([][]string, error) {
commands := make([][]string, 0, 6+len(rules)*4)
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
commands = append(commands, []string{"flush", "chain", nftTableFamily(family), nftForwardTable, "NFT_" + chain})
}
additions, err := createNftForwardCommands(rules)
return append(commands, additions...), err
}
func createNftForwardCommands(rules []Rule) ([][]string, error) {
commands := make([][]string, 0, len(rules)*4)
for _, rule := range rules {
normalized, err := NormalizeRule(rule)
if err != nil {
return nil, err
}
rule = normalized
tableFamily := nftTableFamily(rule.Family)
addressKeyword := tableFamily
comment := strconv.Quote(encodeNftForwardRule(rule))
interfaceMatch := make([]string, 0, 2)
if rule.Interface != "" {
interfaceMatch = append(interfaceMatch, "iifname", strconv.Quote(rule.Interface))
}
if isRemoteTarget(rule.Family, rule.TargetIP) {
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, "NFT_" + ChainPreRouting}
preRouting = append(preRouting, interfaceMatch...)
preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "dnat", "to", forwardingTarget(rule), "comment", comment)
commands = append(commands,
preRouting,
[]string{"add", "rule", tableFamily, nftForwardTable, "NFT_" + ChainPostRouting, addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "masquerade", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, "NFT_" + ChainForward, addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "accept", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, "NFT_" + ChainForward, addressKeyword, "saddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "sport", rule.TargetPort, "accept", "comment", comment},
)
continue
}
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, "NFT_" + ChainPreRouting}
preRouting = append(preRouting, interfaceMatch...)
preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "redirect", "to", ":"+rule.TargetPort, "comment", comment)
commands = append(commands, preRouting)
}
return commands, nil
}
func nftTableFamily(family string) string {
if family == FamilyIPv6 {
return "ip6"
}
return nftForwardFamily
}
func encodeNftForwardRule(rule Rule) string {
family, protocol := "4", "t"
if rule.Family == FamilyIPv6 {
family = "6"
}
if rule.Protocol == "udp" {
protocol = "u"
}
return nftForwardMarker + "v2|" + strings.Join(
[]string{family, protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface},
"|",
)
}
func decodeNftForwardRule(value string) (Rule, bool) {
if !strings.HasPrefix(value, nftForwardMarker) {
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
}
family, protocol := "", ""
switch parts[1] {
case "4":
family = FamilyIPv4
case "6":
family = FamilyIPv6
default:
return Rule{}, false
}
switch parts[2] {
case "t":
protocol = "tcp"
case "u":
protocol = "udp"
default:
return Rule{}, false
}
return Rule{
Family: family, Protocol: protocol, Port: parts[3], TargetIP: parts[4], TargetPort: parts[5], Interface: parts[6],
}, 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") {
commentStart := strings.Index(line, `comment "`+nftForwardMarker)
handleStart := strings.LastIndex(line, "# handle ")
if commentStart < 0 || handleStart < 0 {
continue
}
encodedStart := commentStart + len(`comment "`)
encodedEnd := strings.Index(line[encodedStart:], `"`)
if encodedEnd < 0 {
continue
}
rule, ok := decodeNftForwardRule(line[encodedStart : encodedStart+encodedEnd])
if !ok {
continue
}
rule.Num = strings.TrimSpace(line[handleStart+len("# handle "):])
result = append(result, rule)
}
return result
}
func nftRun(args ...string) (string, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...)
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...)
if err != nil {
return fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err)
}
return nil
}
func nftRunCommands(ctx context.Context, commands [][]string) error {
script, err := nftCommandsScript(commands)
if err != nil {
return err
}
var stderr strings.Builder
err = cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(script)), cmd.WithStderr(&stderr)).RunWithOptionalSudo("nft", "-f", "-")
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)
}
func nftCommandsScript(commands [][]string) (string, error) {
var script strings.Builder
for _, args := range commands {
if len(args) == 0 {
return "", fmt.Errorf("empty nftables command")
}
for _, token := range args {
if strings.ContainsAny(token, "\r\n") {
return "", fmt.Errorf("invalid newline in nftables command token")
}
}
script.WriteString(strings.Join(args, " "))
script.WriteByte('\n')
}
return script.String(), nil
}
@@ -1,18 +0,0 @@
package providers
import (
"errors"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
)
func New(provider string) (forwarding.Adapter, error) {
switch provider {
case "iptables":
return newIptablesNATAdapter(provider), nil
case "nftables":
return newNftablesAdapter(), nil
default:
return nil, errors.New("unsupported forwarding provider: " + provider)
}
}
@@ -1,359 +0,0 @@
package providers
import (
"encoding/base64"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
)
const (
nftForwardFamily = "ip"
nftForwardTable = "nft_1panel_forward"
nftForwardFile = "1panel_forward.nft"
nftForwardMarker = "1panel-forward:"
)
type nftablesAdapter struct{ system forwardingSystem }
func newNftablesAdapter() *nftablesAdapter {
return &nftablesAdapter{system: defaultForwardingSystem{}}
}
func (n *nftablesAdapter) Name() string { return "nftables" }
func (n *nftablesAdapter) List() ([]forwarding.Rule, error) {
rules := make([]forwarding.Rule, 0)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, nftForwardChain(forwarding.ChainPreRouting))
if errors.Is(err, nftables_helper.ErrChainNotFound) {
continue
}
if err != nil {
return nil, fmt.Errorf("failed to list nftables %s forwarding rules: %w", family, err)
}
rules = append(rules, parseNftForwardRules(stdout)...)
}
return rules, nil
}
func (n *nftablesAdapter) Reconcile(rules []forwarding.Rule) error {
if err := ensureNftForwardTables(); err != nil {
return fmt.Errorf("initialize nftables forwarding table: %w", err)
}
commands, err := rebuildNftForwardCommands(rules)
if err != nil {
return err
}
return nftRunCommands(commands)
}
func (n *nftablesAdapter) Enable() error {
if err := ensureForwardingSysctls(n.system, true); err != nil {
return err
}
if err := ensureNftForwardTables(); err != nil {
return fmt.Errorf("initialize nftables forwarding table: %w", err)
}
return nil
}
func (n *nftablesAdapter) Cleanup() error {
commands := make([][]string, 0, 2)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
tableFamily := nftTableFamily(family)
if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil {
continue
}
commands = append(commands, []string{"delete", "table", tableFamily, nftForwardTable})
}
if len(commands) > 0 {
if err := nftRunCommands(commands); err != nil {
return err
}
}
file := filepath.Join(global.Dir.FirewallDir, nftForwardFile)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
func (n *nftablesAdapter) InitStatus() (bool, bool, error) {
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
initialized, bound, err := n.FamilyStatus(family)
if err != nil || !initialized || !bound {
return initialized, bound, err
}
}
return true, true, nil
}
func (n *nftablesAdapter) FamilyStatus(family string) (bool, bool, error) {
sysctlPath := "/proc/sys/net/ipv4/ip_forward"
if family == forwarding.FamilyIPv6 {
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
}
data, err := n.system.ReadFile(sysctlPath)
if err != nil {
return false, false, fmt.Errorf("read %s forwarding status: %w", family, err)
}
for _, chain := range []string{forwarding.ChainPreRouting, forwarding.ChainPostRouting, forwarding.ChainForward} {
if _, err := nftRun("list", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)); err != nil {
return false, false, nil
}
}
return true, strings.TrimSpace(string(data)) != "0", nil
}
func (n *nftablesAdapter) Replay() error {
file := filepath.Join(global.Dir.FirewallDir, nftForwardFile)
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
return nil
} else if err != nil {
return err
}
allPresent := true
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
if _, err := nftRun("list", "table", nftTableFamily(family), nftForwardTable); err != nil {
allPresent = false
}
}
if allPresent {
return nil
}
return nftRunCommand("-f", file)
}
func ensureNftForwardTables() error {
commands := make([][]string, 0, 8)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
tableFamily := nftTableFamily(family)
tableExists := true
if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil {
tableExists = false
commands = append(commands, []string{"add", "table", tableFamily, nftForwardTable})
}
chains := []struct {
name, chainType, hook, priority string
}{
{nftForwardChain(forwarding.ChainPreRouting), "nat", "prerouting", "-100"},
{nftForwardChain(forwarding.ChainPostRouting), "nat", "postrouting", "100"},
{nftForwardChain(forwarding.ChainForward), "filter", "forward", "0"},
}
for _, chain := range chains {
if tableExists {
if _, err := nftRun("list", "chain", tableFamily, nftForwardTable, chain.name); err == nil {
continue
}
}
commands = append(commands, []string{
"add", "chain", tableFamily, nftForwardTable, chain.name,
"{", "type", chain.chainType, "hook", chain.hook, "priority", chain.priority, ";", "policy", "accept", ";", "}",
})
}
}
if len(commands) == 0 {
return nil
}
return nftRunCommands(commands)
}
func rebuildNftForwardCommands(rules []forwarding.Rule) ([][]string, error) {
commands := make([][]string, 0, 6+len(rules)*4)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
for _, chain := range []string{forwarding.ChainPreRouting, forwarding.ChainPostRouting, forwarding.ChainForward} {
commands = append(commands, []string{"flush", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)})
}
}
for _, rule := range rules {
normalized, err := forwarding.NormalizeRule(rule)
if err != nil {
return nil, err
}
rule = normalized
tableFamily := nftTableFamily(rule.Family)
addressKeyword := nftAddressKeyword(rule.Family)
comment := strconv.Quote(encodeNftForwardRule(rule))
interfaceMatch := make([]string, 0, 2)
if rule.Interface != "" {
interfaceMatch = append(interfaceMatch, "iifname", strconv.Quote(rule.Interface))
}
if isRemoteTarget(rule.Family, rule.TargetIP) {
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)}
preRouting = append(preRouting, interfaceMatch...)
preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "dnat", "to", forwardingTarget(rule), "comment", comment)
commands = append(commands,
preRouting,
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPostRouting), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "masquerade", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainForward), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "accept", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainForward), addressKeyword, "saddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "sport", rule.TargetPort, "accept", "comment", comment},
)
continue
}
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)}
preRouting = append(preRouting, interfaceMatch...)
preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "redirect", "to", ":"+rule.TargetPort, "comment", comment)
commands = append(commands, preRouting)
}
return commands, nil
}
func nftTableFamily(family string) string {
if family == forwarding.FamilyIPv6 {
return "ip6"
}
return nftForwardFamily
}
func nftAddressKeyword(family string) string {
if family == forwarding.FamilyIPv6 {
return "ip6"
}
return "ip"
}
func encodeNftForwardRule(rule forwarding.Rule) string {
family, protocol := "4", "t"
if rule.Family == forwarding.FamilyIPv6 {
family = "6"
}
if rule.Protocol == "udp" {
protocol = "u"
}
return nftForwardMarker + "v2|" + strings.Join(
[]string{family, protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface},
"|",
)
}
func decodeNftForwardRule(value string) (forwarding.Rule, bool) {
if !strings.HasPrefix(value, nftForwardMarker) {
return forwarding.Rule{}, false
}
value = strings.TrimPrefix(value, nftForwardMarker)
if strings.HasPrefix(value, "v2|") {
return decodeCompactNftForwardRule(value)
}
return decodeLegacyNftForwardRule(value)
}
func decodeCompactNftForwardRule(value string) (forwarding.Rule, bool) {
parts := strings.Split(value, "|")
if len(parts) != 7 || parts[0] != "v2" {
return forwarding.Rule{}, false
}
family, protocol := "", ""
switch parts[1] {
case "4":
family = forwarding.FamilyIPv4
case "6":
family = forwarding.FamilyIPv6
default:
return forwarding.Rule{}, false
}
switch parts[2] {
case "t":
protocol = "tcp"
case "u":
protocol = "udp"
default:
return forwarding.Rule{}, false
}
return forwarding.Rule{
Family: family, Protocol: protocol, Port: parts[3], TargetIP: parts[4], TargetPort: parts[5], Interface: parts[6],
}, true
}
func decodeLegacyNftForwardRule(value string) (forwarding.Rule, bool) {
parts := strings.Split(value, ".")
if len(parts) != 6 {
return forwarding.Rule{}, false
}
decoded := make([]string, len(parts))
for index, part := range parts {
data, err := base64.RawURLEncoding.DecodeString(part)
if err != nil {
return forwarding.Rule{}, false
}
decoded[index] = string(data)
}
return forwarding.Rule{Family: decoded[0], Protocol: decoded[1], Port: decoded[2], TargetIP: decoded[3], TargetPort: decoded[4], Interface: decoded[5]}, true
}
func parseNftForwardRules(stdout string) []forwarding.Rule {
result := make([]forwarding.Rule, 0)
for _, line := range strings.Split(stdout, "\n") {
commentStart := strings.Index(line, `comment "`+nftForwardMarker)
handleStart := strings.LastIndex(line, "# handle ")
if commentStart < 0 || handleStart < 0 {
continue
}
encodedStart := commentStart + len(`comment "`)
encodedEnd := strings.Index(line[encodedStart:], `"`)
if encodedEnd < 0 {
continue
}
rule, ok := decodeNftForwardRule(line[encodedStart : encodedStart+encodedEnd])
if !ok {
continue
}
rule.Num = strings.TrimSpace(line[handleStart+len("# handle "):])
result = append(result, rule)
}
return result
}
func nftForwardChain(logical string) string {
return "NFT_" + logical
}
func nftRun(args ...string) (string, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout("nft", args...)
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...)
if err != nil {
return fmt.Errorf("command=nft %s failed: %w", strings.Join(args, " "), err)
}
return nil
}
func nftRunCommands(commands [][]string) error {
script, err := nftCommandsScript(commands)
if err != nil {
return err
}
return nftables_helper.RunScript(script)
}
func nftCommandsScript(commands [][]string) (string, error) {
var script strings.Builder
for _, args := range commands {
if len(args) == 0 {
return "", fmt.Errorf("empty nftables command")
}
for _, token := range args {
if strings.ContainsAny(token, "\r\n") {
return "", fmt.Errorf("invalid newline in nftables command token")
}
}
script.WriteString(strings.Join(args, " "))
script.WriteByte('\n')
}
return script.String(), nil
}
@@ -1,67 +0,0 @@
package providers
import (
"errors"
"fmt"
"os"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
)
func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error {
paths := []string{"/proc/sys/net/ipv4/ip_forward"}
if withIPv6 {
paths = append(paths, "/proc/sys/net/ipv6/conf/all/forwarding")
}
for _, path := range paths {
if err := system.WriteFile(path, []byte("1"), constant.FilePerm); err != nil {
return fmt.Errorf("failed to enable IP forwarding at %s: %w", path, err)
}
}
data, err := system.ReadFile("/etc/sysctl.conf")
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to read /etc/sysctl.conf: %w", err)
}
content := enableForwardingSysctls(string(data), withIPv6)
if err := system.WriteFile("/etc/sysctl.conf", []byte(content), constant.FilePerm); err != nil {
return fmt.Errorf("failed to persist IP forwarding: %w", err)
}
if err := system.RunWithOptionalSudo("sysctl", "-p"); err != nil {
return fmt.Errorf("failed to apply IP forwarding: %w", err)
}
return nil
}
func enableForwardingSysctls(content string, withIPv6 bool) 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"
}
found := make(map[string]bool, len(wanted))
for index, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
if replacement, ok := wanted[key]; ok {
lines[index] = replacement
found[key] = true
}
}
for _, key := range []string{"net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding"} {
if replacement, ok := wanted[key]; ok && !found[key] {
lines = append(lines, replacement)
}
}
if len(lines) > 0 && lines[0] == "" {
lines = lines[1:]
}
return strings.Join(lines, "\n") + "\n"
}
+160
View File
@@ -0,0 +1,160 @@
package forwarding
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
)
func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error {
if withIPv6 {
interfaces, err := IPv6RAInterfaces(system.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, ", "))
}
}
paths := []string{"/proc/sys/net/ipv4/ip_forward"}
if withIPv6 {
paths = append(paths, "/proc/sys/net/ipv6/conf/all/forwarding")
}
for _, path := range paths {
if err := system.WriteFile(path, []byte("1"), constant.FilePerm); err != nil {
return fmt.Errorf("failed to enable IP forwarding at %s: %w", path, err)
}
}
data, err := system.ReadFile("/etc/sysctl.conf")
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to read /etc/sysctl.conf: %w", err)
}
content := enableForwardingSysctls(string(data), withIPv6)
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) {
addresses, err := readFile("/proc/net/if_inet6")
if err != nil {
return nil, err
}
const permanentAddress = 0x80
globalAddresses := make(map[string]bool)
raInterfaces := make(map[string]bool)
for _, line := range strings.Split(string(addresses), "\n") {
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
if len(fields) != 6 {
return nil, fmt.Errorf("invalid IPv6 address entry: %q", line)
}
name := fields[5]
if name == "lo" {
continue
}
flags, err := strconv.ParseUint(fields[4], 16, 32)
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
}
}
}
routes, err := readFile("/proc/net/ipv6_route")
if err != nil {
return nil, err
}
const raRouteFlags = 0x00010000 | 0x00800000
for _, line := range strings.Split(string(routes), "\n") {
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
if len(fields) != 10 {
return nil, fmt.Errorf("invalid IPv6 route entry: %q", line)
}
flags, err := strconv.ParseUint(fields[8], 16, 32)
if err != nil {
return nil, err
}
if flags&raRouteFlags != 0 {
raInterfaces[fields[9]] = true
}
}
for name, hasGlobalAddress := range globalAddresses {
if !hasGlobalAddress {
raInterfaces[name] = true
}
}
var interfaces []string
for name := range raInterfaces {
if name == "lo" {
continue
}
if name == "." || name == ".." || filepath.Base(name) != name {
return nil, fmt.Errorf("invalid IPv6 interface %q", name)
}
value, err := readFile("/proc/sys/net/ipv6/conf/" + name + "/accept_ra")
if err != nil {
return nil, err
}
switch strings.TrimSpace(string(value)) {
case "1":
interfaces = append(interfaces, name)
case "0", "2":
default:
return nil, fmt.Errorf("invalid accept_ra value for %s", name)
}
}
sort.Strings(interfaces)
return interfaces, nil
}
func enableForwardingSysctls(content string, withIPv6 bool) 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"
}
found := make(map[string]bool, len(wanted))
for index, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
if replacement, ok := wanted[key]; ok {
lines[index] = replacement
found[key] = true
}
}
for _, key := range []string{"net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding"} {
if replacement, ok := wanted[key]; ok && !found[key] {
lines = append(lines, replacement)
}
}
if len(lines) > 0 && lines[0] == "" {
lines = lines[1:]
}
return strings.Join(lines, "\n") + "\n"
}
@@ -145,3 +145,22 @@ func CheckChainExist(tab, chain string) (bool, error) {
} }
return false, nil return false, nil
} }
func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return "", err
}
executable := commands.IPv4
if ipv6 {
if !commands.IPv6Available() {
return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable)
}
executable = commands.IPv6
}
output, err := runTables(ctx, executable, table, false, true, "-S")
if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
}
return output, err
}
@@ -4,62 +4,10 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
) )
type FilterRules struct {
ID uint `json:"id"`
Chain string `json:"chain"`
Protocol string `json:"protocol"`
SrcPort string `json:"srcPort"`
DstPort string `json:"dstPort"`
SrcIP string `json:"srcIP"`
DstIP string `json:"dstIP"`
Strategy string `json:"strategy"`
Description string `json:"description"`
}
func readFilterRulesByChain(chain string, run func(string, ...string) (string, error)) ([]FilterRules, error) {
var rules []FilterRules
if cmd.CheckIllegal(chain) {
return rules, buserr.New("ErrCmdIllegal")
}
stdout, err := run(FilterTab, "-nL", chain)
if err != nil {
return rules, fmt.Errorf("load filter fules by chain %s failed, %v", chain, err)
}
lines := strings.Split(stdout, "\n")
for i := 0; i < len(lines); i++ {
fields := strings.Fields(lines[i])
if len(fields) > 2 && strings.Contains(fields[2], ":") {
fields = append(fields, "")
copy(fields[3:], fields[2:])
fields[2] = "--"
}
if len(fields) < 5 {
continue
}
strategy := strings.ToLower(fields[0])
if strategy != "accept" && strategy != "drop" && strategy != "reject" {
continue
}
itemRule := FilterRules{
Chain: chain,
Protocol: loadProtocol(fields[1]),
SrcPort: loadPort("src", fields),
DstPort: loadPort("dst", fields),
SrcIP: loadIP(fields[3]),
DstIP: loadIP(fields[4]),
Strategy: strategy,
}
rules = append(rules, itemRule)
}
return rules, nil
}
func LoadInitStatus(tab string) (bool, bool, error) { func LoadInitStatus(tab string) (bool, bool, error) {
return loadInitStatus(tab, RunWithStd, true) return loadInitStatus(tab, RunWithStd, true)
} }
@@ -172,47 +120,3 @@ func checkWithInitAndBind(initRules, bindRules []string, lines []string) (bool,
} }
return true, true return true, true
} }
func loadPort(position string, portStr []string) string {
if len(portStr) < 7 {
return ""
}
var portItem string
if strings.Contains(portStr[6], "spt:") && position == "src" {
portItem = strings.ReplaceAll(portStr[6], "spt:", "")
}
if strings.Contains(portStr[6], "dpt:") && position == "dst" {
portItem = strings.ReplaceAll(portStr[6], "dpt:", "")
}
if strings.Contains(portStr[6], "spts:") && position == "src" {
portItem = strings.ReplaceAll(portStr[6], "spts:", "")
}
if strings.Contains(portStr[6], "dpts:") && position == "dst" {
portItem = strings.ReplaceAll(portStr[6], "dpts:", "")
}
portItem = strings.ReplaceAll(portItem, ":", "-")
return portItem
}
func loadIP(ipStr string) string {
if ipStr == ANYWHERE || ipStr == "0.0.0.0/0" || ipStr == "::/0" {
return ""
}
return ipStr
}
func loadProtocol(protocol string) string {
switch protocol {
case "0":
return "all"
case "1":
return "icmp"
case "6":
return "tcp"
case "17":
return "udp"
default:
return protocol
}
}
+11 -31
View File
@@ -10,22 +10,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
) )
func (m *Manager) EnsureIPv6BaseChains() error { func EnsureIPv6BaseChains(ports []firewall.PortWhitelist) error {
ports, err := m.loadRequiredPorts()
if err != nil {
return err
}
return EnsureIPv6BaseChains(m.panelPort(), ports)
}
func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) error {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil || !commands.IPv6Available() { if err != nil || !commands.IPv6Available() {
return fmt.Errorf("ip6tables and ip6tables-restore are required") return fmt.Errorf("ip6tables and ip6tables-restore are required")
} }
if panelPort == "" {
return fmt.Errorf("panel port is required")
}
output, err := RunIPv6WithStd(FilterTab, "-S") output, err := RunIPv6WithStd(FilterTab, "-S")
if err != nil { if err != nil {
return err return err
@@ -33,7 +22,7 @@ func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) erro
if err := ensureBaseChainsFamily(true); err != nil { if err := ensureBaseChainsFamily(true); err != nil {
return err return err
} }
script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, panelPort, ports, output) script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, ports, output)
if err != nil { if err != nil {
return err return err
} }
@@ -43,16 +32,7 @@ func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) erro
if err := setBaseChainBindings(true, true); err != nil { if err := setBaseChainBindings(true, true); err != nil {
return err return err
} }
for _, chain := range []struct{ name, file string }{ return saveBaseChainsFamily(true)
{BasicBeforeChain, IPv6FileName(BasicBeforeFileName)},
{BasicChain, IPv6FileName(BasicFileName)},
{BasicAfterChain, IPv6FileName(BasicAfterFileName)},
} {
if err := SaveIPv6RulesToFile(FilterTab, chain.name, chain.file); err != nil {
return err
}
}
return nil
} }
func UnbindIPv6BaseChains() error { func UnbindIPv6BaseChains() error {
@@ -71,13 +51,13 @@ func BindIPv6BaseChains() error {
return setBaseChainBindings(true, true) return setBaseChainBindings(true, true)
} }
func buildIPv6BaseInitializationScript(dir, panelPort string, ports []firewall.PortWhitelist, output string) (string, error) { func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelist, output string) (string, error) {
for _, chain := range BasicChains() { for _, chain := range BasicChains() {
if !containsIptablesRule(output, "-N "+chain) { if !containsIptablesRule(output, "-N "+chain) {
return buildBaseChainsRestoreScript(dir, panelPort, true, ports...) return buildBaseChainsRestoreScript(dir, true, ports...)
} }
} }
defaults, err := baseDefaultRules(panelPort, ports, constant.FirewallFamilyIPv6) defaults, err := baseDefaultRules(ports, constant.FirewallFamilyIPv6)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -92,15 +72,15 @@ func buildIPv6BaseInitializationScript(dir, panelPort string, ports []firewall.P
return script.String(), nil return script.String(), nil
} }
func baseDefaultRules(panelPort string, ports []firewall.PortWhitelist, family string) ([]string, error) { func baseDefaultRules(ports []firewall.PortWhitelist, family string) ([]string, error) {
ports, err := firewall.NormalizeRequiredPorts(append([]firewall.PortWhitelist{{Port: panelPort, Protocol: "tcp"}}, ports...)) ports, err := firewall.NormalizeRequiredPorts(ports)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rules := []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} rules := []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule}
for _, port := range ports { for _, port := range firewall.ExpandPortWhitelist(ports) {
if port.Family == "" || port.Family == family { if port.Family == family {
rules = append(rules, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port)) rules = append(rules, iptablesSystemPortRuleLine(port))
} }
} }
return append(rules, "-A "+BasicAfterChain+" "+DropAllTcp, "-A "+BasicAfterChain+" "+DropAllUdp), nil return append(rules, "-A "+BasicAfterChain+" "+DropAllTcp, "-A "+BasicAfterChain+" "+DropAllUdp), nil
+116 -173
View File
@@ -3,9 +3,10 @@ package iptables_helper
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "sort"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
@@ -13,16 +14,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords"
) )
type Manager struct { func Cleanup() error {
UpdateSetting func(key, value string) error if err := disableBase(); err != nil {
PanelPort func() string
LoadRequiredPorts func() ([]firewall.PortWhitelist, error)
}
func (m *Manager) Cleanup() error {
if err := m.disableBase(); err != nil {
return err return err
} }
if err := cleanupBaseChains(false); err != nil { if err := cleanupBaseChains(false); err != nil {
@@ -42,59 +38,64 @@ func (m *Manager) Cleanup() error {
return nil return nil
} }
func (m *Manager) Operate(operation firewall.BaseOperation) error { func Operate(operation firewall.BaseOperation, requiredPorts []firewall.PortWhitelist) error {
switch operation { switch operation {
case firewall.BaseOperationInit, firewall.BaseOperationBind: case firewall.BaseOperationInit, firewall.BaseOperationBind:
if _, err := lifecycle.ResolveIptablesCommands(); err != nil { if _, err := lifecycle.ResolveIptablesCommands(); err != nil {
return fmt.Errorf("failed to find iptables") return fmt.Errorf("failed to find iptables")
} }
return m.enableBase(true) return enableBase(true, requiredPorts)
case firewall.BaseOperationBindWithoutInit: case firewall.BaseOperationBindWithoutInit:
return m.enableBase(false) return enableBase(false, requiredPorts)
case firewall.BaseOperationUnbind: case firewall.BaseOperationUnbind:
return m.disableBase() return disableBase()
default: default:
return fmt.Errorf("unsupported iptables base operation %q", operation) return fmt.Errorf("unsupported iptables base operation %q", operation)
} }
} }
func (m *Manager) enableBase(prepare bool) error { func enableBase(prepare bool, requiredPorts []firewall.PortWhitelist) error {
if prepare { if prepare {
if err := ensureBaseChains(); err != nil { if err := ensureBaseChainsFamily(false); err != nil {
return err return err
} }
if err := m.initPreRules(); err != nil { if err := applyRequiredFirewallPortWhiteListRules(requiredPorts, false, true, false); err != nil {
return err return err
} }
if err := saveBaseChains(); err != nil { if err := saveBaseChainsFamily(false); err != nil {
return err return err
} }
} }
if err := bindBaseChains(); err != nil { if err := setBaseChainBindings(false, true); err != nil {
return err return err
} }
if prepare { if prepare {
if err := m.ensureIPv6BaseChains(); err != nil { commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return err
}
if commands.IPv6Available() {
if err := EnsureIPv6BaseChains(requiredPorts); err != nil {
return err
}
}
if err := syncRequiredPorts(requiredPorts, true); err != nil {
return err return err
} }
} else if err := BindIPv6BaseChains(); err != nil { } else if err := BindIPv6BaseChains(); err != nil {
return err return err
} }
return m.updateSetting("IptablesStatus", constant.StatusEnable) return nil
} }
func (m *Manager) disableBase() error { func disableBase() error {
if err := setBaseChainBindings(false, false); err != nil { if err := setBaseChainBindings(false, false); err != nil {
return err return err
} }
if err := UnbindIPv6BaseChains(); err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) { if err := UnbindIPv6BaseChains(); err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
return err return err
} }
return m.updateSetting("IptablesStatus", constant.StatusDisable) return nil
}
func ensureBaseChains() error {
return ensureBaseChainsFamily(false)
} }
func ensureBaseChainsFamily(ipv6 bool) error { func ensureBaseChainsFamily(ipv6 bool) error {
@@ -165,10 +166,6 @@ func cleanupBaseChains(ipv6 bool) error {
return nil return nil
} }
func bindBaseChains() error {
return setBaseChainBindings(false, true)
}
func setBaseChainBindings(ipv6, bind bool) error { func setBaseChainBindings(ipv6, bind bool) error {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
@@ -228,32 +225,43 @@ func baseChainBindingCommands(output string, bind bool) []string {
return lines return lines
} }
func saveBaseChains() error { func saveBaseChainsFamily(ipv6 bool) error {
read := RunWithStd
if ipv6 {
read = RunIPv6WithStd
}
output, err := read(FilterTab, "-S")
if err != nil {
return err
}
for _, item := range []struct{ chain, file string }{ for _, item := range []struct{ chain, file string }{
{BasicBeforeChain, BasicBeforeFileName}, {BasicBeforeChain, BasicBeforeFileName},
{BasicChain, BasicFileName}, {BasicChain, BasicFileName},
{BasicAfterChain, BasicAfterFileName}, {BasicAfterChain, BasicAfterFileName},
} { } {
if err := SaveRulesToFile(FilterTab, item.chain, item.file); err != nil { if !containsIptablesRule(output, "-N "+item.chain) {
return fmt.Errorf("cannot save missing iptables chain %s", item.chain)
}
file := item.file
if ipv6 {
file = IPv6FileName(file)
}
if err := writeChainRules(output, item.chain, file); err != nil {
return err return err
} }
} }
return nil return nil
} }
func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist) error { func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
port, err := strconv.Atoi(panelPort)
if err != nil || port < 1 || port > 65535 {
return fmt.Errorf("invalid panel port %q", panelPort)
}
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
return err return err
} }
if err := ensureBaseChains(); err != nil { if err := ensureBaseChainsFamily(false); err != nil {
return err return err
} }
input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, false) input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, false, requiredPorts...)
if err != nil { if err != nil {
return err return err
} }
@@ -266,7 +274,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
if err := ensureBaseChainsFamily(true); err != nil { if err := ensureBaseChainsFamily(true); err != nil {
return err return err
} }
input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, true, requiredPorts...) input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, true, requiredPorts...)
if err != nil { if err != nil {
return err return err
} }
@@ -276,7 +284,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
return nil return nil
} }
func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) { func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) {
var script strings.Builder var script strings.Builder
script.WriteString("*filter\n") script.WriteString("*filter\n")
for _, chain := range BasicChains() { for _, chain := range BasicChains() {
@@ -284,8 +292,6 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
script.WriteString(chain) script.WriteString(chain)
script.WriteByte('\n') script.WriteByte('\n')
} }
panelRule := "-A " + BasicBeforeChain + " -p tcp -m tcp --dport " + panelPort + " -j ACCEPT"
panelRuleFound := false
for _, item := range []struct{ chain, file string }{ for _, item := range []struct{ chain, file string }{
{BasicBeforeChain, BasicBeforeFileName}, {BasicBeforeChain, BasicBeforeFileName},
{BasicChain, BasicFileName}, {BasicChain, BasicFileName},
@@ -308,53 +314,31 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") { if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") {
continue continue
} }
if line == panelRule {
panelRuleFound = true
}
script.WriteString(line) script.WriteString(line)
script.WriteByte('\n') script.WriteByte('\n')
} }
} }
if !panelRuleFound { family := constant.FirewallFamilyIPv4
script.WriteString(panelRule)
script.WriteByte('\n')
}
if ipv6 { if ipv6 {
defaults, err := baseDefaultRules(panelPort, requiredPorts, constant.FirewallFamilyIPv6) family = constant.FirewallFamilyIPv6
if err != nil { }
return "", err defaults, err := baseDefaultRules(requiredPorts, family)
} if err != nil {
for _, rule := range defaults { return "", err
if !containsIptablesRule(script.String(), rule) { }
script.WriteString(rule + "\n") for _, rule := range defaults {
if !containsIptablesRule(script.String(), rule) {
if strings.HasPrefix(rule, "-A "+BasicBeforeChain+" ") && strings.Contains(rule, " --dport ") {
rule = strings.Replace(rule, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1)
} }
script.WriteString(rule + "\n")
} }
} }
script.WriteString("COMMIT\n") script.WriteString("COMMIT\n")
return script.String(), nil return script.String(), nil
} }
func (m *Manager) initPreRules() error { func syncRequiredPorts(requiredPorts []firewall.PortWhitelist, withSave bool) error {
requiredPorts, err := m.loadRequiredPorts()
if err != nil {
return err
}
return applyRequiredFirewallPortWhiteListRules(requiredPorts, false, true, false)
}
func (m *Manager) ensureIPv6BaseChains() error {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil || !commands.IPv6Available() {
return nil
}
return m.EnsureIPv6BaseChains()
}
func (m *Manager) SyncRequiredPorts(withSave bool) error {
requiredPorts, err := m.loadRequiredPorts()
if err != nil {
return err
}
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
return err return err
@@ -386,23 +370,22 @@ func (m *Manager) SyncRequiredPorts(withSave bool) error {
} }
func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitelist, withSave, includeDefaults, ipv6 bool) error { func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitelist, withSave, includeDefaults, ipv6 bool) error {
portWhiteList, err := firewall.NormalizeRequiredPorts(portWhiteList) ports, err := firewall.NormalizeRequiredPorts(portWhiteList)
if err != nil { if err != nil {
return err return err
} }
run, save := RunWithStd, SaveRulesToFile rules := firewall.ExpandPortWhitelist(ports)
run := RunWithStd
save := SaveRulesToFile
beforeFile, afterFile := BasicBeforeFileName, BasicAfterFileName beforeFile, afterFile := BasicBeforeFileName, BasicAfterFileName
if ipv6 { if ipv6 {
run, save = RunIPv6WithStd, SaveIPv6RulesToFile run = RunIPv6WithStd
save = SaveIPv6RulesToFile
beforeFile, afterFile = IPv6FileName(beforeFile), IPv6FileName(afterFile) beforeFile, afterFile = IPv6FileName(beforeFile), IPv6FileName(afterFile)
} }
beforeRules, err := readFilterRulesByChain(BasicBeforeChain, run) family := constant.FirewallFamilyIPv4
if err != nil { if ipv6 {
return err family = constant.FirewallFamilyIPv6
}
afterRules, err := readFilterRulesByChain(BasicAfterChain, run)
if err != nil {
return err
} }
beforeRaw, err := run(FilterTab, "-S", BasicBeforeChain) beforeRaw, err := run(FilterTab, "-S", BasicBeforeChain)
if err != nil { if err != nil {
@@ -412,16 +395,16 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
if err != nil { if err != nil {
return err return err
} }
script := buildRequiredPortsRestoreScript(portWhiteList, beforeRules, afterRules, beforeRaw, afterRaw, includeDefaults) commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return err
}
restore := commands.Restore4
if ipv6 {
restore = commands.Restore6
}
script := buildRequiredPortsRestoreScript(rules, family, beforeRaw, afterRaw, includeDefaults)
if script != "" { if script != "" {
commands, resolveErr := lifecycle.ResolveIptablesCommands()
if resolveErr != nil {
return resolveErr
}
restore := commands.Restore4
if ipv6 {
restore = commands.Restore6
}
if err := restoreRules(restore, script); err != nil { if err := restoreRules(restore, script); err != nil {
return fmt.Errorf("batch sync required firewall ports with %s: %w", restore, err) return fmt.Errorf("batch sync required firewall ports with %s: %w", restore, err)
} }
@@ -435,54 +418,20 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
return save(FilterTab, BasicAfterChain, afterFile) return save(FilterTab, BasicAfterChain, afterFile)
} }
func buildRequiredPortsRestoreScript( func buildRequiredPortsRestoreScript(desired []firewall.SystemPort, family string, beforeRaw, afterRaw string, includeDefaults bool) string {
desired []firewall.PortWhitelist, var commands []string
beforeRules, afterRules []FilterRules, for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} {
beforeRaw, afterRaw string, if !containsIptablesRule(beforeRaw, line) {
includeDefaults bool, commands = append(commands, line)
) string {
desiredKeys := firewall.PortWhitelistMap(desired)
kept := make(map[string]struct{}, len(desired))
commands := make([]string, 0)
for _, rule := range beforeRules {
if !simpleAcceptedPortRule(rule) {
continue
}
key := firewall.PortWhitelistKey(firewall.PortWhitelist{Protocol: rule.Protocol, Port: rule.DstPort})
if _, wanted := desiredKeys[key]; wanted {
if _, alreadyKept := kept[key]; !alreadyKept {
kept[key] = struct{}{}
continue
}
}
commands = append(commands, iptablesPortRuleLine("-D", BasicBeforeChain, rule.Protocol, rule.DstPort))
}
for _, rule := range afterRules {
if simpleAcceptedPortRule(rule) && rule.Protocol == "udp" {
commands = append(commands, iptablesPortRuleLine("-D", BasicAfterChain, rule.Protocol, rule.DstPort))
} }
} }
for _, rule := range desired {
if includeDefaults { line := iptablesSystemPortRuleLine(rule)
for _, rule := range []string{ if rule.Family == family && !containsIptablesRule(beforeRaw, line) {
"-A " + BasicBeforeChain + " " + IoRuleIn, commands = append(commands, strings.Replace(line, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1))
"-A " + BasicBeforeChain + " " + EstablishedRule, beforeRaw += "\n" + line
} {
count := countIptablesRule(beforeRaw, rule)
for duplicate := 1; duplicate < count; duplicate++ {
commands = append(commands, strings.Replace(rule, "-A ", "-D ", 1))
}
if count == 0 {
commands = append(commands, rule)
}
} }
} }
for _, port := range desired {
if _, exists := kept[firewall.PortWhitelistKey(port)]; exists {
continue
}
commands = append(commands, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port))
}
if includeDefaults { if includeDefaults {
for _, rule := range []string{DropAllTcp, DropAllUdp} { for _, rule := range []string{DropAllTcp, DropAllUdp} {
line := "-A " + BasicAfterChain + " " + rule line := "-A " + BasicAfterChain + " " + rule
@@ -497,13 +446,12 @@ func buildRequiredPortsRestoreScript(
return "*filter\n" + strings.Join(commands, "\n") + "\nCOMMIT\n" return "*filter\n" + strings.Join(commands, "\n") + "\nCOMMIT\n"
} }
func simpleAcceptedPortRule(rule FilterRules) bool { func iptablesSystemPortRuleLine(rule firewall.SystemPort) string {
return rule.Strategy == "accept" && (rule.Protocol == "tcp" || rule.Protocol == "udp") && rule.DstPort != "" && parts := []string{"-A", BasicBeforeChain}
rule.SrcIP == "" && rule.DstIP == "" && rule.SrcPort == "" if rule.SourceAddress != "" {
} parts = append(parts, "-s", rule.SourceAddress)
}
func iptablesPortRuleLine(operation, chain, protocol, port string) string { return strings.Join(append(parts, "-p", rule.Protocol, "-m", rule.Protocol, "--dport", rule.Port, "-j", "ACCEPT"), " ")
return strings.Join([]string{operation, chain, "-p", protocol, "-m", protocol, "--dport", port, "-j", "ACCEPT"}, " ")
} }
func containsIptablesRule(output, rule string) bool { func containsIptablesRule(output, rule string) bool {
@@ -512,11 +460,27 @@ func containsIptablesRule(output, rule string) bool {
func countIptablesRule(output, rule string) int { func countIptablesRule(output, rule string) int {
canonical := func(value string) string { canonical := func(value string) string {
value = strings.TrimSpace(value) fields, err := shellwords.Parse(value)
if strings.Contains(value, " -j ACCEPT") { if err != nil || len(fields)%2 != 0 {
return strings.Replace(value, " -j ACCEPT", "", 1) + " -j ACCEPT" return strings.TrimSpace(value)
} }
return 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 = canonical(rule)
count := 0 count := 0
@@ -527,24 +491,3 @@ func countIptablesRule(output, rule string) int {
} }
return count return count
} }
func (m *Manager) updateSetting(key, value string) error {
if m != nil && m.UpdateSetting != nil {
return m.UpdateSetting(key, value)
}
return nil
}
func (m *Manager) panelPort() string {
if m != nil && m.PanelPort != nil {
return m.PanelPort()
}
return ""
}
func (m *Manager) loadRequiredPorts() ([]firewall.PortWhitelist, error) {
if m != nil && m.LoadRequiredPorts != nil {
return m.LoadRequiredPorts()
}
return nil, fmt.Errorf("load required firewall ports is not configured")
}
@@ -53,8 +53,6 @@ func IPv6FileName(fileName string) string {
} }
func saveRulesToFile(ctx context.Context, executable, tab, chain, fileName string) error { func saveRulesToFile(ctx context.Context, executable, tab, chain, fileName string) error {
rulesFile := path.Join(global.Dir.FirewallDir, fileName)
var stdout string var stdout string
var err error var err error
if strings.HasPrefix(path.Base(executable), "ip6tables") { if strings.HasPrefix(path.Base(executable), "ip6tables") {
@@ -65,11 +63,16 @@ func saveRulesToFile(ctx context.Context, executable, tab, chain, fileName strin
if err != nil { if err != nil {
return fmt.Errorf("failed to list %s rules: %w", chain, err) return fmt.Errorf("failed to list %s rules: %w", chain, err)
} }
return writeChainRules(stdout, chain, fileName)
}
func writeChainRules(stdout, chain, fileName string) error {
rulesFile := path.Join(global.Dir.FirewallDir, fileName)
var rules []string var rules []string
lines := strings.Split(stdout, "\n") lines := strings.Split(stdout, "\n")
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if strings.HasPrefix(line, fmt.Sprintf("-A %s", chain)) { if strings.HasPrefix(line, "-A "+chain+" ") {
rules = append(rules, line) rules = append(rules, line)
} }
} }
@@ -1,29 +0,0 @@
package iptables_helper
import (
"context"
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return "", err
}
executable := commands.IPv4
if ipv6 {
if !commands.IPv6Available() {
return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable)
}
executable = commands.IPv6
}
output, err := runTables(ctx, executable, table, false, true, "-S")
if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
}
return output, err
}
@@ -9,11 +9,12 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
) )
func (m *Manager) RepairBaseChains() error { func RepairBaseChains(ports []firewall.PortWhitelist) error {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
return err return err
@@ -35,11 +36,7 @@ func (m *Manager) RepairBaseChains() error {
return err return err
} }
script, err := buildBaseChainsRepairScript(global.Dir.FirewallDir, output, ipv6, func() ([]string, error) { script, err := buildBaseChainsRepairScript(global.Dir.FirewallDir, output, ipv6, func() ([]string, error) {
ports, err := m.loadRequiredPorts() return baseDefaultRules(ports, family)
if err != nil {
return nil, err
}
return baseDefaultRules(m.panelPort(), ports, family)
}) })
if err != nil { if err != nil {
return fmt.Errorf("prepare %s base chain repair: %w", family, err) return fmt.Errorf("prepare %s base chain repair: %w", family, err)
+15 -8
View File
@@ -10,6 +10,14 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
) )
type Operation string
const (
OperationStart Operation = "start"
OperationStop Operation = "stop"
OperationRestart Operation = "restart"
)
const ( const (
ProviderFirewalld = constant.FirewallProviderFirewalld ProviderFirewalld = constant.FirewallProviderFirewalld
ProviderUFW = constant.FirewallProviderUFW ProviderUFW = constant.FirewallProviderUFW
@@ -109,15 +117,14 @@ type PreStopResetter interface {
ResetBeforeStop() error ResetBeforeStop() error
} }
func NewClient() (Client, error) { func NewClient(provider string) (Client, error) {
runtime, err := DetectRuntime() if provider == "" {
if err != nil { runtime, err := DetectRuntime()
return nil, err if err != nil {
return nil, err
}
provider = runtime.Provider
} }
return NewClientFor(runtime.Provider)
}
func NewClientFor(provider string) (Client, error) {
switch provider { switch provider {
case "firewalld": case "firewalld":
if !which("firewalld") { if !which("firewalld") {
-159
View File
@@ -1,159 +0,0 @@
package lifecycle
import (
"errors"
"fmt"
"os"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
)
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
type Operation string
const (
OperationStart Operation = "start"
OperationStop Operation = "stop"
OperationRestart Operation = "restart"
)
type Operator struct {
client Client
}
// DockerRestartError reports that the requested firewall operation completed,
// but rebuilding Docker's firewall rules failed.
type DockerRestartError struct {
Err error
}
func (e *DockerRestartError) Error() string {
return fmt.Sprintf("failed to restart Docker: %v", e.Err)
}
func (e *DockerRestartError) Unwrap() error {
return e.Err
}
type CompletedOperationError struct {
Operation Operation
Err error
}
func (e *CompletedOperationError) Error() string {
return fmt.Sprintf("firewall %s completed with recovery errors: %v", e.Operation, e.Err)
}
func (e *CompletedOperationError) Unwrap() error {
return e.Err
}
func NewOperator(client Client) *Operator {
return &Operator{client: client}
}
func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareStart func(Client) error) error {
var recoveryErrors []error
switch operation {
case OperationStart:
if err := o.client.Start(); err != nil {
return err
}
if prepareStart != nil {
if err := prepareStart(o.client); err != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after start: %w", err))
}
}
case OperationStop:
return o.StopWithPrepare(withDockerRestart, nil)
case OperationRestart:
if err := o.client.Restart(); err != nil {
return err
}
if prepareStart != nil {
if err := prepareStart(o.client); err != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after restart: %w", err))
}
}
default:
return fmt.Errorf("not supported operation: %s", operation)
}
if withDockerRestart {
if err := controller.HandleRestart("docker"); err != nil {
recoveryErrors = append(recoveryErrors, &DockerRestartError{Err: err})
}
}
if o.client.Name() == ProviderFirewalld && operation == OperationStart {
if err := restoreFail2BanAfterFirewallStart(); err != nil {
recoveryErrors = append(recoveryErrors, err)
}
}
if err := errors.Join(recoveryErrors...); err != nil {
return &CompletedOperationError{Operation: operation, Err: err}
}
return nil
}
// StopWithPrepare records dependent service state, runs preparation, stops the
// firewall, and optionally restarts Docker in that order.
func (o *Operator) StopWithPrepare(withDockerRestart bool, prepareStop func() error) error {
if o.client.Name() == ProviderFirewalld {
if err := rememberFail2BanBeforeFirewallStop(); err != nil {
return err
}
}
if prepareStop != nil {
if err := prepareStop(); err != nil {
return err
}
}
if err := o.client.Stop(); err != nil {
return err
}
if withDockerRestart {
if err := controller.HandleRestart("docker"); err != nil {
return &DockerRestartError{Err: err}
}
}
return nil
}
func rememberFail2BanBeforeFirewallStop() error {
exists, err := controller.CheckExist("fail2ban.service")
if err != nil {
global.LOG.Warnf("check fail2ban.service installation before stopping the firewall failed: %v", err)
}
if !exists {
return nil
}
active, err := controller.CheckActive("fail2ban.service")
if err != nil {
global.LOG.Warnf("check fail2ban.service status before stopping the firewall failed: %v", err)
}
if !active {
return nil
}
if err := os.WriteFile(fail2BanRestoreWithFirewallMarker, nil, 0600); err != nil {
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
}
return nil
}
func restoreFail2BanAfterFirewallStart() error {
if _, err := os.Stat(fail2BanRestoreWithFirewallMarker); err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
}
if err := controller.HandleStart("fail2ban.service"); err != nil {
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
}
if err := os.Remove(fail2BanRestoreWithFirewallMarker); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
}
return nil
}
@@ -11,6 +11,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
) )
type Firewalld struct{} type Firewalld struct{}
@@ -37,7 +38,8 @@ func (f *Firewalld) Name() string {
func (f *Firewalld) Status() (bool, error) { func (f *Firewalld) Status() (bool, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--state") stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--state")
if err != nil { if err != nil {
if firewalldStopped(stdout, err) { message := strings.ToLower(strings.TrimSpace(stdout)) + " " + strings.ToLower(err.Error())
if strings.Contains(message, "not running") {
return false, nil return false, nil
} }
return false, fmt.Errorf("load firewall status failed: %w", err) return false, fmt.Errorf("load firewall status failed: %w", err)
@@ -45,14 +47,6 @@ func (f *Firewalld) Status() (bool, error) {
return strings.TrimSpace(stdout) == "running", nil return strings.TrimSpace(stdout) == "running", nil
} }
func firewalldStopped(stdout string, err error) bool {
message := strings.ToLower(strings.TrimSpace(stdout))
if err != nil {
message += " " + strings.ToLower(err.Error())
}
return strings.Contains(message, "not running")
}
func (f *Firewalld) Version() (string, error) { func (f *Firewalld) Version() (string, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--version") stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--version")
if err != nil { if err != nil {
@@ -68,6 +62,19 @@ func (f *Firewalld) Start() error {
return nil return nil
} }
func RemoveFirewalldSSHService() error {
manager := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en"))
for _, args := range [][]string{
{"--zone=" + filter.FirewalldInputZone, "--remove-service=ssh"},
{"--permanent", "--zone=" + filter.FirewalldInputZone, "--remove-service=ssh"},
} {
if err := manager.RunWithOptionalSudo("firewall-cmd", args...); err != nil {
return fmt.Errorf("remove firewalld SSH service: %w", err)
}
}
return nil
}
func (f *Firewalld) Stop() error { func (f *Firewalld) Stop() error {
if err := controller.HandleStop("firewalld"); err != nil { if err := controller.HandleStop("firewalld"); err != nil {
return fmt.Errorf("stop the firewall failed, err: %v", err) return fmt.Errorf("stop the firewall failed, err: %v", err)
@@ -105,12 +112,7 @@ func (f *Firewalld) ResetBeforeStop() error {
return nil return nil
} }
func replaceFirewalldConfig( func replaceFirewalldConfig(configDir string, backupDir string, prepare func(string) error, validate func() error) (func() error, error) {
configDir string,
backupDir string,
prepare func(string) error,
validate func() error,
) (func() error, error) {
info, err := os.Lstat(configDir) info, err := os.Lstat(configDir)
hadConfig := err == nil hadConfig := err == nil
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
+100 -180
View File
@@ -4,25 +4,20 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"net/netip"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
) )
const requiredPortComment = "1Panel Port Whitelist" const requiredPortComment = "1Panel Port Whitelist"
type Manager struct { func Cleanup() error {
UpdateSetting func(key, value string) error
LoadRequiredPorts func() ([]firewall.PortWhitelist, error)
}
func (m *Manager) Cleanup() error {
commands := make([][]string, 0, 2) commands := make([][]string, 0, 2)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
@@ -38,69 +33,58 @@ func (m *Manager) Cleanup() error {
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return err return err
} }
return m.updateSetting("IptablesStatus", constant.StatusDisable) return nil
} }
func (m *Manager) Operate(operation firewall.BaseOperation) error { func Operate(operation firewall.BaseOperation, requiredPorts []firewall.PortWhitelist) error {
switch operation { switch operation {
case firewall.BaseOperationInit, firewall.BaseOperationBind: case firewall.BaseOperationInit, firewall.BaseOperationBind:
return m.enableBase(true) return enableBase(true, requiredPorts)
case firewall.BaseOperationBindWithoutInit: case firewall.BaseOperationBindWithoutInit:
return m.enableBase(false) return enableBase(false, requiredPorts)
case firewall.BaseOperationUnbind: case firewall.BaseOperationUnbind:
return m.disableBase() return Unbind()
default: default:
return fmt.Errorf("unsupported nftables base operation %q", operation) return fmt.Errorf("unsupported nftables base operation %q", operation)
} }
} }
func (m *Manager) enableBase(prepare bool) error { func enableBase(prepare bool, requiredPorts []firewall.PortWhitelist) error {
if prepare { if prepare {
if err := m.ensureBaseChains(); err != nil { if err := ensureBaseChains(); err != nil {
return err return err
} }
if err := m.initPreRules(); err != nil { if err := initPreRules(requiredPorts); err != nil {
return err return err
} }
} }
if err := Bind(); err != nil { if err := Bind(); err != nil {
return err return err
} }
return m.updateSetting("IptablesStatus", constant.StatusEnable) return nil
} }
func (m *Manager) disableBase() error { func ensureBaseChains() error {
if err := Unbind(); err != nil {
return err
}
return m.updateSetting("IptablesStatus", constant.StatusDisable)
}
func (m *Manager) ensureBaseChains() error {
commands := make([][]string, 0, 10) commands := make([][]string, 0, 10)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
tableExists := true output, tableExists, err := ReadTable(run, tableFamily, TableName)
if _, err := run("list", "table", tableFamily, TableName); err != nil { if err != nil {
tableExists = false return err
}
chains := ParseTableChains(output)
if !tableExists {
commands = append(commands, []string{"add", "table", tableFamily, TableName}) commands = append(commands, []string{"add", "table", tableFamily, TableName})
} }
if !tableExists { if _, exists := chains[InputChain]; !exists {
commands = append(commands, []string{
"add", "chain", tableFamily, TableName, InputChain,
"{", "type", "filter", "hook", "input", "priority", "0", ";", "policy", "accept", ";", "}",
})
} else if _, err := run("list", "chain", tableFamily, TableName, InputChain); err != nil {
commands = append(commands, []string{ commands = append(commands, []string{
"add", "chain", tableFamily, TableName, InputChain, "add", "chain", tableFamily, TableName, InputChain,
"{", "type", "filter", "hook", "input", "priority", "0", ";", "policy", "accept", ";", "}", "{", "type", "filter", "hook", "input", "priority", "0", ";", "policy", "accept", ";", "}",
}) })
} }
for _, nativeChain := range BasicChains() { for _, nativeChain := range BasicChains() {
if tableExists { if _, exists := chains[nativeChain]; exists {
if _, err := run("list", "chain", tableFamily, TableName, nativeChain); err == nil { continue
continue
}
} }
commands = append(commands, []string{"add", "chain", tableFamily, TableName, nativeChain}) commands = append(commands, []string{"add", "chain", tableFamily, TableName, nativeChain})
} }
@@ -111,33 +95,45 @@ func (m *Manager) ensureBaseChains() error {
return nil return nil
} }
func requiredPortCommand(tableFamily string, port firewall.PortWhitelist) []string { func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string {
return []string{ command := []string{
"add", "rule", tableFamily, TableName, BasicBeforeChain, "insert", "rule", tableFamily, TableName, BasicBeforeChain,
"meta", "l4proto", port.Protocol, port.Protocol, "dport", port.Port,
"accept", "comment", `"` + requiredPortComment + `"`,
} }
if rule.SourceAddress != "" {
command = append(command, tableFamily, "saddr", rule.SourceAddress)
}
return append(command, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port,
"accept", "comment", `"`+requiredPortComment+`"`)
} }
func (m *Manager) initPreRules() error { func initPreRules(requiredPorts []firewall.PortWhitelist) error {
ports, err := m.loadRequiredPorts() ports, err := firewall.NormalizeRequiredPorts(requiredPorts)
if err != nil { if err != nil {
return err return err
} }
ports, err = firewall.NormalizeRequiredPorts(ports) rules := firewall.ExpandPortWhitelist(ports)
if err != nil { var commands [][]string
return err
}
commands := make([][]string, 0, 12+len(ports)*2)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
commands = append(commands, output, _, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain)
[]string{"flush", "chain", tableFamily, TableName, BasicBeforeChain}, if err != nil {
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`}, return err
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`}, }
) candidates := [][]string{
for _, port := range ports { {"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
commands = append(commands, requiredPortCommand(tableFamily, port)) {"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
}
for _, rule := range rules {
if rule.Family == string(family) {
candidates = append(candidates, requiredPortCommand(tableFamily, rule))
}
}
for _, command := range candidates {
expression := strings.Join(command[5:], " ")
if !containsRequiredPortRule(output, expression) {
commands = append(commands, command)
output += "\n" + expression
}
} }
commands = append(commands, commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicAfterChain}, []string{"flush", "chain", tableFamily, TableName, BasicAfterChain},
@@ -148,132 +144,56 @@ func (m *Manager) initPreRules() error {
return runBatch(commands...) return runBatch(commands...)
} }
func (m *Manager) SyncRequiredPorts() error { func containsRequiredPortRule(output, expression string) bool {
ports, err := m.loadRequiredPorts() canonical := func(line string) string {
if err != nil { line, _, _ = strings.Cut(line, " comment ")
return err line, _, _ = strings.Cut(line, " # handle ")
} for _, protocol := range []string{"tcp", "udp"} {
ports, err = firewall.NormalizeRequiredPorts(ports) line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "")
if err != nil {
return err
}
commands, err := requiredPortSyncCommands(run, ports)
if err != nil {
return err
}
if err := runBatch(commands...); err != nil {
return err
}
return PersistRuleset(context.Background())
}
func requiredPortSyncCommands(run func(...string) (string, error), ports []firewall.PortWhitelist) ([][]string, error) {
commands := make([][]string, 0)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
_, exists, err := readNftObject(run, "list", "table", tableFamily, TableName)
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
} }
if err != nil { line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
return nil, err fields := strings.Fields(line)
} for index, field := range fields {
if !exists { if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" {
continue states := strings.Split(field, ",")
} for i, state := range states {
stdout, err := run("-n", "-a", "list", "chain", tableFamily, TableName, BasicBeforeChain) value, err := strconv.ParseUint(state, 0, 64)
if err != nil { if err != nil {
return nil, err continue
} }
existing := requiredPortRules(stdout) switch value {
missing, staleHandles := requiredPortChanges(existing, ports) case 1:
for _, port := range missing { states[i] = "invalid"
commands = append(commands, requiredPortCommand(tableFamily, port)) case 2:
} states[i] = "established"
for _, handle := range staleHandles { case 4:
commands = append(commands, []string{"delete", "rule", tableFamily, TableName, BasicBeforeChain, "handle", handle}) states[i] = "related"
} case 8:
} states[i] = "new"
return commands, nil case 64:
} states[i] = "untracked"
}
type requiredPortRule struct { }
Key string slices.Sort(states)
Handle string fields[index] = strings.Join(states, ",")
} }
if prefix, err := netip.ParsePrefix(field); err == nil {
func requiredPortChanges(existing []requiredPortRule, desiredPorts []firewall.PortWhitelist) ([]firewall.PortWhitelist, []string) { prefix = prefix.Masked()
desired := firewall.PortWhitelistMap(desiredPorts) fields[index] = prefix.String()
existingKeys := make(map[string]struct{}, len(existing)) if prefix.Bits() == prefix.Addr().BitLen() {
for _, rule := range existing { fields[index] = prefix.Addr().String()
existingKeys[rule.Key] = struct{}{} }
}
missing := make([]firewall.PortWhitelist, 0)
for _, port := range desiredPorts {
if _, exists := existingKeys[firewall.PortWhitelistKey(port)]; !exists {
missing = append(missing, port)
}
}
kept := make(map[string]struct{}, len(existing))
staleHandles := make([]string, 0)
for _, rule := range existing {
if _, wanted := desired[rule.Key]; wanted {
if _, alreadyKept := kept[rule.Key]; !alreadyKept {
kept[rule.Key] = struct{}{}
continue
} }
} }
staleHandles = append(staleHandles, rule.Handle) return strings.Join(fields, " ")
} }
return missing, staleHandles wanted := canonical(expression)
}
func requiredPortRules(output string) []requiredPortRule {
rules := make([]requiredPortRule, 0)
marker := `comment "` + requiredPortComment + `"`
for _, line := range strings.Split(output, "\n") { for _, line := range strings.Split(output, "\n") {
if !strings.Contains(line, marker) { if canonical(line) == wanted {
continue return true
}
handleIndex := strings.LastIndex(line, "# handle ")
if handleIndex < 0 {
continue
}
handle := strings.TrimSpace(line[handleIndex+len("# handle "):])
if _, err := strconv.ParseUint(handle, 10, 64); err != nil {
continue
}
fields := strings.Fields(line[:handleIndex])
for index := 0; index+2 < len(fields); index++ {
protocol := fields[index]
if (protocol != "tcp" && protocol != "udp") || fields[index+1] != "dport" {
continue
}
port, err := strconv.Atoi(fields[index+2])
if err != nil || port < 1 || port > 65535 {
break
}
rules = append(rules, requiredPortRule{
Key: firewall.PortWhitelistKey(firewall.PortWhitelist{Protocol: protocol, Port: strconv.Itoa(port)}), Handle: handle,
})
break
} }
} }
return rules return false
}
func (m *Manager) updateSetting(key, value string) error {
if m != nil && m.UpdateSetting != nil {
return m.UpdateSetting(key, value)
}
return nil
}
func (m *Manager) loadRequiredPorts() ([]firewall.PortWhitelist, error) {
if m != nil && m.LoadRequiredPorts != nil {
return m.LoadRequiredPorts()
}
return nil, fmt.Errorf("load required firewall ports is not configured")
} }
func Bind() error { func Bind() error {
@@ -1,16 +0,0 @@
package nftables_helper
import "errors"
var ErrChainNotFound = errors.New("nftables chain is not initialized")
func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) {
output, exists, err := readNftObject(run, "-a", "list", "chain", family, table, chain)
if err != nil {
return "", err
}
if !exists {
return "", ErrChainNotFound
}
return output, nil
}

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