mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-12 16:00:33 +00:00
Compare commits
39
Commits
release-2.3.0
...
v2.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65243c68c4 | ||
|
|
256e79ca81 | ||
|
|
4861eb69cb | ||
|
|
fb8cf15537 | ||
|
|
c4a6791271 | ||
|
|
f58e147636 | ||
|
|
387e9fbeed | ||
|
|
4eb627bc79 | ||
|
|
850c86229c | ||
|
|
f984917a66 | ||
|
|
8588217fbf | ||
|
|
a2307c5f64 | ||
|
|
5923290de8 | ||
|
|
1c994fba4a | ||
|
|
415ab96aab | ||
|
|
19bb823b05 | ||
|
|
3a5371652e | ||
|
|
a267b4148a | ||
|
|
36a01eb60d | ||
|
|
65f6fdd045 | ||
|
|
75b60b32e4 | ||
|
|
6cb65e2290 | ||
|
|
0bad1b471f | ||
|
|
3814525edd | ||
|
|
8162dd1856 | ||
|
|
e833787020 | ||
|
|
673ffac516 | ||
|
|
e864610015 | ||
|
|
78402e1b7d | ||
|
|
782bc1e67c | ||
|
|
86e4ed6f64 | ||
|
|
ee8bac39af | ||
|
|
fe742b9f41 | ||
|
|
9a5bd9bcba | ||
|
|
b9c8e39560 | ||
|
|
6b20ff0b13 | ||
|
|
89bd32b6d4 | ||
|
|
005f240fb7 | ||
|
|
75da53e374 |
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"`
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+271
-900
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+304
-320
@@ -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
@@ -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 != "" {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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": [],
|
||||||
|
|||||||
@@ -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
@@ -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 }}"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 }}"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 }}"
|
||||||
|
|||||||
@@ -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
@@ -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 }}"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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).
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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...)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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}},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, ",")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 == "" {
|
||||||
|
|||||||
+167
-97
@@ -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":
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user